diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..c5f140320 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @oschwartz10612 @miloschwartz diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fff21995d..54ae22194 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -266,7 +266,7 @@ jobs: - name: Install Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: - go-version: 1.24 + go-version: 1.25 - name: Update version in package.json run: | @@ -415,7 +415,7 @@ jobs: - name: Install cosign # cosign is used to sign and verify container images (key and keyless) - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Dual-sign and verify (GHCR & Docker Hub) # Sign each image by digest using keyless (OIDC) and key-based signing, diff --git a/.github/workflows/mirror.yaml b/.github/workflows/mirror.yaml index d6dfdb8fb..f60922d21 100644 --- a/.github/workflows/mirror.yaml +++ b/.github/workflows/mirror.yaml @@ -23,7 +23,7 @@ jobs: skopeo --version - name: Install cosign - uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Input check run: | diff --git a/README.md b/README.md index bac7b7e56..f11196f77 100644 --- a/README.md +++ b/README.md @@ -35,43 +35,53 @@ -

- - We're Hiring! - -

-

Get started with Pangolin at app.pangolin.net

-Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control. +Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls. ## Installation -- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin. -- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer. +- Get started for free with [Pangolin Cloud](https://app.pangolin.net/). +- Or, check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to self-host Pangolin. + - Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer. - +Pangolin ## Deployment Options -| | Description | -|-----------------|--------------| -| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. | -| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. | -| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. | +- **Pangolin Cloud** — Fully managed service - no infrastructure required. +- **Self-Host: Community Edition** — Free, open source, and licensed under AGPL-3. +- **Self-Host: Enterprise Edition** — Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue. ## Key Features -| | | -|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| -| **Connect remote networks with sites**

Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | | -| **Browser-based reverse proxy access**

Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | | -| **Client-based private resource access**

Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | | -| **Zero-trust granular access**

Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | | +### Connect remote networks with sites and NAT traversal + +Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform. + +Sites + +### Browser-based reverse proxy access + +Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. + +Reverse proxy access + +### Client-based private resource access + +Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network. + +Private resources + +### Give users and roles access to resources + +Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define. + +Users from identity provider with roles ## Download Clients @@ -87,7 +97,7 @@ Download the Pangolin client for your platform: ### Sign up now -Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available. +Create a free account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. ### Check out the docs @@ -102,7 +112,3 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License ## Contributions Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. - ---- - -WireGuard® is a registered trademark of Jason A. Donenfeld. diff --git a/SECURITY.md b/SECURITY.md index 6b7372c24..02fbe77d7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,7 +3,7 @@ If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us: 1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk. -2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include: +2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) with the following information: - Description and location of the vulnerability. - Potential impact of the vulnerability. diff --git a/config/db/.gitignore b/config/db/.gitignore new file mode 100644 index 000000000..9d4b1bb9c --- /dev/null +++ b/config/db/.gitignore @@ -0,0 +1 @@ +*-journal diff --git a/install/config.go b/install/config.go index 548e2ab33..d03415e30 100644 --- a/install/config.go +++ b/install/config.go @@ -99,11 +99,6 @@ func ReadAppConfig(configPath string) (*AppConfigValues, error) { return values, nil } -// findPattern finds the start of a pattern in a string -func findPattern(s, pattern string) int { - return bytes.Index([]byte(s), []byte(pattern)) -} - func copyDockerService(sourceFile, destFile, serviceName string) error { // Read source file sourceData, err := os.ReadFile(sourceFile) @@ -187,7 +182,7 @@ func backupConfig() error { return nil } -func MarshalYAMLWithIndent(data any, indent int) ([]byte, error) { +func MarshalYAMLWithIndent(data any, indent int) (resp []byte, err error) { buffer := new(bytes.Buffer) encoder := yaml.NewEncoder(buffer) encoder.SetIndent(indent) @@ -196,7 +191,12 @@ func MarshalYAMLWithIndent(data any, indent int) ([]byte, error) { return nil, err } - defer encoder.Close() + defer func() { + if cerr := encoder.Close(); cerr != nil && err == nil { + err = cerr + } + }() + return buffer.Bytes(), nil } diff --git a/install/config/crowdsec/traefik_config.yml b/install/config/crowdsec/traefik_config.yml index 198693ef8..c1145934d 100644 --- a/install/config/crowdsec/traefik_config.yml +++ b/install/config/crowdsec/traefik_config.yml @@ -81,11 +81,19 @@ entryPoints: transport: respondingTimeouts: readTimeout: "30m" + http3: + advertisedPort: 443 http: tls: certResolver: "letsencrypt" - middlewares: + middlewares: - crowdsec@file + encodedCharacters: + allowEncodedSlash: true + allowEncodedQuestionMark: true serversTransport: - insecureSkipVerify: true \ No newline at end of file + insecureSkipVerify: true + +ping: + entryPoint: "web" diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index c0206e5bf..505089bed 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -38,6 +38,7 @@ services: - 51820:51820/udp - 21820:21820/udp - 443:443 + - 443:443/udp # For http3 QUIC if desired - 80:80 {{end}} traefik: diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 0709b4611..45f5ebb07 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -40,6 +40,8 @@ entryPoints: transport: respondingTimeouts: readTimeout: "30m" + http3: + advertisedPort: 443 http: tls: certResolver: "letsencrypt" diff --git a/install/go.mod b/install/go.mod index da73eec0f..005a079df 100644 --- a/install/go.mod +++ b/install/go.mod @@ -3,7 +3,7 @@ module installer go 1.25.0 require ( - github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 golang.org/x/term v0.41.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/install/go.sum b/install/go.sum index e0b2a6c5e..b67ae57e5 100644 --- a/install/go.sum +++ b/install/go.sum @@ -14,8 +14,8 @@ github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGs github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= -github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= diff --git a/install/input.go b/install/input.go index 8b444ecb9..93739e0d0 100644 --- a/install/input.go +++ b/install/input.go @@ -85,33 +85,6 @@ func readString(prompt string, defaultValue string) string { return value } -func readStringNoDefault(prompt string) string { - var value string - - for { - input := huh.NewInput(). - Title(prompt). - Value(&value). - Validate(func(s string) error { - if s == "" { - return fmt.Errorf("this field is required") - } - return nil - }) - - err := runField(input) - handleAbort(err) - - if value != "" { - // Print the answer so it remains visible in terminal history - if !isAccessibleMode() { - fmt.Printf("%s: %s\n", prompt, value) - } - return value - } - } -} - func readPassword(prompt string) string { var value string diff --git a/install/main.go b/install/main.go index 9de332b60..a38d78fc6 100644 --- a/install/main.go +++ b/install/main.go @@ -8,12 +8,12 @@ import ( "io" "io/fs" "net" - "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime" + "strconv" "strings" "text/template" "time" @@ -90,6 +90,13 @@ func main() { var config Config var alreadyInstalled = false + // Determine installation directory + installDir := findOrSelectInstallDirectory() + if err := os.Chdir(installDir); err != nil { + fmt.Printf("Error changing to installation directory: %v\n", err) + os.Exit(1) + } + // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { config = collectUserInput() @@ -287,6 +294,117 @@ func main() { fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } +func hasExistingInstall(dir string) bool { + configPath := filepath.Join(dir, "config", "config.yml") + _, err := os.Stat(configPath) + return err == nil +} + +func findOrSelectInstallDirectory() string { + const defaultInstallDir = "/opt/pangolin" + + // Get current working directory + cwd, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting current directory: %v\n", err) + os.Exit(1) + } + + // 1. Check current directory for existing install + if hasExistingInstall(cwd) { + fmt.Printf("Found existing Pangolin installation in current directory: %s\n", cwd) + return cwd + } + + // 2. Check default location (/opt/pangolin) for existing install + if cwd != defaultInstallDir && hasExistingInstall(defaultInstallDir) { + fmt.Printf("\nFound existing Pangolin installation at: %s\n", defaultInstallDir) + if readBool(fmt.Sprintf("Would you like to use the existing installation at %s?", defaultInstallDir), true) { + return defaultInstallDir + } + } + + // 3. No existing install found, prompt for installation directory + fmt.Println("\n=== Installation Directory ===") + fmt.Println("No existing Pangolin installation detected.") + + installDir := readString("Enter the installation directory", defaultInstallDir) + + // Expand ~ to home directory if present + if strings.HasPrefix(installDir, "~") { + home, err := os.UserHomeDir() + if err != nil { + fmt.Printf("Error getting home directory: %v\n", err) + os.Exit(1) + } + installDir = filepath.Join(home, installDir[1:]) + } + + // Convert to absolute path + absPath, err := filepath.Abs(installDir) + if err != nil { + fmt.Printf("Error resolving path: %v\n", err) + os.Exit(1) + } + installDir = absPath + + // Check if directory exists + if _, err := os.Stat(installDir); os.IsNotExist(err) { + // Directory doesn't exist, create it + if readBool(fmt.Sprintf("Directory %s does not exist. Create it?", installDir), true) { + if err := os.MkdirAll(installDir, 0755); err != nil { + fmt.Printf("Error creating directory: %v\n", err) + os.Exit(1) + } + fmt.Printf("Created directory: %s\n", installDir) + + // Offer to change ownership if running via sudo + changeDirectoryOwnership(installDir) + } else { + fmt.Println("Installation cancelled.") + os.Exit(0) + } + } + + fmt.Printf("Installation directory: %s\n", installDir) + return installDir +} + +func changeDirectoryOwnership(dir string) { + // Check if we're running via sudo by looking for SUDO_USER + sudoUser := os.Getenv("SUDO_USER") + if sudoUser == "" || os.Geteuid() != 0 { + return + } + + sudoUID := os.Getenv("SUDO_UID") + sudoGID := os.Getenv("SUDO_GID") + + if sudoUID == "" || sudoGID == "" { + return + } + + fmt.Printf("\nRunning as root via sudo (original user: %s)\n", sudoUser) + if readBool(fmt.Sprintf("Would you like to change ownership of %s to user '%s'? This makes it easier to manage config files without sudo.", dir, sudoUser), true) { + uid, err := strconv.Atoi(sudoUID) + if err != nil { + fmt.Printf("Warning: Could not parse SUDO_UID: %v\n", err) + return + } + gid, err := strconv.Atoi(sudoGID) + if err != nil { + fmt.Printf("Warning: Could not parse SUDO_GID: %v\n", err) + return + } + + if err := os.Chown(dir, uid, gid); err != nil { + fmt.Printf("Warning: Could not change ownership: %v\n", err) + } else { + fmt.Printf("Changed ownership of %s to %s\n", dir, sudoUser) + } + } +} + func podmanOrDocker() SupportedContainer { inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker") @@ -430,9 +548,9 @@ func createConfigFiles(config Config) error { } // Walk through all embedded files - err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err + err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, walkErr error) (err error) { + if walkErr != nil { + return walkErr } // Skip the root fs directory itself @@ -483,7 +601,11 @@ func createConfigFiles(config Config) error { if err != nil { return fmt.Errorf("failed to create %s: %v", path, err) } - defer outFile.Close() + defer func() { + if cerr := outFile.Close(); cerr != nil && err == nil { + err = cerr + } + }() // Execute template if err := tmpl.Execute(outFile, config); err != nil { @@ -499,18 +621,26 @@ func createConfigFiles(config Config) error { return nil } -func copyFile(src, dst string) error { +func copyFile(src, dst string) (err error) { source, err := os.Open(src) if err != nil { return err } - defer source.Close() + defer func() { + if cerr := source.Close(); cerr != nil && err == nil { + err = cerr + } + }() destination, err := os.Create(dst) if err != nil { return err } - defer destination.Close() + defer func() { + if cerr := destination.Close(); cerr != nil && err == nil { + err = cerr + } + }() _, err = io.Copy(destination, source) return err @@ -622,32 +752,6 @@ func generateRandomSecretKey() string { return base64.StdEncoding.EncodeToString(secret) } -func getPublicIP() string { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get("https://ifconfig.io/ip") - if err != nil { - return "" - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "" - } - - ip := strings.TrimSpace(string(body)) - - // Validate that it's a valid IP address - if net.ParseIP(ip) != nil { - return ip - } - - return "" -} - // Run external commands with stdio/stderr attached. func run(name string, args ...string) error { cmd := exec.Command(name, args...) diff --git a/license.py b/license_header_checker.py similarity index 100% rename from license.py rename to license_header_checker.py diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 10b83f38d..10204713a 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -148,6 +148,11 @@ "createLink": "Създаване на връзка", "resourcesNotFound": "Не са намерени ресурси", "resourceSearch": "Търсене на ресурси", + "machineSearch": "Търсене на машини", + "machinesSearch": "Търсене на клиенти на машини...", + "machineNotFound": "Не са намерени машини", + "userDeviceSearch": "Търсене на устройства на потребителя", + "userDevicesSearch": "Търсене на устройства на потребителя...", "openMenu": "Отваряне на менюто", "resource": "Ресурс", "title": "Заглавие", @@ -323,6 +328,54 @@ "apiKeysDelete": "Изтрийте API ключа", "apiKeysManage": "Управление на API ключове", "apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API", + "provisioningKeysTitle": "Ключ за осигуряване", + "provisioningKeysManage": "Управление на ключове за осигуряване", + "provisioningKeysDescription": "Ключовете за осигуряване се използват за удостоверяване на автоматичното осигуряване на сайта за вашата организация.", + "provisioningManage": "Осигуряване", + "provisioningDescription": "Управление на ключовете за осигуряване и преглед на чаканещите сайтове за одобрение.", + "pendingSites": "Чаканещи сайтове", + "siteApproveSuccess": "Сайтът е одобрен успешно", + "siteApproveError": "Грешка при одобряването на сайта", + "provisioningKeys": "Ключове за осигуряване", + "searchProvisioningKeys": "Търсене на ключове за осигуряване...", + "provisioningKeysAdd": "Генериране на ключ за осигуряване", + "provisioningKeysErrorDelete": "Грешка при изтриване на ключ за осигуряване", + "provisioningKeysErrorDeleteMessage": "Грешка при изтриване на ключ за осигуряване", + "provisioningKeysQuestionRemove": "Сигурни ли сте, че искате да премахнете този ключ за осигуряване от организацията?", + "provisioningKeysMessageRemove": "След като бъде премахнат, ключът няма да бъде използван за осигуряване на сайтове.", + "provisioningKeysDeleteConfirm": "Потвърдете изтриването на ключ за осигуряване", + "provisioningKeysDelete": "Изтриване на ключ за осигуряване", + "provisioningKeysCreate": "Генериране на ключ за осигуряване", + "provisioningKeysCreateDescription": "Генерирайте нов ключ за осигуряване за организацията", + "provisioningKeysSeeAll": "Вижте всички ключове за осигуряване", + "provisioningKeysSave": "Запазете ключа за осигуряване", + "provisioningKeysSaveDescription": "Ще можете да видите това само веднъж. Копирайте го на сигурно място.", + "provisioningKeysErrorCreate": "Грешка при създаване на ключ за осигуряване", + "provisioningKeysList": "Нов ключ за осигуряване", + "provisioningKeysMaxBatchSize": "Максимален размер на пакет", + "provisioningKeysUnlimitedBatchSize": "Неограничен размер на партида (без лимит)", + "provisioningKeysMaxBatchUnlimited": "Неограничено", + "provisioningKeysMaxBatchSizeInvalid": "Въведете валиден максимален размер на партида (1–1,000,000).", + "provisioningKeysValidUntil": "Валиден до", + "provisioningKeysValidUntilHint": "Оставете празно за неограничено валидност.", + "provisioningKeysValidUntilInvalid": "Въведете валидна дата и час.", + "provisioningKeysNumUsed": "Брой използвания", + "provisioningKeysLastUsed": "Последно използван", + "provisioningKeysNoExpiry": "Без изтичане", + "provisioningKeysNeverUsed": "Никога", + "provisioningKeysEdit": "Редактиране на ключ за осигуряване", + "provisioningKeysEditDescription": "Актуализирайте максималния размер на партида и времето на изтичане за този ключ.", + "provisioningKeysApproveNewSites": "Одобрете нови сайтове", + "provisioningKeysApproveNewSitesDescription": "Автоматично одобряване на сайтове, които се регистрират с този ключ.", + "provisioningKeysUpdateError": "Грешка при актуализирането на ключа за осигуряване", + "provisioningKeysUpdated": "Ключът за осигуряване е актуализиран", + "provisioningKeysUpdatedDescription": "Вашите промени бяха запазени.", + "provisioningKeysBannerTitle": "Ключове за осигуряване на сайта", + "provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте със съединителя Newt за автоматично създаване на сайтове при първоначално стартиране - не е необходимо да се създават отделни идентификационни данни за всеки сайт.", + "provisioningKeysBannerButtonText": "Научете повече", + "pendingSitesBannerTitle": "Чакащи сайтове", + "pendingSitesBannerDescription": "Сайтовете, които се свързват с ключ за осигуряване, ще се появят тук за преглед.", + "pendingSitesBannerButtonText": "Научете повече", "apiKeysSettings": "Настройки на {apiKeyName}", "userTitle": "Управление на всички потребители", "userDescription": "Преглед и управление на всички потребители в системата", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Неуспешно активиране на лицензионния ключ", "licenseErrorKeyActivateDescription": "Възникна грешка при активирането на лицензионния ключ.", "licenseAbout": "Относно лицензите", + "licenseBannerTitle": "Активирайте своята корпоративна лицензия", + "licenseBannerDescription": "Отключете корпоративните функции за вашият хостинг на Pangolin. Закупете лицензионен ключ, за да активирате премиум възможности, след това го добавете по-долу.", + "licenseBannerGetLicense": "Вземете лиценз", + "licenseBannerViewDocs": "Преглед на документацията", "communityEdition": "Комюнити издание", "licenseAboutDescription": "Това е за бизнес и корпоративни потребители, които използват Pangolin в търговска среда. Ако използвате Pangolin за лична употреба, можете да игнорирате този раздел.", "licenseKeyActivated": "Лицензионният ключ е активиран", @@ -509,9 +566,12 @@ "userSaved": "Потребителят е запазен", "userSavedDescription": "Потребителят беше актуализиран.", "autoProvisioned": "Автоматично предоставено", + "autoProvisionSettings": "Настройки за автоматично осигуряване", "autoProvisionedDescription": "Позволете този потребител да бъде автоматично управляван от доставчик на идентификационни данни", "accessControlsDescription": "Управлявайте какво може да достъпва и прави този потребител в организацията", "accessControlsSubmit": "Запазване на контролите за достъп", + "singleRolePerUserPlanNotice": "Вашият план поддържа само една роля на потребител.", + "singleRolePerUserEditionNotice": "Това издание поддържа само една роля на потребител.", "roles": "Роли", "accessUsersRoles": "Управление на потребители и роли", "accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до организацията", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт", "targetErrorNoSite": "Няма избран сайт", "targetErrorNoSiteDescription": "Моля, изберете сайт за целта", + "targetTargetsCleared": "Мишените са премахнати", + "targetTargetsClearedDescription": "Всички цели са били премахнати от този ресурс", "targetCreated": "Целта е създадена", "targetCreatedDescription": "Целта беше успешно създадена", "targetErrorCreate": "Неуспешно създаване на целта", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.", "setupTokenRequired": "Необходим е конфигурационен токен", "actionUpdateSite": "Актуализиране на сайт", + "actionResetSiteBandwidth": "Нулиране на честотната лента на организацията", "actionListSiteRoles": "Изброяване на позволените роли за сайта", "actionCreateResource": "Създаване на ресурс", "actionDeleteResource": "Изтриване на ресурс", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Изтрийте потребител", "actionListUsers": "Изброяване на потребители", "actionAddUserRole": "Добавяне на роля на потребител", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Задайте роли на потребители", "actionGenerateAccessToken": "Генериране на токен за достъп", "actionDeleteAccessToken": "Изтриване на токен за достъп", "actionListAccessTokens": "Изброяване на токени за достъп", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Роли", "sidebarShareableLinks": "Връзки", "sidebarApiKeys": "API ключове", + "sidebarProvisioning": "Осигуряване", "sidebarSettings": "Настройки", "sidebarAllUsers": "Всички потребители", "sidebarIdentityProviders": "Идентификационни доставчици", @@ -1890,6 +1954,40 @@ "exitNode": "Изходен възел", "country": "Държава", "rulesMatchCountry": "Понастоящем на базата на изходния IP", + "region": "Регион", + "selectRegion": "Изберете регион", + "searchRegions": "Търсене на региони...", + "noRegionFound": "Регионът не е намерен.", + "rulesMatchRegion": "Изберете регионална групировка на държави", + "rulesErrorInvalidRegion": "Невалиден регион", + "rulesErrorInvalidRegionDescription": "Моля, изберете валиден регион.", + "regionAfrica": "Африка", + "regionNorthernAfrica": "Северна Африка", + "regionEasternAfrica": "Източна Африка", + "regionMiddleAfrica": "Централна Африка", + "regionSouthernAfrica": "Южна Африка", + "regionWesternAfrica": "Западна Африка", + "regionAmericas": "Америките", + "regionCaribbean": "Карибите", + "regionCentralAmerica": "Централна Америка", + "regionSouthAmerica": "Южна Америка", + "regionNorthernAmerica": "Северна Америка", + "regionAsia": "Азия", + "regionCentralAsia": "Централна Азия", + "regionEasternAsia": "Източна Азия", + "regionSouthEasternAsia": "Югоизточна Азия", + "regionSouthernAsia": "Южна Азия", + "regionWesternAsia": "Западна Азия", + "regionEurope": "Европа", + "regionEasternEurope": "Източна Европа", + "regionNorthernEurope": "Северна Европа", + "regionSouthernEurope": "Южна Европа", + "regionWesternEurope": "Западна Европа", + "regionOceania": "Океания", + "regionAustraliaAndNewZealand": "Австралия и Нова Зеландия", + "regionMelanesia": "Меланезия", + "regionMicronesia": "Микронезия", + "regionPolynesia": "Полинезия", "managedSelfHosted": { "title": "Управлявано Самостоятелно-хоствано", "description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри", @@ -1938,6 +2036,25 @@ "invalidValue": "Невалидна стойност", "idpTypeLabel": "Тип на доставчика на идентичност", "roleMappingExpressionPlaceholder": "напр.: contains(groups, 'admin') && 'Admin' || 'Member'", + "roleMappingModeFixedRoles": "Фиксирани роли", + "roleMappingModeMappingBuilder": "Строител на карти", + "roleMappingModeRawExpression": "Необработено израз", + "roleMappingFixedRolesPlaceholderSelect": "Изберете една или повече роли", + "roleMappingFixedRolesPlaceholderFreeform": "Въведете имена на роли (точно съвпадение на организацията)", + "roleMappingFixedRolesDescriptionSameForAll": "Присвойте същият набор от роли на всеки автоматично осигурен потребител.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "За стандартните политики въведете имена на роли, които съществуват във всяка организация, където е осигурен потребител. Имената трябва да съвпадат точно.", + "roleMappingClaimPath": "Път на иск", + "roleMappingClaimPathPlaceholder": "групи", + "roleMappingClaimPathDescription": "Път в съдържанието на маркера, който съдържа изходни стойности (например групи).", + "roleMappingMatchValue": "Съвпадение на стойност", + "roleMappingAssignRoles": "Присвояване на роли", + "roleMappingAddMappingRule": "Добавяне на правило за картироване", + "roleMappingRawExpressionResultDescription": "Изразът трябва да бъде оценен на низ или масив от низове.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Изразът трябва да бъде оценен на низ (едно име на роля).", + "roleMappingMatchValuePlaceholder": "Съвпадение на стойност (например: администратор)", + "roleMappingAssignRolesPlaceholderFreeform": "Въведете имена на роли (точно по организация)", + "roleMappingBuilderFreeformRowHint": "Имената на ролите трябва да съвпадат с роля във всяка целева организация.", + "roleMappingRemoveRule": "Премахни", "idpGoogleConfiguration": "Конфигурация на Google", "idpGoogleConfigurationDescription": "Конфигурирайте Google OAuth2 идентификационни данни", "idpGoogleClientIdDescription": "Google OAuth2 идентификационен клиент", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Изберете домейн за страницата за удостоверяване на организацията", "domainPickerProvidedDomain": "Предоставен домейн", "domainPickerFreeProvidedDomain": "Безплатен предоставен домейн", + "domainPickerFreeDomainsPaidFeature": "Предоставените домейни са платена функция. Абонирайте се, за да получите домейн, включен във вашия план - няма нужда да използвате вашия собствен.", "domainPickerVerified": "Проверено", "domainPickerUnverified": "Непроверено", + "domainPickerManual": "Ръчно", "domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.", "domainPickerError": "Грешка", "domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията", @@ -2235,7 +2354,7 @@ "description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка." } }, - "personalUseOnly": "Само за лична употреба (безплатен лиценз — без плащане)", + "personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)", "buttons": { "continueToCheckout": "Продължете към плащане" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп", "logRetentionActionLabel": "Задържане на логове за действия", "logRetentionActionDescription": "Колко дълго да се задържат логовете за действия", + "logRetentionConnectionLabel": "Запазване на дневниците на връзките", + "logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките", "logRetentionDisabled": "Деактивирано", "logRetention3Days": "3 дни", "logRetention7Days": "7 дни", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Край на следващата година", "actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация", "accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация", + "connectionLogs": "Логове на връзката", + "connectionLogsDescription": "Вижте логовете на връзките за тунелите в тази организация", + "sidebarLogsConnection": "Логове на връзката", + "sidebarLogsStreaming": "Потоци", + "sourceAddress": "Източен адрес", + "destinationAddress": "Адрес на дестинация", + "duration": "Продължителност", "licenseRequiredToUse": "Изисква се лиценз за Enterprise Edition или Pangolin Cloud за използване на тази функция. Резервирайте демонстрация или пробен POC.", "ossEnterpriseEditionRequired": "Enterprise Edition е необходим за използване на тази функция. Тази функция също е налична в Pangolin Cloud. Резервирайте демонстрация или пробен POC.", "certResolver": "Решавач на сертификати", @@ -2487,6 +2615,9 @@ "machineClients": "Машинни клиенти", "install": "Инсталирай", "run": "Изпълни", + "envFile": "Файл за среда", + "serviceFile": "Файл за услуга", + "enableAndStart": "Активиране и стартиране", "clientNameDescription": "Показваното име на клиента, което може да се промени по-късно.", "clientAddress": "Клиентски адрес (Разширено)", "setupFailedToFetchSubnet": "Неуспешно извличане на подмрежа по подразбиране", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Редактирайте ролята и активирайте опцията 'Изискване на одобрения за устройства'. Потребители с тази роля ще трябва администраторско одобрение за нови устройства.", "approvalsEmptyStatePreviewDescription": "Преглед: Когато е активирано, чакащите заявки за устройства ще се появят тук за преглед", "approvalsEmptyStateButtonText": "Управлявайте роли", - "domainErrorTitle": "Имаме проблем с проверката на вашия домейн" + "domainErrorTitle": "Имаме проблем с проверката на вашия домейн", + "idpAdminAutoProvisionPoliciesTabHint": "Конфигурирайте картографирането на ролите и организационните политики на раздела Настройки за автоматично осигуряване.", + "streamingTitle": "Събитийни потоци", + "streamingDescription": "Предавайте събития от вашата организация до външни дестинации в реално време.", + "streamingUnnamedDestination": "Неименувана дестинация", + "streamingNoUrlConfigured": "Не е конфигуриран URL", + "streamingAddDestination": "Добавяне на дестинация", + "streamingHttpWebhookTitle": "HTTP Уеб хук", + "streamingHttpWebhookDescription": "Изпратете събития до всяка HTTP крайна точка с гъвкаво удостоверяване и шаблониране.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Предавайте събития на хранилище, съвместимо с S3. Очаквайте скоро.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.", + "streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.", + "streamingFailedToLoad": "Неуспешно зареждане на дестинации", + "streamingUnexpectedError": "Възникна неочаквана грешка.", + "streamingFailedToUpdate": "Неуспешно актуализиране на дестинация", + "streamingDeletedSuccess": "Дестинацията беше изтрита успешно", + "streamingFailedToDelete": "Неуспешно изтриване на дестинацията", + "streamingDeleteTitle": "Изтриване на дестинация", + "streamingDeleteButtonText": "Изтриване на дестинация", + "streamingDeleteDialogAreYouSure": "Сигурни ли сте, че искате да изтриете", + "streamingDeleteDialogThisDestination": "тази дестинация", + "streamingDeleteDialogPermanentlyRemoved": "? Всички конфигурации ще бъдат премахнати завинаги.", + "httpDestEditTitle": "Редактиране на дестинация", + "httpDestAddTitle": "Добавяне на HTTP дестинация", + "httpDestEditDescription": "Актуализирайте конфигурацията за този HTTP събитий.", + "httpDestAddDescription": "Конфигурирайте нов HTTP крайна точка, за да получавате събития на вашата организация.", + "httpDestTabSettings": "Настройки", + "httpDestTabHeaders": "Заглавки", + "httpDestTabBody": "Тяло", + "httpDestTabLogs": "Логове", + "httpDestNamePlaceholder": "Моята HTTP дестинация", + "httpDestUrlLabel": "Дестинация URL", + "httpDestUrlErrorHttpRequired": "URL адресът трябва да използва http или https", + "httpDestUrlErrorHttpsRequired": "SSL е необходимо за облачни инсталации", + "httpDestUrlErrorInvalid": "Въведете валиден URL (напр. https://example.com/webhook)", + "httpDestAuthTitle": "Удостоверяване", + "httpDestAuthDescription": "Изберете как заявленията ви се удостоверяват.", + "httpDestAuthNoneTitle": "Без удостоверяване", + "httpDestAuthNoneDescription": "Изпращане на заявки без заглавие за удостоверяване.", + "httpDestAuthBearerTitle": "Bearer Токен", + "httpDestAuthBearerDescription": "Добавя заглавие Authorization: Bearer '' към всяка заявка.", + "httpDestAuthBearerPlaceholder": "Вашият API ключ или токен", + "httpDestAuthBasicTitle": "Основно удостоверяване", + "httpDestAuthBasicDescription": "Добавя заглавие Authorization: Basic ''. Осигурете идентификационни данни като потребителско име:парола.", + "httpDestAuthBasicPlaceholder": "потребителско име:парола", + "httpDestAuthCustomTitle": "Персонализирано заглавие", + "httpDestAuthCustomDescription": "Посочете персонализирано име и стойност на заглавието за удостоверяване (например X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Имя на заглавието (напр. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Стойност на заглавието", + "httpDestCustomHeadersTitle": "Персонализирани заглавия за HTTP", + "httpDestCustomHeadersDescription": "Добавяне на персонализирани заглавия към всяка изходяща заявка. Полезно за статични токени или персонални Content-Type. По подразбиране се изпраща Content-Type: application/json.", + "httpDestNoHeadersConfigured": "Персонализирани заглавия не са конфигурирани. Кликнете \"Добавяне на заглавие\" да добавите такова.", + "httpDestHeaderNamePlaceholder": "Име на заглавието", + "httpDestHeaderValuePlaceholder": "Стойност на заглавието", + "httpDestAddHeader": "Добавяне на заглавие", + "httpDestBodyTemplateTitle": "Шаблон на персонализирано тяло", + "httpDestBodyTemplateDescription": "Управлявайте структурата на JSON съобщението, изпратено до вашата крайна точка. Ако е деактивирано, по подразбиране се изпраща JSON обект за всяко събитие.", + "httpDestEnableBodyTemplate": "Активиране на персонализиран шаблон на тяло", + "httpDestBodyTemplateLabel": "Шаблон за тяло (JSON)", + "httpDestBodyTemplateHint": "Използвайте шаблонни променливи за позоваване на полетата на събитията в съобщението си.", + "httpDestPayloadFormatTitle": "Формат на полезния товар", + "httpDestPayloadFormatDescription": "Как се сериализират събитията във всеки заявка.", + "httpDestFormatJsonArrayTitle": "JSON масив", + "httpDestFormatJsonArrayDescription": "Една заявка на партида, тялото е JSON масив. Съвместим с повечето общи уеб куки и Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Една заявка на партида, тялото е ново линии отделени JSON — един обект на ред, няма външен масив. Изисквано от Splunk HEC, Elastic / OpenSearch и Grafana.", + "httpDestFormatSingleTitle": "Едно събитие на заявка", + "httpDestFormatSingleDescription": "Изпращат се отделни HTTP POST за всяко индивидуално събитие. Използвайте само за крайни точки, които не могат да обработват партиди.", + "httpDestLogTypesTitle": "Видове логове", + "httpDestLogTypesDescription": "Изберете кои видове журнални записи ще се предават към тази дестинация. Предаването ще се прави само за активирани видове журнални записи.", + "httpDestAccessLogsTitle": "Логове за достъп", + "httpDestAccessLogsDescription": "Опити за достъп до ресурс, включително удостоверени и отказани заявки.", + "httpDestActionLogsTitle": "Логове на действия", + "httpDestActionLogsDescription": "Административни действия, извършени от потребители в организацията.", + "httpDestConnectionLogsTitle": "Логове на връзката", + "httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.", + "httpDestRequestLogsTitle": "Заявки за логове", + "httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.", + "httpDestSaveChanges": "Запази промените", + "httpDestCreateDestination": "Създаване на дестинация", + "httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно", + "httpDestCreatedSuccess": "Дестинацията беше създадена успешно", + "httpDestUpdateFailed": "Неуспешно актуализиране на дестинацията", + "httpDestCreateFailed": "Неуспешно създаване на дестинацията" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index ea12fe535..5b7122867 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -148,6 +148,11 @@ "createLink": "Vytvořit odkaz", "resourcesNotFound": "Nebyly nalezeny žádné zdroje", "resourceSearch": "Vyhledat zdroje", + "machineSearch": "Vyhledávací stroje", + "machinesSearch": "Hledat klienty stroje...", + "machineNotFound": "Nebyly nalezeny žádné stroje", + "userDeviceSearch": "Hledat uživatelská zařízení", + "userDevicesSearch": "Hledat uživatelská zařízení...", "openMenu": "Otevřít nabídku", "resource": "Zdroj", "title": "Název", @@ -323,6 +328,54 @@ "apiKeysDelete": "Odstranit klíč API", "apiKeysManage": "Správa API klíčů", "apiKeysDescription": "API klíče se používají k ověření s integračním API", + "provisioningKeysTitle": "Zajišťovací klíč", + "provisioningKeysManage": "Spravovat zajišťovací klíče", + "provisioningKeysDescription": "Zajišťovací klíče slouží k ověření automatického poskytování služeb vaší organizaci.", + "provisioningManage": "Zajištění", + "provisioningDescription": "Spravovat klíče pro nastavení a zkontrolovat čekající stránky čekající na schválení.", + "pendingSites": "Nevyřízené weby", + "siteApproveSuccess": "Web byl úspěšně schválen", + "siteApproveError": "Chyba při schvalování webu", + "provisioningKeys": "Poskytovací klíče", + "searchProvisioningKeys": "Hledat klíče k zajišťování...", + "provisioningKeysAdd": "Generovat zajišťovací klíč", + "provisioningKeysErrorDelete": "Chyba při odstraňování klíče pro úpravu", + "provisioningKeysErrorDeleteMessage": "Chyba při odstraňování klíče pro úpravu", + "provisioningKeysQuestionRemove": "Jste si jisti, že chcete odstranit tento konfigurační klíč z organizace?", + "provisioningKeysMessageRemove": "Jakmile je klíč odstraněn, nelze již použít pro poskytování služeb.", + "provisioningKeysDeleteConfirm": "Potvrdit odstranění zajišťovacího klíče", + "provisioningKeysDelete": "Odstranit zajišťovací klíč", + "provisioningKeysCreate": "Generovat zajišťovací klíč", + "provisioningKeysCreateDescription": "Vygenerovat nový klíč pro organizaci", + "provisioningKeysSeeAll": "Zobrazit všechny doplňovací klíče", + "provisioningKeysSave": "Uložit konfigurační klíč", + "provisioningKeysSaveDescription": "Toto můžete vidět pouze jednou. Zkopírujte ho na bezpečné místo.", + "provisioningKeysErrorCreate": "Chyba při vytváření doplňovacího klíče", + "provisioningKeysList": "Nový klíč pro poskytování informací", + "provisioningKeysMaxBatchSize": "Maximální velikost dávky", + "provisioningKeysUnlimitedBatchSize": "Neomezená velikost šarže (bez omezení)", + "provisioningKeysMaxBatchUnlimited": "Bez omezení", + "provisioningKeysMaxBatchSizeInvalid": "Zadejte platnou maximální velikost šarže (1–1,000,000).", + "provisioningKeysValidUntil": "Platné do", + "provisioningKeysValidUntilHint": "Ponechte prázdné, pokud vyprší platnost.", + "provisioningKeysValidUntilInvalid": "Zadejte platné datum a čas.", + "provisioningKeysNumUsed": "Časy použití", + "provisioningKeysLastUsed": "Naposledy použito", + "provisioningKeysNoExpiry": "Bez vypršení platnosti", + "provisioningKeysNeverUsed": "Nikdy", + "provisioningKeysEdit": "Upravit zajišťovací klíč", + "provisioningKeysEditDescription": "Aktualizujte maximální velikost dávky a dobu vypršení platnosti tohoto klíče.", + "provisioningKeysApproveNewSites": "Schválit nové stránky", + "provisioningKeysApproveNewSitesDescription": "Automaticky schvalovat weby, které se registrují pomocí tohoto klíče.", + "provisioningKeysUpdateError": "Chyba při aktualizaci klíče", + "provisioningKeysUpdated": "Zajišťovací klíč byl aktualizován", + "provisioningKeysUpdatedDescription": "Vaše změny byly uloženy.", + "provisioningKeysBannerTitle": "Klíče pro poskytování webu", + "provisioningKeysBannerDescription": "Vygenerujte klíč pro zřízení a použijte ho s Newt konektorem k automatickému vytvoření stránek při prvním spuštění – není potřeba nastavit samostatné přihlašovací údaje pro každou stránku.", + "provisioningKeysBannerButtonText": "Zjistit více", + "pendingSitesBannerTitle": "Nevyřízené weby", + "pendingSitesBannerDescription": "Stránky, které se připojují pomocí klíče pro zřízení, se zde objeví ke kontrole.", + "pendingSitesBannerButtonText": "Zjistit více", "apiKeysSettings": "Nastavení {apiKeyName}", "userTitle": "Spravovat všechny uživatele", "userDescription": "Zobrazit a spravovat všechny uživatele v systému", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Nepodařilo se aktivovat licenční klíč", "licenseErrorKeyActivateDescription": "Došlo k chybě při aktivaci licenčního klíče.", "licenseAbout": "O licencích", + "licenseBannerTitle": "Aktivovat vaši firemní licenci", + "licenseBannerDescription": "Odemkněte firemní funkce pro vaši samohostovanou instanci Pangolin. Zakupte si licenční klíč pro aktivaci prémiových možností a poté jej přidejte níže.", + "licenseBannerGetLicense": "Zakoupit licenci", + "licenseBannerViewDocs": "Zobrazit dokumentaci", "communityEdition": "Komunitní edice", "licenseAboutDescription": "To je pro obchodní a podnikové uživatele, kteří používají Pangolin v komerčním prostředí. Pokud používáte Pangolin pro osobní použití, můžete tuto sekci ignorovat.", "licenseKeyActivated": "Licenční klíč aktivován", @@ -509,9 +566,12 @@ "userSaved": "Uživatel uložen", "userSavedDescription": "Uživatel byl aktualizován.", "autoProvisioned": "Automaticky poskytnuto", + "autoProvisionSettings": "Automatická nastavení", "autoProvisionedDescription": "Povolit tomuto uživateli automaticky spravovat poskytovatel identity", "accessControlsDescription": "Spravovat co může tento uživatel přistupovat a dělat v organizaci", "accessControlsSubmit": "Uložit kontroly přístupu", + "singleRolePerUserPlanNotice": "Váš plán podporuje pouze jednu roli na uživatele.", + "singleRolePerUserEditionNotice": "Tato verze podporuje pouze jednu roli na uživatele.", "roles": "Role", "accessUsersRoles": "Spravovat uživatele a role", "accessUsersRolesDescription": "Pozvěte uživatele a přidejte je do rolí pro správu přístupu k organizaci", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Zadejte platné číslo portu", "targetErrorNoSite": "Není vybrán žádný web", "targetErrorNoSiteDescription": "Vyberte prosím web pro cíl", + "targetTargetsCleared": "Cíle vymazány", + "targetTargetsClearedDescription": "Všechny cíle byly odstraněny z tohoto zdroje", "targetCreated": "Cíl byl vytvořen", "targetCreatedDescription": "Cíl byl úspěšně vytvořen", "targetErrorCreate": "Nepodařilo se vytvořit cíl", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Zadejte nastavovací token z konzole serveru.", "setupTokenRequired": "Je vyžadován token nastavení", "actionUpdateSite": "Aktualizovat stránku", + "actionResetSiteBandwidth": "Resetovat šířku pásma organizace", "actionListSiteRoles": "Seznam povolených rolí webu", "actionCreateResource": "Vytvořit zdroj", "actionDeleteResource": "Odstranit dokument", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Odstranit uživatele", "actionListUsers": "Seznam uživatelů", "actionAddUserRole": "Přidat uživatelskou roli", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Nastavit uživatelské role", "actionGenerateAccessToken": "Generovat přístupový token", "actionDeleteAccessToken": "Odstranit přístupový token", "actionListAccessTokens": "Seznam přístupových tokenů", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Role", "sidebarShareableLinks": "Odkazy", "sidebarApiKeys": "API klíče", + "sidebarProvisioning": "Zajištění", "sidebarSettings": "Nastavení", "sidebarAllUsers": "Všichni uživatelé", "sidebarIdentityProviders": "Poskytovatelé identity", @@ -1890,6 +1954,40 @@ "exitNode": "Ukončit uzel", "country": "L 343, 22.12.2009, s. 1).", "rulesMatchCountry": "Aktuálně založené na zdrojové IP adrese", + "region": "Oblasti", + "selectRegion": "Vyberte region", + "searchRegions": "Hledat regiony...", + "noRegionFound": "Nebyl nalezen žádný region.", + "rulesMatchRegion": "Vyberte regionální seskupení zemí", + "rulesErrorInvalidRegion": "Neplatný region", + "rulesErrorInvalidRegionDescription": "Vyberte prosím platný region.", + "regionAfrica": "Afrika", + "regionNorthernAfrica": "Severní Afrika", + "regionEasternAfrica": "Východní Afrika", + "regionMiddleAfrica": "Střední Afrika", + "regionSouthernAfrica": "Jižní Afrika", + "regionWesternAfrica": "Západní Afrika", + "regionAmericas": "Ameriky", + "regionCaribbean": "Karibské", + "regionCentralAmerica": "Střední Amerika", + "regionSouthAmerica": "Jižní Amerika", + "regionNorthernAmerica": "Severní Amerika", + "regionAsia": "Asie", + "regionCentralAsia": "Střední Asie", + "regionEasternAsia": "Východní Asie", + "regionSouthEasternAsia": "jihovýchodní Asie", + "regionSouthernAsia": "Jižní Asie", + "regionWesternAsia": "Západní Asie", + "regionEurope": "L 347, 20.12.2013, s. 965).", + "regionEasternEurope": "Východní Evropa", + "regionNorthernEurope": "Severní Evropa", + "regionSouthernEurope": "Jižní Evropa", + "regionWesternEurope": "Západní Evropa", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Austrálie a Nový Zéland", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Spravované vlastní hostování", "description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami", @@ -1938,6 +2036,25 @@ "invalidValue": "Neplatná hodnota", "idpTypeLabel": "Typ poskytovatele identity", "roleMappingExpressionPlaceholder": "např. obsahuje(skupiny, 'admin') && 'Admin' || 'Member'", + "roleMappingModeFixedRoles": "Pevné role", + "roleMappingModeMappingBuilder": "Tvorba mapování", + "roleMappingModeRawExpression": "Surový výraz", + "roleMappingFixedRolesPlaceholderSelect": "Vyberte jednu nebo více rolí", + "roleMappingFixedRolesPlaceholderFreeform": "Napište názvy rolí (shoda podle organizace)", + "roleMappingFixedRolesDescriptionSameForAll": "Přiřadit stejnou roli nastavenou každému uživateli automatického poskytování.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Pro výchozí zásady zadejte názvy rolí, které existují v každé organizaci, kde jsou uživatelé poskytováni. Jména musí přesně odpovídat.", + "roleMappingClaimPath": "Cesta k žádosti", + "roleMappingClaimPathPlaceholder": "skupiny", + "roleMappingClaimPathDescription": "Cesta k užitečnému zatížení tokenu, která obsahuje zdrojové hodnoty (například skupiny).", + "roleMappingMatchValue": "Hodnota zápasu", + "roleMappingAssignRoles": "Přiřadit role", + "roleMappingAddMappingRule": "Přidat pravidlo pro mapování", + "roleMappingRawExpressionResultDescription": "Výraz se musí vyhodnotit do pole řetězce nebo řetězce.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Výraz musí být vyhodnocen na řetězec (jediný název role).", + "roleMappingMatchValuePlaceholder": "Hodnota zápasu (například: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Napište názvy rolí (exact per org)", + "roleMappingBuilderFreeformRowHint": "Názvy rolí musí odpovídat roli v každé cílové organizaci.", + "roleMappingRemoveRule": "Odstranit", "idpGoogleConfiguration": "Konfigurace Google", "idpGoogleConfigurationDescription": "Konfigurace přihlašovacích údajů Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Vyberte doménu pro ověřovací stránku organizace", "domainPickerProvidedDomain": "Poskytnutá doména", "domainPickerFreeProvidedDomain": "Zdarma poskytnutá doména", + "domainPickerFreeDomainsPaidFeature": "Poskytnuté domény jsou placenou funkcí. Předplaťte si plán, abyste získali doménu zahrnutou v plánu – nemusíte si přinést vlastní.", "domainPickerVerified": "Ověřeno", "domainPickerUnverified": "Neověřeno", + "domainPickerManual": "Ruční nastavení", "domainPickerInvalidSubdomainStructure": "Tato subdoména obsahuje neplatné znaky nebo strukturu. Bude automaticky sanitována při uložení.", "domainPickerError": "Chyba", "domainPickerErrorLoadDomains": "Nepodařilo se načíst domény organizace", @@ -2235,7 +2354,7 @@ "description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory." } }, - "personalUseOnly": "Pouze osobní použití (bezplatná licence – bez platby)", + "personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)", "buttons": { "continueToCheckout": "Pokračovat do pokladny" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy", "logRetentionActionLabel": "Uchovávání protokolu akcí", "logRetentionActionDescription": "Jak dlouho uchovávat záznamy akcí", + "logRetentionConnectionLabel": "Uchovávání protokolu připojení", + "logRetentionConnectionDescription": "Jak dlouho uchovávat protokoly připojení", "logRetentionDisabled": "Zakázáno", "logRetention3Days": "3 dny", "logRetention7Days": "7 dní", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Konec následujícího roku", "actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci", "accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci", + "connectionLogs": "Protokoly připojení", + "connectionLogsDescription": "Zobrazit protokoly připojení pro tunely v této organizaci", + "sidebarLogsConnection": "Protokoly připojení", + "sidebarLogsStreaming": "Streamování", + "sourceAddress": "Zdrojová adresa", + "destinationAddress": "Cílová adresa", + "duration": "Doba trvání", "licenseRequiredToUse": "Pro použití této funkce je vyžadována licence Enterprise Edition nebo Pangolin Cloud . Zarezervujte si demo nebo POC zkušební verzi.", "ossEnterpriseEditionRequired": "Enterprise Edition je vyžadována pro použití této funkce. Tato funkce je také k dispozici v Pangolin Cloud. Rezervujte si demo nebo POC zkušební verzi.", "certResolver": "Oddělovač certifikátů", @@ -2487,6 +2615,9 @@ "machineClients": "Strojoví klienti", "install": "Instalovat", "run": "Spustit", + "envFile": "Konfigurační soubor prostředí", + "serviceFile": "Služební soubor", + "enableAndStart": "Povolit a spustit", "clientNameDescription": "Zobrazované jméno klienta, které lze později změnit.", "clientAddress": "Adresa klienta (Rozšířeno)", "setupFailedToFetchSubnet": "Nepodařilo se načíst výchozí podsíť", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Upravte roli a povolte možnost 'Vyžadovat schválení zařízení'. Uživatelé s touto rolí budou potřebovat schválení pro nová zařízení správce.", "approvalsEmptyStatePreviewDescription": "Náhled: Pokud je povoleno, čekající na zařízení se zde zobrazí žádosti o recenzi", "approvalsEmptyStateButtonText": "Spravovat role", - "domainErrorTitle": "Máme problém s ověřením tvé domény" + "domainErrorTitle": "Máme problém s ověřením tvé domény", + "idpAdminAutoProvisionPoliciesTabHint": "Nastavte pravidla mapování rolí a organizace na kartě Automatická úprava nastavení.", + "streamingTitle": "Streamování událostí", + "streamingDescription": "Streamujte události z vaší organizace do externích destinací v reálném čase.", + "streamingUnnamedDestination": "Nepojmenovaný cíl", + "streamingNoUrlConfigured": "Není nakonfigurována žádná URL", + "streamingAddDestination": "Přidat cíl", + "streamingHttpWebhookTitle": "HTTP webový háček", + "streamingHttpWebhookDescription": "Odeslat události na libovolný HTTP koncový bod s pružnou autentizací a šablonou.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Streamujte události do úložiště, které je kompatibilní se S3. Brzy přijde.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.", + "streamingTypePickerDescription": "Vyberte cílový typ pro začátek.", + "streamingFailedToLoad": "Nepodařilo se načíst destinace", + "streamingUnexpectedError": "Došlo k neočekávané chybě.", + "streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl", + "streamingDeletedSuccess": "Cíl byl úspěšně odstraněn", + "streamingFailedToDelete": "Nepodařilo se odstranit cíl", + "streamingDeleteTitle": "Odstranit cíl", + "streamingDeleteButtonText": "Odstranit cíl", + "streamingDeleteDialogAreYouSure": "Jste si jisti, že chcete odstranit", + "streamingDeleteDialogThisDestination": "tato destinace", + "streamingDeleteDialogPermanentlyRemoved": "? Všechny konfigurace budou trvale odstraněny.", + "httpDestEditTitle": "Upravit cíl", + "httpDestAddTitle": "Přidat cíl HTTP", + "httpDestEditDescription": "Aktualizovat konfiguraci pro tuto destinaci HTTP události", + "httpDestAddDescription": "Konfigurace nového koncového bodu HTTP pro příjem událostí vaší organizace.", + "httpDestTabSettings": "Nastavení", + "httpDestTabHeaders": "Záhlaví", + "httpDestTabBody": "Tělo", + "httpDestTabLogs": "Logy", + "httpDestNamePlaceholder": "Moje HTTP cíl", + "httpDestUrlLabel": "Cílová adresa URL", + "httpDestUrlErrorHttpRequired": "URL musí používat http nebo https", + "httpDestUrlErrorHttpsRequired": "HTTPS je vyžadován při nasazení do cloudu", + "httpDestUrlErrorInvalid": "Zadejte platnou URL (např. https://example.com/webhook)", + "httpDestAuthTitle": "Autentifikace", + "httpDestAuthDescription": "Zvolte, jak jsou požadavky na tvůj koncový bod ověřeny.", + "httpDestAuthNoneTitle": "Žádné ověření", + "httpDestAuthNoneDescription": "Odešle žádosti bez záhlaví autorizace.", + "httpDestAuthBearerTitle": "Token na doručitele", + "httpDestAuthBearerDescription": "Přidává hlavičku Authorization: Bearer '' k každému požadavku.", + "httpDestAuthBearerPlaceholder": "Váš API klíč nebo token", + "httpDestAuthBasicTitle": "Základní ověření", + "httpDestAuthBasicDescription": "Přidává hlavičku Authorization: Basic ''. Poskytněte přihlašovací údaje ve formátu uživatelské jméno:heslo.", + "httpDestAuthBasicPlaceholder": "uživatelské jméno:heslo", + "httpDestAuthCustomTitle": "Vlastní záhlaví", + "httpDestAuthCustomDescription": "Zadejte název a hodnotu vlastního HTTP hlavičky pro ověření (např. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Název záhlaví (např. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Hodnota záhlaví", + "httpDestCustomHeadersTitle": "Vlastní HTTP hlavičky", + "httpDestCustomHeadersDescription": "Přidat vlastní hlavičky ke každému odchozímu požadavku. Užitečné pro statické tokeny nebo vlastní Typ obsahu. Ve výchozím nastavení je typ obsahu: application/json.", + "httpDestNoHeadersConfigured": "Nejsou nakonfigurovány žádné vlastní záhlaví. Pro přidání klikněte na \"Přidat záhlaví\".", + "httpDestHeaderNamePlaceholder": "Název záhlaví", + "httpDestHeaderValuePlaceholder": "Hodnota", + "httpDestAddHeader": "Přidat záhlaví", + "httpDestBodyTemplateTitle": "Vlastní šablona těla", + "httpDestBodyTemplateDescription": "Ovládá strukturu užitečného zatížení JSON odeslanou na váš koncový bod. Pokud je vypnuto, je pro každou událost zaslán výchozí objekt JSON.", + "httpDestEnableBodyTemplate": "Povolit vlastní šablonu těla", + "httpDestBodyTemplateLabel": "Šablona těla (JSON)", + "httpDestBodyTemplateHint": "Použijte šablonové proměnné pro referenční pole události ve vašem užitečném zatížení.", + "httpDestPayloadFormatTitle": "Formát datového zatížení", + "httpDestPayloadFormatDescription": "Jak jsou události serializovány v každém žádajícím subjektu.", + "httpDestFormatJsonArrayTitle": "JSON pole", + "httpDestFormatJsonArrayDescription": "Jeden požadavek na každou šarži, tělo je pole JSON. Kompatibilní s většinou generických webových háčků a Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Jeden požadavek na každou šarži, tělo je nově ohraničené JSON – jeden objekt na jednu čáru, bez vnějšího pole. Vyžaduje Splunk HEC, Elastic / OpenSearch, a Grafana Loki.", + "httpDestFormatSingleTitle": "Jedna událost na požadavek", + "httpDestFormatSingleDescription": "Odešle samostatnou HTTP POST pro každou jednotlivou událost. Používejte pouze pro koncové body, které nemohou zpracovávat dávky.", + "httpDestLogTypesTitle": "Typy protokolů", + "httpDestLogTypesDescription": "Vyberte, které typy logů jsou přesměrovány do této destinace. Budou streamovány pouze povolené typy logů.", + "httpDestAccessLogsTitle": "Protokoly přístupu", + "httpDestAccessLogsDescription": "Pokusy o přístup k dokumentům, včetně ověřených a zamítnutých požadavků.", + "httpDestActionLogsTitle": "Záznamy akcí", + "httpDestActionLogsDescription": "Správní opatření prováděná uživateli v rámci organizace.", + "httpDestConnectionLogsTitle": "Protokoly připojení", + "httpDestConnectionLogsDescription": "Události týkající se připojení lokality a tunelu, včetně připojení a odpojení.", + "httpDestRequestLogsTitle": "Záznamy požadavků", + "httpDestRequestLogsDescription": "HTTP záznamy požadavků pro proxy zdroje, včetně metod, cesty a kódu odpovědi.", + "httpDestSaveChanges": "Uložit změny", + "httpDestCreateDestination": "Vytvořit cíl", + "httpDestUpdatedSuccess": "Cíl byl úspěšně aktualizován", + "httpDestCreatedSuccess": "Cíl byl úspěšně vytvořen", + "httpDestUpdateFailed": "Nepodařilo se aktualizovat cíl", + "httpDestCreateFailed": "Nepodařilo se vytvořit cíl" } diff --git a/messages/de-DE.json b/messages/de-DE.json index cef7223fb..5edc95cbc 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -148,6 +148,11 @@ "createLink": "Link erstellen", "resourcesNotFound": "Keine Ressourcen gefunden", "resourceSearch": "Suche Ressourcen", + "machineSearch": "Maschinen suchen", + "machinesSearch": "Suche Maschinen-Klienten...", + "machineNotFound": "Keine Maschinen gefunden", + "userDeviceSearch": "Benutzergeräte durchsuchen", + "userDevicesSearch": "Benutzergeräte durchsuchen...", "openMenu": "Menü öffnen", "resource": "Ressource", "title": "Titel", @@ -323,6 +328,54 @@ "apiKeysDelete": "API-Schlüssel löschen", "apiKeysManage": "API-Schlüssel verwalten", "apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet", + "provisioningKeysTitle": "Bereitstellungsschlüssel", + "provisioningKeysManage": "Bereitstellungsschlüssel verwalten", + "provisioningKeysDescription": "Bereitstellungsschlüssel werden verwendet, um die automatisierte Bereitstellung von Seiten für Ihr Unternehmen zu authentifizieren.", + "provisioningManage": "Bereitstellung", + "provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Seiten prüfen, die noch auf Genehmigung warten.", + "pendingSites": "Ausstehende Seiten", + "siteApproveSuccess": "Site erfolgreich freigegeben", + "siteApproveError": "Fehler beim Bestätigen der Seite", + "provisioningKeys": "Bereitstellungsschlüssel", + "searchProvisioningKeys": "Bereitstellungsschlüssel suchen...", + "provisioningKeysAdd": "Bereitstellungsschlüssel generieren", + "provisioningKeysErrorDelete": "Fehler beim Löschen des Bereitstellungsschlüssels", + "provisioningKeysErrorDeleteMessage": "Fehler beim Löschen des Bereitstellungsschlüssels", + "provisioningKeysQuestionRemove": "Sind Sie sicher, dass Sie diesen Bereitstellungsschlüssel aus der Organisation entfernen möchten?", + "provisioningKeysMessageRemove": "Einmal entfernt, kann der Schlüssel nicht mehr für die Bereitstellung der Site verwendet werden.", + "provisioningKeysDeleteConfirm": "Bereitstellungsschlüssel löschen bestätigen", + "provisioningKeysDelete": "Bereitstellungsschlüssel löschen", + "provisioningKeysCreate": "Bereitstellungsschlüssel generieren", + "provisioningKeysCreateDescription": "Einen neuen Bereitstellungsschlüssel für die Organisation generieren", + "provisioningKeysSeeAll": "Alle Bereitstellungsschlüssel anzeigen", + "provisioningKeysSave": "Bereitstellungsschlüssel speichern", + "provisioningKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.", + "provisioningKeysErrorCreate": "Fehler beim Erstellen des Bereitstellungsschlüssels", + "provisioningKeysList": "Neuer Bereitstellungsschlüssel", + "provisioningKeysMaxBatchSize": "Max. Batch-Größe", + "provisioningKeysUnlimitedBatchSize": "Unbegrenzte Batch-Größe (kein Limit)", + "provisioningKeysMaxBatchUnlimited": "Unbegrenzt", + "provisioningKeysMaxBatchSizeInvalid": "Geben Sie eine gültige maximale Batchgröße ein (1–1.000.000).", + "provisioningKeysValidUntil": "Gültig bis", + "provisioningKeysValidUntilHint": "Leer lassen für keine Verjährung.", + "provisioningKeysValidUntilInvalid": "Geben Sie ein gültiges Datum und Zeit ein.", + "provisioningKeysNumUsed": "Verwendete Zeiten", + "provisioningKeysLastUsed": "Zuletzt verwendet", + "provisioningKeysNoExpiry": "Kein Ablauf", + "provisioningKeysNeverUsed": "Nie", + "provisioningKeysEdit": "Bereitstellungsschlüssel bearbeiten", + "provisioningKeysEditDescription": "Aktualisieren Sie die maximale Batch-Größe und Ablaufzeit für diesen Schlüssel.", + "provisioningKeysApproveNewSites": "Neue Seiten genehmigen", + "provisioningKeysApproveNewSitesDescription": "Sites, die sich mit diesem Schlüssel registrieren, automatisch freigeben.", + "provisioningKeysUpdateError": "Fehler beim Aktualisieren des Bereitstellungsschlüssels", + "provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert", + "provisioningKeysUpdatedDescription": "Ihre Änderungen wurden gespeichert.", + "provisioningKeysBannerTitle": "Website-Bereitstellungsschlüssel", + "provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.", + "provisioningKeysBannerButtonText": "Mehr erfahren", + "pendingSitesBannerTitle": "Ausstehende Seiten", + "pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.", + "pendingSitesBannerButtonText": "Mehr erfahren", "apiKeysSettings": "{apiKeyName} Einstellungen", "userTitle": "Alle Benutzer verwalten", "userDescription": "Alle Benutzer im System anzeigen und verwalten", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Fehler beim Aktivieren des Lizenzschlüssels", "licenseErrorKeyActivateDescription": "Beim Aktivieren des Lizenzschlüssels ist ein Fehler aufgetreten.", "licenseAbout": "Über Lizenzierung", + "licenseBannerTitle": "Aktivieren Sie Ihre Enterprise-Lizenz", + "licenseBannerDescription": "Schalten Sie Unternehmensfunktionen für Ihre selbstgehostete Pangolin-Instanz frei. Kaufen Sie einen Lizenzschlüssel, um Premium-Funktionen zu aktivieren, und fügen Sie ihn dann unten hinzu.", + "licenseBannerGetLicense": "Lizenz erhalten", + "licenseBannerViewDocs": "Dokumentation anzeigen", "communityEdition": "Community-Edition", "licenseAboutDescription": "Dies ist für Geschäfts- und Unternehmensanwender, die Pangolin in einem kommerziellen Umfeld einsetzen. Wenn Sie Pangolin für den persönlichen Gebrauch verwenden, können Sie diesen Abschnitt ignorieren.", "licenseKeyActivated": "Lizenzschlüssel aktiviert", @@ -509,9 +566,12 @@ "userSaved": "Benutzer gespeichert", "userSavedDescription": "Der Benutzer wurde aktualisiert.", "autoProvisioned": "Automatisch bereitgestellt", + "autoProvisionSettings": "Auto-Bereitstellungseinstellungen", "autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter", "accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann", "accessControlsSubmit": "Zugriffskontrollen speichern", + "singleRolePerUserPlanNotice": "Ihr Plan unterstützt nur eine Rolle pro Benutzer.", + "singleRolePerUserEditionNotice": "Diese Ausgabe unterstützt nur eine Rolle pro Benutzer.", "roles": "Rollen", "accessUsersRoles": "Benutzer & Rollen verwalten", "accessUsersRolesDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf die Organisation zu verwalten", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein", "targetErrorNoSite": "Kein Standort ausgewählt", "targetErrorNoSiteDescription": "Bitte wähle einen Standort für das Ziel aus", + "targetTargetsCleared": "Ziele gelöscht", + "targetTargetsClearedDescription": "Alle Ziele wurden aus dieser Ressource entfernt", "targetCreated": "Ziel erstellt", "targetCreatedDescription": "Ziel wurde erfolgreich erstellt", "targetErrorCreate": "Fehler beim Erstellen des Ziels", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.", "setupTokenRequired": "Setup-Token ist erforderlich", "actionUpdateSite": "Standorte aktualisieren", + "actionResetSiteBandwidth": "Organisations-Bandbreite zurücksetzen", "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", "actionCreateResource": "Ressource erstellen", "actionDeleteResource": "Ressource löschen", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Benutzer entfernen", "actionListUsers": "Benutzer auflisten", "actionAddUserRole": "Benutzerrolle hinzufügen", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Benutzerrollen festlegen", "actionGenerateAccessToken": "Zugriffstoken generieren", "actionDeleteAccessToken": "Zugriffstoken löschen", "actionListAccessTokens": "Zugriffstoken auflisten", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Rollen", "sidebarShareableLinks": "Links", "sidebarApiKeys": "API-Schlüssel", + "sidebarProvisioning": "Bereitstellung", "sidebarSettings": "Einstellungen", "sidebarAllUsers": "Alle Benutzer", "sidebarIdentityProviders": "Identitätsanbieter", @@ -1890,6 +1954,40 @@ "exitNode": "Exit-Node", "country": "Land", "rulesMatchCountry": "Derzeit basierend auf der Quell-IP", + "region": "Region", + "selectRegion": "Region wählen...", + "searchRegions": "Regionen suchen...", + "noRegionFound": "Keine Region gefunden.", + "rulesMatchRegion": "Wählen Sie eine Regionalgruppe von Ländern", + "rulesErrorInvalidRegion": "Ungültige Region", + "rulesErrorInvalidRegionDescription": "Bitte wählen Sie eine gültige Region aus.", + "regionAfrica": "Afrika", + "regionNorthernAfrica": "Nordafrika", + "regionEasternAfrica": "Ostafrika", + "regionMiddleAfrica": "Zentralafrika", + "regionSouthernAfrica": "Südliches Afrika", + "regionWesternAfrica": "Westafrika", + "regionAmericas": "Amerika", + "regionCaribbean": "Karibik", + "regionCentralAmerica": "Mittelamerika", + "regionSouthAmerica": "Südamerika", + "regionNorthernAmerica": "Nordamerika", + "regionAsia": "Asien", + "regionCentralAsia": "Zentralasien", + "regionEasternAsia": "Ostasien", + "regionSouthEasternAsia": "Südostasien", + "regionSouthernAsia": "Südasien", + "regionWesternAsia": "Westasien", + "regionEurope": "Europa", + "regionEasternEurope": "Osteuropa", + "regionNorthernEurope": "Nordeuropa", + "regionSouthernEurope": "Südeuropa", + "regionWesternEurope": "Westeuropa", + "regionOceania": "Ozeanien", + "regionAustraliaAndNewZealand": "Australien und Neuseeland", + "regionMelanesia": "Melanesien", + "regionMicronesia": "Mikronesien", + "regionPolynesia": "Polynesien", "managedSelfHosted": { "title": "Verwaltetes Selbsthosted", "description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen", @@ -1938,6 +2036,25 @@ "invalidValue": "Ungültiger Wert", "idpTypeLabel": "Identitätsanbietertyp", "roleMappingExpressionPlaceholder": "z. B. enthalten(Gruppen, 'admin') && 'Admin' || 'Mitglied'", + "roleMappingModeFixedRoles": "Feste Rollen", + "roleMappingModeMappingBuilder": "Mapping Builder", + "roleMappingModeRawExpression": "Roher Ausdruck", + "roleMappingFixedRolesPlaceholderSelect": "Wählen Sie eine oder mehrere Rollen", + "roleMappingFixedRolesPlaceholderFreeform": "Rollennamen eingeben (exakte Übereinstimmung pro Organisation)", + "roleMappingFixedRolesDescriptionSameForAll": "Weisen Sie jedem auto-provisionierten Benutzer die gleiche Rolle zu.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Für Standardrichtlinien geben Sie Rollennamen ein, die in jeder Organisation existieren, in der Benutzer angegeben sind. Namen müssen exakt übereinstimmen.", + "roleMappingClaimPath": "Pfad einfordern", + "roleMappingClaimPathPlaceholder": "gruppen", + "roleMappingClaimPathDescription": "Pfad in der Token Payload mit Quellwerten (zum Beispiel Gruppen).", + "roleMappingMatchValue": "Match-Wert", + "roleMappingAssignRoles": "Rollen zuweisen", + "roleMappingAddMappingRule": "Zuordnungsregel hinzufügen", + "roleMappingRawExpressionResultDescription": "Ausdruck muss zu einem String oder String Array ausgewertet werden.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Ausdruck muss zu einem String (einem einzigen Rollennamen) ausgewertet werden.", + "roleMappingMatchValuePlaceholder": "Match-Wert (z. B.: Admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Rollennamen eingeben (exakt pro Ort)", + "roleMappingBuilderFreeformRowHint": "Rollennamen müssen mit einer Rolle in jeder Zielorganisation übereinstimmen.", + "roleMappingRemoveRule": "Entfernen", "idpGoogleConfiguration": "Google-Konfiguration", "idpGoogleConfigurationDescription": "Google OAuth2 Zugangsdaten konfigurieren", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Wählen Sie eine Domain für die Authentifizierungsseite der Organisation", "domainPickerProvidedDomain": "Angegebene Domain", "domainPickerFreeProvidedDomain": "Kostenlose Domain", + "domainPickerFreeDomainsPaidFeature": "Bereitgestellte Domains sind ein kostenpflichtiges Feature. Abonnieren Sie, um eine Domain in Ihrem Tarif zu erhalten – keine Notwendigkeit, Ihre eigene mitzubringen.", "domainPickerVerified": "Verifiziert", "domainPickerUnverified": "Nicht verifiziert", + "domainPickerManual": "Manuell", "domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.", "domainPickerError": "Fehler", "domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domains", @@ -2235,7 +2354,7 @@ "description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung." } }, - "personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz — keine Kasse)", + "personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)", "buttons": { "continueToCheckout": "Weiter zur Kasse" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen", "logRetentionActionLabel": "Aktionsprotokoll-Speicherung", "logRetentionActionDescription": "Dauer des Action-Logs", + "logRetentionConnectionLabel": "Verbindungsprotokoll-Speicherung", + "logRetentionConnectionDescription": "Wie lange Verbindungsprotokolle gespeichert werden sollen", "logRetentionDisabled": "Deaktiviert", "logRetention3Days": "3 Tage", "logRetention7Days": "7 Tage", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Ende des folgenden Jahres", "actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen", "accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen", + "connectionLogs": "Verbindungsprotokolle", + "connectionLogsDescription": "Verbindungsprotokolle für Tunnel in dieser Organisation anzeigen", + "sidebarLogsConnection": "Verbindungsprotokolle", + "sidebarLogsStreaming": "Streaming", + "sourceAddress": "Quelladresse", + "destinationAddress": "Zieladresse", + "duration": "Dauer", "licenseRequiredToUse": "Eine Enterprise Edition Lizenz oder Pangolin Cloud wird benötigt, um diese Funktion nutzen zu können. Buchen Sie eine Demo oder POC Testversion.", "ossEnterpriseEditionRequired": "Die Enterprise Edition wird benötigt, um diese Funktion nutzen zu können. Diese Funktion ist auch in Pangolin Cloudverfügbar. Buchen Sie eine Demo oder POC Testversion.", "certResolver": "Zertifikatsauflöser", @@ -2487,6 +2615,9 @@ "machineClients": "Maschinen-Clients", "install": "Installieren", "run": "Ausführen", + "envFile": "Umgebungsdatei", + "serviceFile": "Servicedatei", + "enableAndStart": "Aktivieren und Starten", "clientNameDescription": "Der Anzeigename des Clients, der später geändert werden kann.", "clientAddress": "Clientadresse (Erweitert)", "setupFailedToFetchSubnet": "Fehler beim Abrufen des Standard-Subnetzes", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.", "approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt", "approvalsEmptyStateButtonText": "Rollen verwalten", - "domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain" + "domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain", + "idpAdminAutoProvisionPoliciesTabHint": "Konfigurieren Sie Rollenzuordnungs- und Organisationsrichtlinien auf der Registerkarte Auto-Bereitstellungseinstellungen.", + "streamingTitle": "Event Streaming", + "streamingDescription": "Streamen Sie Events aus Ihrem Unternehmen in Echtzeit zu externen Zielen.", + "streamingUnnamedDestination": "Unbenanntes Ziel", + "streamingNoUrlConfigured": "Keine URL konfiguriert", + "streamingAddDestination": "Ziel hinzufügen", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Sende Ereignisse an jeden HTTP-Endpunkt mit flexibler Authentifizierung und Vorlage.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Streame Ereignisse in eine S3-kompatible Objekt-Speicher-Eimer. Kommt bald.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.", + "streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.", + "streamingFailedToLoad": "Fehler beim Laden der Ziele", + "streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.", + "streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels", + "streamingDeletedSuccess": "Ziel erfolgreich gelöscht", + "streamingFailedToDelete": "Fehler beim Löschen des Ziels", + "streamingDeleteTitle": "Ziel löschen", + "streamingDeleteButtonText": "Ziel löschen", + "streamingDeleteDialogAreYouSure": "Sind Sie sicher, dass Sie löschen möchten", + "streamingDeleteDialogThisDestination": "dieses Ziel", + "streamingDeleteDialogPermanentlyRemoved": "? Alle Konfiguration wird dauerhaft entfernt.", + "httpDestEditTitle": "Ziel bearbeiten", + "httpDestAddTitle": "HTTP-Ziel hinzufügen", + "httpDestEditDescription": "Aktualisiere die Konfiguration für dieses HTTP-Streaming-Ziel.", + "httpDestAddDescription": "Konfigurieren Sie einen neuen HTTP-Endpunkt, um die Ereignisse Ihrer Organisation zu empfangen.", + "httpDestTabSettings": "Einstellungen", + "httpDestTabHeaders": "Kopfzeilen", + "httpDestTabBody": "Körper", + "httpDestTabLogs": "Logs", + "httpDestNamePlaceholder": "Mein HTTP-Ziel", + "httpDestUrlLabel": "Ziel-URL", + "httpDestUrlErrorHttpRequired": "URL muss http oder https verwenden", + "httpDestUrlErrorHttpsRequired": "HTTPS wird für Cloud-Deployment benötigt", + "httpDestUrlErrorInvalid": "Geben Sie eine gültige URL ein (z.B. https://example.com/webhook)", + "httpDestAuthTitle": "Authentifizierung", + "httpDestAuthDescription": "Legen Sie fest, wie Anfragen an Ihren Endpunkt authentifiziert werden.", + "httpDestAuthNoneTitle": "Keine Authentifizierung", + "httpDestAuthNoneDescription": "Sendet Anfragen ohne Autorisierungs-Header.", + "httpDestAuthBearerTitle": "Bären-Token", + "httpDestAuthBearerDescription": "Fügt jedem Anfrage-Header eine \"Authorization: Bearer ''\" hinzu.", + "httpDestAuthBearerPlaceholder": "Ihr API-Schlüssel oder Token", + "httpDestAuthBasicTitle": "Einfacher Auth", + "httpDestAuthBasicDescription": "Fügt einen \"Authorization: Basic ''\"-Header hinzu. Geben Sie die Anmeldedaten als Benutzername:Passwort an.", + "httpDestAuthBasicPlaceholder": "benutzername:password", + "httpDestAuthCustomTitle": "Eigene Kopfzeile", + "httpDestAuthCustomDescription": "Geben Sie einen eigenen HTTP-Header-Namen und einen Wert für die Authentifizierung an (z.B. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Headername (z.B. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Header-Wert", + "httpDestCustomHeadersTitle": "Eigene HTTP-Header", + "httpDestCustomHeadersDescription": "Fügen Sie jeder ausgehenden Anfrage benutzerdefinierte Kopfzeilen hinzu. Nützlich für statische Tokens oder einen benutzerdefinierten Content-Typ. Standardmäßig wird Content-Type: application/json gesendet.", + "httpDestNoHeadersConfigured": "Keine benutzerdefinierten Header konfiguriert. Klicken Sie auf \"Header hinzufügen\", um einen hinzuzufügen.", + "httpDestHeaderNamePlaceholder": "Header-Name", + "httpDestHeaderValuePlaceholder": "Wert", + "httpDestAddHeader": "Header hinzufügen", + "httpDestBodyTemplateTitle": "Eigene Body-Vorlage", + "httpDestBodyTemplateDescription": "Steuere die JSON-Payload-Struktur, die an deinen Endpunkt gesendet wurde. Wenn deaktiviert, wird für jede Veranstaltung ein Standard-JSON-Objekt gesendet.", + "httpDestEnableBodyTemplate": "Eigene Körpervorlage aktivieren", + "httpDestBodyTemplateLabel": "Body-Vorlage (JSON)", + "httpDestBodyTemplateHint": "Verwenden Sie Template-Variablen, um Ereignisfelder in Ihrer Payload zu referenzieren.", + "httpDestPayloadFormatTitle": "Payload-Format", + "httpDestPayloadFormatDescription": "Wie Ereignisse in jedes Anfragegremium serialisiert werden.", + "httpDestFormatJsonArrayTitle": "JSON Array", + "httpDestFormatJsonArrayDescription": "Eine Anfrage pro Stapel ist ein JSON-Array. Kompatibel mit den meisten generischen Webhooks und Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Eine Anfrage pro Batch, der Körper ist newline-getrenntes JSON — ein Objekt pro Zeile, kein äußeres Array. Benötigt von Splunk HEC, Elastic / OpenSearch, und Grafana Loki.", + "httpDestFormatSingleTitle": "Ein Ereignis pro Anfrage", + "httpDestFormatSingleDescription": "Sendet eine separate HTTP-POST für jedes einzelne Ereignis. Nur für Endpunkte, die Batches nicht handhaben können.", + "httpDestLogTypesTitle": "Log-Typen", + "httpDestLogTypesDescription": "Wählen Sie, welche Log-Typen an dieses Ziel weitergeleitet werden. Nur aktivierte Log-Typen werden gestreamt.", + "httpDestAccessLogsTitle": "Zugriffsprotokolle", + "httpDestAccessLogsDescription": "Ressourcenzugriffe, einschließlich authentifizierter und abgelehnter Anfragen.", + "httpDestActionLogsTitle": "Aktionsprotokolle", + "httpDestActionLogsDescription": "Administrative Maßnahmen, die von Benutzern innerhalb der Organisation durchgeführt werden.", + "httpDestConnectionLogsTitle": "Verbindungsprotokolle", + "httpDestConnectionLogsDescription": "Site- und Tunnelverbindungen, einschließlich Verbindungen und Trennungen.", + "httpDestRequestLogsTitle": "Logs anfordern", + "httpDestRequestLogsDescription": "HTTP-Request-Protokolle für proxiierte Ressourcen, einschließlich Methode, Pfad und Antwort-Code.", + "httpDestSaveChanges": "Änderungen speichern", + "httpDestCreateDestination": "Ziel erstellen", + "httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert", + "httpDestCreatedSuccess": "Ziel erfolgreich erstellt", + "httpDestUpdateFailed": "Fehler beim Aktualisieren des Ziels", + "httpDestCreateFailed": "Fehler beim Erstellen des Ziels" } diff --git a/messages/en-US.json b/messages/en-US.json index 753437a35..23a973697 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -331,6 +331,11 @@ "provisioningKeysTitle": "Provisioning Key", "provisioningKeysManage": "Manage Provisioning Keys", "provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.", + "provisioningManage": "Provisioning", + "provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.", + "pendingSites": "Pending Sites", + "siteApproveSuccess": "Site approved successfully", + "siteApproveError": "Error approving site", "provisioningKeys": "Provisioning Keys", "searchProvisioningKeys": "Search provisioning keys...", "provisioningKeysAdd": "Generate Provisioning Key", @@ -360,9 +365,17 @@ "provisioningKeysNeverUsed": "Never", "provisioningKeysEdit": "Edit Provisioning Key", "provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.", + "provisioningKeysApproveNewSites": "Approve new sites", + "provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.", "provisioningKeysUpdateError": "Error updating provisioning key", "provisioningKeysUpdated": "Provisioning key updated", "provisioningKeysUpdatedDescription": "Your changes have been saved.", + "provisioningKeysBannerTitle": "Site Provisioning Keys", + "provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup - no need to set up separate credentials for each site.", + "provisioningKeysBannerButtonText": "Learn More", + "pendingSitesBannerTitle": "Pending Sites", + "pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.", + "pendingSitesBannerButtonText": "Learn More", "apiKeysSettings": "{apiKeyName} Settings", "userTitle": "Manage All Users", "userDescription": "View and manage all users in the system", @@ -392,6 +405,10 @@ "licenseErrorKeyActivate": "Failed to activate license key", "licenseErrorKeyActivateDescription": "An error occurred while activating the license key.", "licenseAbout": "About Licensing", + "licenseBannerTitle": "Enable Your Enterprise License", + "licenseBannerDescription": "Unlock enterprise features for your self-hosted Pangolin instance. Purchase a license key to activate premium capabilities, then add it below.", + "licenseBannerGetLicense": "Get a License", + "licenseBannerViewDocs": "View Documentation", "communityEdition": "Community Edition", "licenseAboutDescription": "This is for business and enterprise users who are using Pangolin in a commercial environment. If you are using Pangolin for personal use, you can ignore this section.", "licenseKeyActivated": "License key activated", @@ -611,6 +628,8 @@ "targetErrorInvalidPortDescription": "Please enter a valid port number", "targetErrorNoSite": "No site selected", "targetErrorNoSiteDescription": "Please select a site for the target", + "targetTargetsCleared": "Targets cleared", + "targetTargetsClearedDescription": "All targets have been removed from this resource", "targetCreated": "Target created", "targetCreatedDescription": "Target has been created successfully", "targetErrorCreate": "Failed to create target", @@ -2022,6 +2041,40 @@ "exitNode": "Exit Node", "country": "Country", "rulesMatchCountry": "Currently based on source IP", + "region": "Region", + "selectRegion": "Select region", + "searchRegions": "Search regions...", + "noRegionFound": "No region found.", + "rulesMatchRegion": "Select a regional grouping of countries", + "rulesErrorInvalidRegion": "Invalid region", + "rulesErrorInvalidRegionDescription": "Please select a valid region.", + "regionAfrica": "Africa", + "regionNorthernAfrica": "Northern Africa", + "regionEasternAfrica": "Eastern Africa", + "regionMiddleAfrica": "Middle Africa", + "regionSouthernAfrica": "Southern Africa", + "regionWesternAfrica": "Western Africa", + "regionAmericas": "Americas", + "regionCaribbean": "Caribbean", + "regionCentralAmerica": "Central America", + "regionSouthAmerica": "South America", + "regionNorthernAmerica": "Northern America", + "regionAsia": "Asia", + "regionCentralAsia": "Central Asia", + "regionEasternAsia": "Eastern Asia", + "regionSouthEasternAsia": "South-Eastern Asia", + "regionSouthernAsia": "Southern Asia", + "regionWesternAsia": "Western Asia", + "regionEurope": "Europe", + "regionEasternEurope": "Eastern Europe", + "regionNorthernEurope": "Northern Europe", + "regionSouthernEurope": "Southern Europe", + "regionWesternEurope": "Western Europe", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australia and New Zealand", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Managed Self-Hosted", "description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles", @@ -2151,9 +2204,11 @@ "addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.", "selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page", "domainPickerProvidedDomain": "Provided Domain", - "domainPickerFreeProvidedDomain": "Free Provided Domain", + "domainPickerFreeProvidedDomain": "Provided Domain", + "domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan — no need to bring your own.", "domainPickerVerified": "Verified", "domainPickerUnverified": "Unverified", + "domainPickerManual": "Manual", "domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.", "domainPickerError": "Error", "domainPickerErrorLoadDomains": "Failed to load organization domains", @@ -2383,10 +2438,10 @@ }, "scale": { "title": "Scale", - "description": "Enterprise features, 50 users, 50 sites, and priority support." + "description": "Enterprise features, 50 users, 100 sites, and priority support." } }, - "personalUseOnly": "Personal use only (free license — no checkout)", + "personalUseOnly": "Personal use only (free license - no checkout)", "buttons": { "continueToCheckout": "Continue to Checkout" }, @@ -2485,6 +2540,8 @@ "logRetentionAccessDescription": "How long to retain access logs", "logRetentionActionLabel": "Action Log Retention", "logRetentionActionDescription": "How long to retain action logs", + "logRetentionConnectionLabel": "Connection Log Retention", + "logRetentionConnectionDescription": "How long to retain connection logs", "logRetentionDisabled": "Disabled", "logRetention3Days": "3 days", "logRetention7Days": "7 days", @@ -2498,11 +2555,12 @@ "connectionLogs": "Connection Logs", "connectionLogsDescription": "View connection logs for tunnels in this organization", "sidebarLogsConnection": "Connection Logs", + "sidebarLogsStreaming": "Streaming", "sourceAddress": "Source Address", "destinationAddress": "Destination Address", "duration": "Duration", - "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature. Book a demo or POC trial.", - "ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud. Book a demo or POC trial.", + "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature. Book a free demo or POC trial to learn more.", + "ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud. Book a free demo or POC trial to learn more.", "certResolver": "Certificate Resolver", "certResolverDescription": "Select the certificate resolver to use for this resource.", "selectCertResolver": "Select Certificate Resolver", @@ -2644,6 +2702,9 @@ "machineClients": "Machine Clients", "install": "Install", "run": "Run", + "envFile": "Environment File", + "serviceFile": "Service File", + "enableAndStart": "Enable and Start", "clientNameDescription": "The display name of the client that can be changed later.", "clientAddress": "Client Address (Advanced)", "setupFailedToFetchSubnet": "Failed to fetch default subnet", @@ -2841,5 +2902,89 @@ "approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review", "approvalsEmptyStateButtonText": "Manage Roles", "domainErrorTitle": "We are having trouble verifying your domain", - "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the Auto Provision Settings tab." + "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the Auto Provision Settings tab.", + "streamingTitle": "Event Streaming", + "streamingDescription": "Stream events from your organization to external destinations in real time.", + "streamingUnnamedDestination": "Unnamed destination", + "streamingNoUrlConfigured": "No URL configured", + "streamingAddDestination": "Add Destination", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Stream events to an S3-compatible object storage bucket. Contact support to enable this destination.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Forward events directly to your Datadog account. Contact support to enable this destination.", + "streamingTypePickerDescription": "Choose a destination type to get started.", + "streamingFailedToLoad": "Failed to load destinations", + "streamingUnexpectedError": "An unexpected error occurred.", + "streamingFailedToUpdate": "Failed to update destination", + "streamingDeletedSuccess": "Destination deleted successfully", + "streamingFailedToDelete": "Failed to delete destination", + "streamingDeleteTitle": "Delete Destination", + "streamingDeleteButtonText": "Delete Destination", + "streamingDeleteDialogAreYouSure": "Are you sure you want to delete", + "streamingDeleteDialogThisDestination": "this destination", + "streamingDeleteDialogPermanentlyRemoved": "? All configuration will be permanently removed.", + "httpDestEditTitle": "Edit Destination", + "httpDestAddTitle": "Add HTTP Destination", + "httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.", + "httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.", + "httpDestTabSettings": "Settings", + "httpDestTabHeaders": "Headers", + "httpDestTabBody": "Body", + "httpDestTabLogs": "Logs", + "httpDestNamePlaceholder": "My HTTP destination", + "httpDestUrlLabel": "Destination URL", + "httpDestUrlErrorHttpRequired": "URL must use http or https", + "httpDestUrlErrorHttpsRequired": "HTTPS is required", + "httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)", + "httpDestAuthTitle": "Authentication", + "httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.", + "httpDestAuthNoneTitle": "No Authentication", + "httpDestAuthNoneDescription": "Sends requests without an Authorization header.", + "httpDestAuthBearerTitle": "Bearer Token", + "httpDestAuthBearerDescription": "Adds an Authorization: Bearer '' header to each request.", + "httpDestAuthBearerPlaceholder": "Your API key or token", + "httpDestAuthBasicTitle": "Basic Auth", + "httpDestAuthBasicDescription": "Adds an Authorization: Basic '' header. Provide credentials as username:password.", + "httpDestAuthBasicPlaceholder": "username:password", + "httpDestAuthCustomTitle": "Custom Header", + "httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Header name (e.g. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Header value", + "httpDestCustomHeadersTitle": "Custom HTTP Headers", + "httpDestCustomHeadersDescription": "Add custom headers to every outgoing request. Useful for static tokens or a custom Content-Type. By default, Content-Type: application/json is sent.", + "httpDestNoHeadersConfigured": "No custom headers configured. Click \"Add Header\" to add one.", + "httpDestHeaderNamePlaceholder": "Header name", + "httpDestHeaderValuePlaceholder": "Value", + "httpDestAddHeader": "Add Header", + "httpDestBodyTemplateTitle": "Custom Body Template", + "httpDestBodyTemplateDescription": "Control the JSON payload structure sent to your endpoint. If disabled, a default JSON object is sent for each event.", + "httpDestEnableBodyTemplate": "Enable custom body template", + "httpDestBodyTemplateLabel": "Body Template (JSON)", + "httpDestBodyTemplateHint": "Use template variables to reference event fields in your payload.", + "httpDestPayloadFormatTitle": "Payload Format", + "httpDestPayloadFormatDescription": "How events are serialised into each request body.", + "httpDestFormatJsonArrayTitle": "JSON Array", + "httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.", + "httpDestFormatSingleTitle": "One Event Per Request", + "httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.", + "httpDestLogTypesTitle": "Log Types", + "httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.", + "httpDestAccessLogsTitle": "Access Logs", + "httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.", + "httpDestActionLogsTitle": "Action Logs", + "httpDestActionLogsDescription": "Administrative actions performed by users within the organization.", + "httpDestConnectionLogsTitle": "Connection Logs", + "httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.", + "httpDestRequestLogsTitle": "Request Logs", + "httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.", + "httpDestSaveChanges": "Save Changes", + "httpDestCreateDestination": "Create Destination", + "httpDestUpdatedSuccess": "Destination updated successfully", + "httpDestCreatedSuccess": "Destination created successfully", + "httpDestUpdateFailed": "Failed to update destination", + "httpDestCreateFailed": "Failed to create destination" } diff --git a/messages/es-ES.json b/messages/es-ES.json index 2fc52b885..72251ffba 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -148,6 +148,11 @@ "createLink": "Crear enlace", "resourcesNotFound": "No se encontraron recursos", "resourceSearch": "Buscar recursos", + "machineSearch": "Buscar máquinas", + "machinesSearch": "Buscar clientes...", + "machineNotFound": "No hay máquinas", + "userDeviceSearch": "Buscar dispositivos de usuario", + "userDevicesSearch": "Buscar dispositivos de usuario...", "openMenu": "Abrir menú", "resource": "Recurso", "title": "Título", @@ -323,6 +328,54 @@ "apiKeysDelete": "Borrar Clave API", "apiKeysManage": "Administrar claves API", "apiKeysDescription": "Las claves API se utilizan para autenticar con la API de integración", + "provisioningKeysTitle": "Clave de aprovisionamiento", + "provisioningKeysManage": "Administrar Claves de Aprovisionamiento", + "provisioningKeysDescription": "Las claves de aprovisionamiento se utilizan para autenticar la provisión automatizada del sitio para su organización.", + "provisioningManage": "Aprovisionamiento", + "provisioningDescription": "Administrar las claves de aprovisionamiento y revisar los sitios pendientes de aprobación.", + "pendingSites": "Sitios pendientes", + "siteApproveSuccess": "Sitio aprobado con éxito", + "siteApproveError": "Error al aprobar el sitio", + "provisioningKeys": "Claves de aprovisionamiento", + "searchProvisioningKeys": "Buscar claves de suministro...", + "provisioningKeysAdd": "Generar clave de aprovisionamiento", + "provisioningKeysErrorDelete": "Error al eliminar la clave de aprovisionamiento", + "provisioningKeysErrorDeleteMessage": "Error al eliminar la clave de aprovisionamiento", + "provisioningKeysQuestionRemove": "¿Está seguro que desea eliminar esta clave de aprovisionamiento de la organización?", + "provisioningKeysMessageRemove": "Una vez eliminada, la clave ya no se puede utilizar para la disposición del sitio.", + "provisioningKeysDeleteConfirm": "Confirmar Eliminar Clave de Aprovisionamiento", + "provisioningKeysDelete": "Eliminar clave de aprovisionamiento", + "provisioningKeysCreate": "Generar clave de aprovisionamiento", + "provisioningKeysCreateDescription": "Generar una nueva clave de aprovisionamiento para la organización", + "provisioningKeysSeeAll": "Ver todas las claves de aprovisionamiento", + "provisioningKeysSave": "Guardar la clave de aprovisionamiento", + "provisioningKeysSaveDescription": "Sólo podrás verlo una vez. Copítalo a un lugar seguro.", + "provisioningKeysErrorCreate": "Error al crear la clave de provisioning", + "provisioningKeysList": "Nueva clave de aprovisionamiento", + "provisioningKeysMaxBatchSize": "Tamaño máximo de lote", + "provisioningKeysUnlimitedBatchSize": "Tamaño ilimitado del lote (sin límite)", + "provisioningKeysMaxBatchUnlimited": "Ilimitado", + "provisioningKeysMaxBatchSizeInvalid": "Introduzca un tamaño máximo de lote válido (1–1,000,000).", + "provisioningKeysValidUntil": "Válido hasta", + "provisioningKeysValidUntilHint": "Dejar vacío para no expirar.", + "provisioningKeysValidUntilInvalid": "Introduzca una fecha y hora válidas.", + "provisioningKeysNumUsed": "Tiempos usados", + "provisioningKeysLastUsed": "Último uso", + "provisioningKeysNoExpiry": "No expiración", + "provisioningKeysNeverUsed": "Nunca", + "provisioningKeysEdit": "Editar clave de aprovisionamiento", + "provisioningKeysEditDescription": "Actualizar el tamaño máximo de lote y el tiempo de caducidad para esta clave.", + "provisioningKeysApproveNewSites": "Aprobar nuevos sitios", + "provisioningKeysApproveNewSitesDescription": "Aprobar automáticamente los sitios que se registran con esta clave.", + "provisioningKeysUpdateError": "Error al actualizar la clave de aprovisionamiento", + "provisioningKeysUpdated": "Clave de aprovisionamiento actualizada", + "provisioningKeysUpdatedDescription": "Sus cambios han sido guardados.", + "provisioningKeysBannerTitle": "Claves de aprovisionamiento del sitio", + "provisioningKeysBannerDescription": "Genere una clave de aprovisionamiento y utilícela con el conector Newt para crear automáticamente sitios en el primer inicio: no es necesario configurar credenciales separadas para cada sitio.", + "provisioningKeysBannerButtonText": "Saber más", + "pendingSitesBannerTitle": "Sitios pendientes", + "pendingSitesBannerDescription": "Los sitios que se conectan utilizando una clave de aprovisionamiento aparecerán aquí para su revisión.", + "pendingSitesBannerButtonText": "Saber más", "apiKeysSettings": "Ajustes {apiKeyName}", "userTitle": "Administrar todos los usuarios", "userDescription": "Ver y administrar todos los usuarios en el sistema", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Error al activar la clave de licencia", "licenseErrorKeyActivateDescription": "Se ha producido un error al activar la clave de licencia.", "licenseAbout": "Acerca de la licencia", + "licenseBannerTitle": "Habilitar su Licencia Enterprise", + "licenseBannerDescription": "Desbloquea funciones empresariales para tu instancia autohospedada de Pangolin. Compra una clave de licencia para activar capacidades premium, luego agréguela a continuación.", + "licenseBannerGetLicense": "Obtener una Licencia", + "licenseBannerViewDocs": "Ver Documentación", "communityEdition": "Edición comunitaria", "licenseAboutDescription": "Esto es para usuarios empresariales y empresariales que utilizan Pangolin en un entorno comercial. Si estás usando Pangolin para uso personal, puedes ignorar esta sección.", "licenseKeyActivated": "Clave de licencia activada", @@ -509,9 +566,12 @@ "userSaved": "Usuario guardado", "userSavedDescription": "El usuario ha sido actualizado.", "autoProvisioned": "Auto asegurado", + "autoProvisionSettings": "Configuración de Auto Provision", "autoProvisionedDescription": "Permitir a este usuario ser administrado automáticamente por el proveedor de identidad", "accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización", "accessControlsSubmit": "Guardar controles de acceso", + "singleRolePerUserPlanNotice": "Tu plan sólo soporta un rol por usuario.", + "singleRolePerUserEditionNotice": "Esta edición sólo soporta un rol por usuario.", "roles": "Roles", "accessUsersRoles": "Administrar usuarios y roles", "accessUsersRolesDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a la organización", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido", "targetErrorNoSite": "Ningún sitio seleccionado", "targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo", + "targetTargetsCleared": "Objetivos eliminados", + "targetTargetsClearedDescription": "Todos los objetivos han sido eliminados de este recurso", "targetCreated": "Objetivo creado", "targetCreatedDescription": "El objetivo se ha creado correctamente", "targetErrorCreate": "Error al crear el objetivo", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.", "setupTokenRequired": "Se requiere el token de configuración", "actionUpdateSite": "Actualizar sitio", + "actionResetSiteBandwidth": "Restablecer ancho de banda de la organización", "actionListSiteRoles": "Lista de roles permitidos del sitio", "actionCreateResource": "Crear Recurso", "actionDeleteResource": "Eliminar Recurso", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Eliminar usuario", "actionListUsers": "Listar usuarios", "actionAddUserRole": "Añadir rol de usuario", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Establecer roles de usuario", "actionGenerateAccessToken": "Generar token de acceso", "actionDeleteAccessToken": "Eliminar token de acceso", "actionListAccessTokens": "Lista de Tokens de Acceso", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Roles", "sidebarShareableLinks": "Enlaces", "sidebarApiKeys": "Claves API", + "sidebarProvisioning": "Aprovisionamiento", "sidebarSettings": "Ajustes", "sidebarAllUsers": "Todos los usuarios", "sidebarIdentityProviders": "Proveedores de identidad", @@ -1890,6 +1954,40 @@ "exitNode": "Nodo de Salida", "country": "País", "rulesMatchCountry": "Actualmente basado en IP de origen", + "region": "Región", + "selectRegion": "Seleccionar región", + "searchRegions": "Buscar regiones...", + "noRegionFound": "Región no encontrada.", + "rulesMatchRegion": "Seleccione una agrupación regional de países", + "rulesErrorInvalidRegion": "Región no válida", + "rulesErrorInvalidRegionDescription": "Por favor, seleccione una región válida.", + "regionAfrica": "Africa", + "regionNorthernAfrica": "África septentrional", + "regionEasternAfrica": "África oriental", + "regionMiddleAfrica": "África central", + "regionSouthernAfrica": "África del Sur", + "regionWesternAfrica": "África Occidental", + "regionAmericas": "Américas", + "regionCaribbean": "Caribe", + "regionCentralAmerica": "América Central", + "regionSouthAmerica": "América del Sur", + "regionNorthernAmerica": "América del Norte", + "regionAsia": "Asia", + "regionCentralAsia": "Asia Central", + "regionEasternAsia": "Asia oriental", + "regionSouthEasternAsia": "Asia sudoriental", + "regionSouthernAsia": "Asia meridional", + "regionWesternAsia": "Asia Occidental", + "regionEurope": "Europa", + "regionEasternEurope": "Europa del Este", + "regionNorthernEurope": "Europa septentrional", + "regionSouthernEurope": "Europa meridional", + "regionWesternEurope": "Europa Occidental", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australia y Nueva Zelanda", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Autogestionado", "description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra", @@ -1938,6 +2036,25 @@ "invalidValue": "Valor inválido", "idpTypeLabel": "Tipo de proveedor de identidad", "roleMappingExpressionPlaceholder": "e.g., contiene(grupos, 'administrador') && 'administrador' || 'miembro'", + "roleMappingModeFixedRoles": "Roles fijos", + "roleMappingModeMappingBuilder": "Constructor de mapeo", + "roleMappingModeRawExpression": "Expresión sin procesar", + "roleMappingFixedRolesPlaceholderSelect": "Seleccione uno o más roles", + "roleMappingFixedRolesPlaceholderFreeform": "Nombre de rol de tipo (coincidencia exacta por organización)", + "roleMappingFixedRolesDescriptionSameForAll": "Asignar el mismo rol establecido a cada usuario auto-provisionado.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Para las políticas predeterminadas, escriba nombres de roles que existen en cada organización donde los usuarios son proporcionados. Los nombres deben coincidir exactamente.", + "roleMappingClaimPath": "Reclamar ruta", + "roleMappingClaimPathPlaceholder": "grupos", + "roleMappingClaimPathDescription": "Ruta en el payload del token que contiene valores de origen (por ejemplo, grupos).", + "roleMappingMatchValue": "Valor de partida", + "roleMappingAssignRoles": "Asignar roles", + "roleMappingAddMappingRule": "Añadir regla de mapeo", + "roleMappingRawExpressionResultDescription": "La expresión debe evaluar a un array de cadenas o cadenas.", + "roleMappingRawExpressionResultDescriptionSingleRole": "La expresión debe evaluar una cadena (un solo nombre de rol).", + "roleMappingMatchValuePlaceholder": "Valor coincidente (por ejemplo: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Escriba nombres de rol (exacto por org)", + "roleMappingBuilderFreeformRowHint": "Los nombres de rol deben coincidir con un rol en cada organización objetivo.", + "roleMappingRemoveRule": "Eliminar", "idpGoogleConfiguration": "Configuración de Google", "idpGoogleConfigurationDescription": "Configurar las credenciales de Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización", "domainPickerProvidedDomain": "Dominio proporcionado", "domainPickerFreeProvidedDomain": "Dominio proporcionado gratis", + "domainPickerFreeDomainsPaidFeature": "Los dominios proporcionados son una función de pago. Suscríbete para obtener un dominio incluido con tu plan — no necesitas traer el tuyo propio.", "domainPickerVerified": "Verificado", "domainPickerUnverified": "Sin verificar", + "domainPickerManual": "Manual", "domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.", "domainPickerError": "Error", "domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización", @@ -2235,7 +2354,7 @@ "description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario." } }, - "personalUseOnly": "Solo uso personal (licencia gratuita, sin pago)", + "personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)", "buttons": { "continueToCheckout": "Continuar con el pago" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Cuánto tiempo retener los registros de acceso", "logRetentionActionLabel": "Retención de registro de acción", "logRetentionActionDescription": "Cuánto tiempo retener los registros de acción", + "logRetentionConnectionLabel": "Retención de Registro de Conexión", + "logRetentionConnectionDescription": "Cuánto tiempo conservar los registros de conexión", "logRetentionDisabled": "Deshabilitado", "logRetention3Days": "3 días", "logRetention7Days": "7 días", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Fin del año siguiente", "actionLogsDescription": "Ver un historial de acciones realizadas en esta organización", "accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización", + "connectionLogs": "Registros de conexión", + "connectionLogsDescription": "Ver registros de conexión para túneles en esta organización", + "sidebarLogsConnection": "Registros de conexión", + "sidebarLogsStreaming": "Transmisión", + "sourceAddress": "Dirección de origen", + "destinationAddress": "Dirección de destino", + "duration": "Duración", "licenseRequiredToUse": "Se requiere una licencia Enterprise Edition o Pangolin Cloud para usar esta función. Reserve una demostración o prueba POC.", "ossEnterpriseEditionRequired": "La Enterprise Edition es necesaria para utilizar esta función. Esta función también está disponible en Pangolin Cloud. Reserva una demostración o prueba POC.", "certResolver": "Resolver certificado", @@ -2487,6 +2615,9 @@ "machineClients": "Clientes de la máquina", "install": "Instalar", "run": "Ejecutar", + "envFile": "Archivo de Entorno", + "serviceFile": "Archivo de Servicio", + "enableAndStart": "Habilitar y empezar", "clientNameDescription": "El nombre mostrado del cliente que se puede cambiar más adelante.", "clientAddress": "Dirección del cliente (Avanzado)", "setupFailedToFetchSubnet": "No se pudo obtener la subred por defecto", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Editar un rol y habilitar la opción 'Requerir aprobaciones de dispositivos'. Los usuarios con este rol necesitarán la aprobación del administrador para nuevos dispositivos.", "approvalsEmptyStatePreviewDescription": "Vista previa: Cuando está habilitado, las solicitudes de dispositivo pendientes aparecerán aquí para su revisión", "approvalsEmptyStateButtonText": "Administrar roles", - "domainErrorTitle": "Estamos teniendo problemas para verificar su dominio" + "domainErrorTitle": "Estamos teniendo problemas para verificar su dominio", + "idpAdminAutoProvisionPoliciesTabHint": "Configure el mapeo de roles y las políticas de organización en la pestaña Configuración de provisión automática.", + "streamingTitle": "Transmisión de Eventos", + "streamingDescription": "Transmita eventos desde su organización a destinos externos en tiempo real.", + "streamingUnnamedDestination": "Destino sin nombre", + "streamingNoUrlConfigured": "No hay URL configurada", + "streamingAddDestination": "Añadir destino", + "streamingHttpWebhookTitle": "Webhook HTTP", + "streamingHttpWebhookDescription": "Enviar eventos a cualquier extremo HTTP con autenticación flexible y plantilla.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Transmite eventos a un bucket de almacenamiento de objetos compatible con S3. Próximamente.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.", + "streamingTypePickerDescription": "Elija un tipo de destino para empezar.", + "streamingFailedToLoad": "Error al cargar destinos", + "streamingUnexpectedError": "Se ha producido un error inesperado.", + "streamingFailedToUpdate": "Error al actualizar destino", + "streamingDeletedSuccess": "Destino eliminado correctamente", + "streamingFailedToDelete": "Error al eliminar destino", + "streamingDeleteTitle": "Eliminar destino", + "streamingDeleteButtonText": "Eliminar destino", + "streamingDeleteDialogAreYouSure": "¿Está seguro que desea eliminar", + "streamingDeleteDialogThisDestination": "este destino", + "streamingDeleteDialogPermanentlyRemoved": "? Toda la configuración se eliminará permanentemente.", + "httpDestEditTitle": "Editar destino", + "httpDestAddTitle": "Añadir destino HTTP", + "httpDestEditDescription": "Actualizar la configuración para este destino de transmisión de eventos HTTP.", + "httpDestAddDescription": "Configure un nuevo extremo HTTP para recibir los eventos de su organización.", + "httpDestTabSettings": "Ajustes", + "httpDestTabHeaders": "Encabezados", + "httpDestTabBody": "Cuerpo", + "httpDestTabLogs": "Registros", + "httpDestNamePlaceholder": "Mi destino HTTP", + "httpDestUrlLabel": "URL de destino", + "httpDestUrlErrorHttpRequired": "URL debe usar http o https", + "httpDestUrlErrorHttpsRequired": "HTTPS es necesario en implementaciones en la nube", + "httpDestUrlErrorInvalid": "Introduzca una URL válida (ej. https://example.com/webhook)", + "httpDestAuthTitle": "Autenticación", + "httpDestAuthDescription": "Elija cómo están autenticadas las solicitudes en su punto final.", + "httpDestAuthNoneTitle": "Sin autenticación", + "httpDestAuthNoneDescription": "Envía solicitudes sin un encabezado de autorización.", + "httpDestAuthBearerTitle": "Tóken de portador", + "httpDestAuthBearerDescription": "Añade un encabezado Authorization: Bearer '' a cada solicitud.", + "httpDestAuthBearerPlaceholder": "Tu clave o token API", + "httpDestAuthBasicTitle": "Auth Básica", + "httpDestAuthBasicDescription": "Añade un encabezado Authorization: Basic ''. Proporcione las credenciales como nombredeusuario:contraseña.", + "httpDestAuthBasicPlaceholder": "usuario:contraseña", + "httpDestAuthCustomTitle": "Cabecera personalizada", + "httpDestAuthCustomDescription": "Especifique un nombre de cabecera HTTP personalizado y un valor para la autenticación (por ejemplo, X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Nombre de cabecera (ej. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Valor de cabecera", + "httpDestCustomHeadersTitle": "Cabeceras HTTP personalizadas", + "httpDestCustomHeadersDescription": "Añadir cabeceras personalizadas a cada petición saliente. Útil para tokens estáticos o un tipo de contenido personalizado. De forma predeterminada, Content Type: application/json es enviado.", + "httpDestNoHeadersConfigured": "No hay cabeceras personalizadas. Haga clic en \"Añadir cabecera\" para añadir una.", + "httpDestHeaderNamePlaceholder": "Nombre de cabecera", + "httpDestHeaderValuePlaceholder": "Valor", + "httpDestAddHeader": "Añadir cabecera", + "httpDestBodyTemplateTitle": "Plantilla de cuerpo personalizada", + "httpDestBodyTemplateDescription": "Controla la estructura de carga de JSON enviada a tu punto final. Si está desactivado, se envía un objeto JSON por defecto para cada evento.", + "httpDestEnableBodyTemplate": "Activar plantilla de cuerpo personalizado", + "httpDestBodyTemplateLabel": "Plantilla de cuerpo (JSON)", + "httpDestBodyTemplateHint": "Utilice variables de plantilla para referenciar los campos del evento en su carga útil.", + "httpDestPayloadFormatTitle": "Formato de carga", + "httpDestPayloadFormatDescription": "Cómo se serializan los eventos en cada cuerpo de solicitud.", + "httpDestFormatJsonArrayTitle": "Matriz JSON", + "httpDestFormatJsonArrayDescription": "Una petición por lote, cuerpo es una matriz JSON. Compatible con la mayoría de los webhooks y Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Una petición por lote, el cuerpo es JSON delimitado por línea — un objeto por línea, sin arrays externos. Requerido por Splunk HEC, Elastic / OpenSearch, y Grafana Loki.", + "httpDestFormatSingleTitle": "Un evento por solicitud", + "httpDestFormatSingleDescription": "Envía un HTTP POST separado para cada evento individual. Úsalo sólo para los extremos que no pueden manejar lotes.", + "httpDestLogTypesTitle": "Tipos de Log", + "httpDestLogTypesDescription": "Elija qué tipos de registro son reenviados a este destino. Sólo los tipos de registro habilitados serán transmitidos.", + "httpDestAccessLogsTitle": "Registros de acceso", + "httpDestAccessLogsDescription": "Intentos de acceso a recursos, incluyendo solicitudes autenticadas y denegadas.", + "httpDestActionLogsTitle": "Registros de acción", + "httpDestActionLogsDescription": "Acciones administrativas realizadas por los usuarios dentro de la organización.", + "httpDestConnectionLogsTitle": "Registros de conexión", + "httpDestConnectionLogsDescription": "Eventos de conexión de sitios y túneles, incluyendo conexiones y desconexiones.", + "httpDestRequestLogsTitle": "Registros de Solicitud", + "httpDestRequestLogsDescription": "Registros de peticiones HTTP para recursos proxyficados, incluyendo método, ruta y código de respuesta.", + "httpDestSaveChanges": "Guardar Cambios", + "httpDestCreateDestination": "Crear destino", + "httpDestUpdatedSuccess": "Destino actualizado correctamente", + "httpDestCreatedSuccess": "Destino creado correctamente", + "httpDestUpdateFailed": "Error al actualizar destino", + "httpDestCreateFailed": "Error al crear el destino" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 2b3368a07..8ede738ec 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -148,6 +148,11 @@ "createLink": "Créer un lien", "resourcesNotFound": "Aucune ressource trouvée", "resourceSearch": "Rechercher des ressources", + "machineSearch": "Rechercher des machines", + "machinesSearch": "Rechercher des clients de la machine...", + "machineNotFound": "Aucune machine trouvée", + "userDeviceSearch": "Rechercher des périphériques utilisateur", + "userDevicesSearch": "Rechercher des appareils utilisateurs...", "openMenu": "Ouvrir le menu", "resource": "Ressource", "title": "Titre de la page", @@ -323,6 +328,54 @@ "apiKeysDelete": "Supprimer la clé d'API", "apiKeysManage": "Gérer les clés d'API", "apiKeysDescription": "Les clés d'API sont utilisées pour s'authentifier avec l'API d'intégration", + "provisioningKeysTitle": "Clé de provisioning", + "provisioningKeysManage": "Gérer les clés de provisioning", + "provisioningKeysDescription": "Les clés de provisioning sont utilisées pour authentifier la fourniture automatique de sites pour votre organisation.", + "provisioningManage": "Mise en place", + "provisioningDescription": "Gérer les clés de provisioning et examiner les sites en attente d'approbation.", + "pendingSites": "Sites en attente", + "siteApproveSuccess": "Site approuvé avec succès", + "siteApproveError": "Erreur lors de l'approbation du site", + "provisioningKeys": "Clés de provisionnement", + "searchProvisioningKeys": "Recherche des clés de provision...", + "provisioningKeysAdd": "Générer une clé de provisioning", + "provisioningKeysErrorDelete": "Erreur lors de la suppression de la clé de provisioning", + "provisioningKeysErrorDeleteMessage": "Erreur lors de la suppression de la clé de provisioning", + "provisioningKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer cette clé de provisioning de l'organisation ?", + "provisioningKeysMessageRemove": "Une fois supprimée, la clé ne peut plus être utilisée pour le provisionnement du site.", + "provisioningKeysDeleteConfirm": "Confirmer la suppression de la clé de provisioning", + "provisioningKeysDelete": "Supprimer la clé de provisioning", + "provisioningKeysCreate": "Générer une clé de provisioning", + "provisioningKeysCreateDescription": "Générer une nouvelle clé de provisioning pour l'organisation", + "provisioningKeysSeeAll": "Voir toutes les clés de provisioning", + "provisioningKeysSave": "Enregistrer la clé de provisioning", + "provisioningKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Copiez-le dans un endroit sécurisé.", + "provisioningKeysErrorCreate": "Erreur lors de la création de la clé de provisioning", + "provisioningKeysList": "Nouvelle clé de provisioning", + "provisioningKeysMaxBatchSize": "Taille maximale du lot", + "provisioningKeysUnlimitedBatchSize": "Taille de lot illimitée (sans limite)", + "provisioningKeysMaxBatchUnlimited": "Illimité", + "provisioningKeysMaxBatchSizeInvalid": "Entrez une taille de lot maximale valide (1–1 000 000).", + "provisioningKeysValidUntil": "Valable jusqu'au", + "provisioningKeysValidUntilHint": "Laisser vide pour ne pas expirer.", + "provisioningKeysValidUntilInvalid": "Entrez une date et une heure valides.", + "provisioningKeysNumUsed": "Nombre de fois utilisées", + "provisioningKeysLastUsed": "Dernière utilisation", + "provisioningKeysNoExpiry": "Pas d'expiration", + "provisioningKeysNeverUsed": "Jamais", + "provisioningKeysEdit": "Modifier la clé de provisioning", + "provisioningKeysEditDescription": "Mettre à jour la taille maximale du lot et la durée d'expiration de cette clé.", + "provisioningKeysApproveNewSites": "Approuver les nouveaux sites", + "provisioningKeysApproveNewSitesDescription": "Approuver automatiquement les sites qui s'inscrivent avec cette clé.", + "provisioningKeysUpdateError": "Erreur lors de la mise à jour de la clé de provisioning", + "provisioningKeysUpdated": "Clé de provisioning mise à jour", + "provisioningKeysUpdatedDescription": "Vos modifications ont été enregistrées.", + "provisioningKeysBannerTitle": "Clés de provisioning du site", + "provisioningKeysBannerDescription": "Générez une clé de provisionnement et utilisez-la avec le connecteur Newt pour créer automatiquement des sites lors du premier démarrage - sans besoin de configurer des identifiants séparés pour chaque site.", + "provisioningKeysBannerButtonText": "En savoir plus", + "pendingSitesBannerTitle": "Sites en attente", + "pendingSitesBannerDescription": "Les sites qui se connectent en utilisant une clé de provisionnement apparaissent ici pour révision.", + "pendingSitesBannerButtonText": "En savoir plus", "apiKeysSettings": "Paramètres de {apiKeyName}", "userTitle": "Gérer tous les utilisateurs", "userDescription": "Voir et gérer tous les utilisateurs du système", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Échec de l'activation de la clé de licence", "licenseErrorKeyActivateDescription": "Une erreur s'est produite lors de l'activation de la clé de licence.", "licenseAbout": "À propos de la licence", + "licenseBannerTitle": "Activer Votre Licence Entreprise", + "licenseBannerDescription": "Débloquez les fonctionnalités d'entreprise pour votre instance autohébergée de Pangolin. Achetez une clé de licence pour activer les capacités premium, puis ajoutez-la ci-dessous.", + "licenseBannerGetLicense": "Obtenez une Licence", + "licenseBannerViewDocs": "Afficher la Documentation", "communityEdition": "Edition Communautaire", "licenseAboutDescription": "Ceci est destiné aux entreprises qui utilisent Pangolin dans un environnement commercial. Si vous utilisez Pangolin pour un usage personnel, vous pouvez ignorer cette section.", "licenseKeyActivated": "Clé de licence activée", @@ -509,9 +566,12 @@ "userSaved": "Utilisateur enregistré", "userSavedDescription": "L'utilisateur a été mis à jour.", "autoProvisioned": "Auto-provisionné", + "autoProvisionSettings": "Paramètres de la fourniture automatique", "autoProvisionedDescription": "Permettre à cet utilisateur d'être géré automatiquement par le fournisseur d'identité", "accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation", "accessControlsSubmit": "Enregistrer les contrôles d'accès", + "singleRolePerUserPlanNotice": "Votre plan ne prend en charge qu'un seul rôle par utilisateur.", + "singleRolePerUserEditionNotice": "Cette édition ne prend en charge qu'un rôle par utilisateur.", "roles": "Rôles", "accessUsersRoles": "Gérer les utilisateurs et les rôles", "accessUsersRolesDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à l'organisation", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide", "targetErrorNoSite": "Aucun site sélectionné", "targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible", + "targetTargetsCleared": "Cibles effacées", + "targetTargetsClearedDescription": "Toutes les cibles ont été retirées de cette ressource", "targetCreated": "Cible créée", "targetCreatedDescription": "La cible a été créée avec succès", "targetErrorCreate": "Impossible de créer la cible", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.", "setupTokenRequired": "Le jeton de configuration est requis.", "actionUpdateSite": "Mettre à jour un site", + "actionResetSiteBandwidth": "Réinitialiser la bande passante de l'organisation", "actionListSiteRoles": "Lister les rôles autorisés du site", "actionCreateResource": "Créer une ressource", "actionDeleteResource": "Supprimer une ressource", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Supprimer un utilisateur", "actionListUsers": "Lister les utilisateurs", "actionAddUserRole": "Ajouter un rôle utilisateur", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Définir les rôles de l'utilisateur", "actionGenerateAccessToken": "Générer un jeton d'accès", "actionDeleteAccessToken": "Supprimer un jeton d'accès", "actionListAccessTokens": "Lister les jetons d'accès", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Rôles", "sidebarShareableLinks": "Liens", "sidebarApiKeys": "Clés API", + "sidebarProvisioning": "Mise en place", "sidebarSettings": "Réglages", "sidebarAllUsers": "Tous les utilisateurs", "sidebarIdentityProviders": "Fournisseurs d'identité", @@ -1890,6 +1954,40 @@ "exitNode": "Nœud de sortie", "country": "Pays", "rulesMatchCountry": "Actuellement basé sur l'IP source", + "region": "Région", + "selectRegion": "Sélectionner une région", + "searchRegions": "Rechercher des régions...", + "noRegionFound": "Aucune région trouvée.", + "rulesMatchRegion": "Sélectionnez un groupement régional de pays", + "rulesErrorInvalidRegion": "Région invalide", + "rulesErrorInvalidRegionDescription": "Veuillez sélectionner une région valide.", + "regionAfrica": "L'Afrique", + "regionNorthernAfrica": "Afrique du Nord", + "regionEasternAfrica": "Afrique de l'Est", + "regionMiddleAfrica": "Afrique Moyenne", + "regionSouthernAfrica": "Afrique australe", + "regionWesternAfrica": "Afrique de l'Ouest", + "regionAmericas": "Amériques", + "regionCaribbean": "Caraïbes", + "regionCentralAmerica": "Amérique centrale", + "regionSouthAmerica": "Amérique du Sud", + "regionNorthernAmerica": "Amérique du Nord", + "regionAsia": "L'Asie", + "regionCentralAsia": "Asie centrale", + "regionEasternAsia": "Asie de l'Est", + "regionSouthEasternAsia": "Asie du Sud-Est", + "regionSouthernAsia": "Asie du Sud", + "regionWesternAsia": "Asie de l'Ouest", + "regionEurope": "L’Europe", + "regionEasternEurope": "Europe de l'Est", + "regionNorthernEurope": "Europe du Nord", + "regionSouthernEurope": "Europe du Sud", + "regionWesternEurope": "Europe occidentale", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australie et Nouvelle-Zélande", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Gestion autonome", "description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires", @@ -1938,6 +2036,25 @@ "invalidValue": "Valeur non valide", "idpTypeLabel": "Type de fournisseur d'identité", "roleMappingExpressionPlaceholder": "ex: contenu(groupes) && 'admin' || 'membre'", + "roleMappingModeFixedRoles": "Rôles fixes", + "roleMappingModeMappingBuilder": "Constructeur de cartographie", + "roleMappingModeRawExpression": "Expression brute", + "roleMappingFixedRolesPlaceholderSelect": "Sélectionnez un ou plusieurs rôles", + "roleMappingFixedRolesPlaceholderFreeform": "Tapez les noms des rôles (correspondance exacte par organisation)", + "roleMappingFixedRolesDescriptionSameForAll": "Assigner le même jeu de rôles à chaque utilisateur auto-provisionné.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Pour les politiques par défaut, les noms de rôles de type qui existent dans chaque organisation où les utilisateurs sont fournis. Les noms doivent correspondre exactement.", + "roleMappingClaimPath": "Chemin de revendication", + "roleMappingClaimPathPlaceholder": "Groupes", + "roleMappingClaimPathDescription": "Chemin dans le bloc de jeton qui contient les valeurs source (par exemple, les groupes).", + "roleMappingMatchValue": "Valeur de la correspondance", + "roleMappingAssignRoles": "Assigner des rôles", + "roleMappingAddMappingRule": "Ajouter une règle de mappage", + "roleMappingRawExpressionResultDescription": "L'expression doit être évaluée à une chaîne ou un tableau de chaînes.", + "roleMappingRawExpressionResultDescriptionSingleRole": "L'expression doit être évaluée à une chaîne (un seul nom de rôle).", + "roleMappingMatchValuePlaceholder": "Valeur de la correspondance (par exemple: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Tapez les noms des rôles (exact par org)", + "roleMappingBuilderFreeformRowHint": "Les noms de rôle doivent correspondre à un rôle dans chaque organisation cible.", + "roleMappingRemoveRule": "Supprimer", "idpGoogleConfiguration": "Configuration Google", "idpGoogleConfigurationDescription": "Configurer les identifiants Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation", "domainPickerProvidedDomain": "Domaine fourni", "domainPickerFreeProvidedDomain": "Domaine fourni gratuitement", + "domainPickerFreeDomainsPaidFeature": "Les domaines fournis sont une fonctionnalité payante. Abonnez-vous pour obtenir un domaine inclus avec votre plan — plus besoin de fournir le vôtre.", "domainPickerVerified": "Vérifié", "domainPickerUnverified": "Non vérifié", + "domainPickerManual": "Manuel", "domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.", "domainPickerError": "Erreur", "domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation", @@ -2235,7 +2354,7 @@ "description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire." } }, - "personalUseOnly": "Utilisation personnelle uniquement (licence gratuite — sans checkout)", + "personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)", "buttons": { "continueToCheckout": "Continuer vers le paiement" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Durée de conservation des journaux d'accès", "logRetentionActionLabel": "Retention du journal des actions", "logRetentionActionDescription": "Durée de conservation du journal des actions", + "logRetentionConnectionLabel": "Rétention du journal de connexion", + "logRetentionConnectionDescription": "Durée de conservation des logs de connexion", "logRetentionDisabled": "Désactivé", "logRetention3Days": "3 jours", "logRetention7Days": "7 jours", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Fin de l'année suivante", "actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation", "accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation", + "connectionLogs": "Journaux de connexion", + "connectionLogsDescription": "Voir les journaux de connexion pour les tunnels de cette organisation", + "sidebarLogsConnection": "Journaux de connexion", + "sidebarLogsStreaming": "Streaming en cours", + "sourceAddress": "Adresse source", + "destinationAddress": "Adresse de destination", + "duration": "Durée", "licenseRequiredToUse": "Une licence Enterprise Edition ou Pangolin Cloud est requise pour utiliser cette fonctionnalité. Réservez une démonstration ou une évaluation de POC.", "ossEnterpriseEditionRequired": "La version Enterprise Edition est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans Pangolin Cloud. Réservez une démo ou un essai POC.", "certResolver": "Résolveur de certificat", @@ -2487,6 +2615,9 @@ "machineClients": "Clients Machines", "install": "Installer", "run": "Exécuter", + "envFile": "Fichier Environnement", + "serviceFile": "Fichier de Service", + "enableAndStart": "Activer et Démarrer", "clientNameDescription": "Le nom d'affichage du client qui peut être modifié plus tard.", "clientAddress": "Adresse du client (Avancé)", "setupFailedToFetchSubnet": "Impossible de récupérer le sous-réseau par défaut", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Modifier un rôle et activer l'option 'Exiger les autorisations de l'appareil'. Les utilisateurs avec ce rôle auront besoin de l'approbation de l'administrateur pour les nouveaux appareils.", "approvalsEmptyStatePreviewDescription": "Aperçu: Lorsque cette option est activée, les demandes de périphérique en attente apparaîtront ici pour vérification", "approvalsEmptyStateButtonText": "Gérer les rôles", - "domainErrorTitle": "Nous avons des difficultés à vérifier votre domaine" + "domainErrorTitle": "Nous avons des difficultés à vérifier votre domaine", + "idpAdminAutoProvisionPoliciesTabHint": "Configurer les politiques de mappage des rôles et de l'organisation dans l'onglet Paramètres de la fourniture automatique.", + "streamingTitle": "Streaming d'événements", + "streamingDescription": "Diffusez en temps réel des événements de votre organisation vers des destinations externes.", + "streamingUnnamedDestination": "Destination sans nom", + "streamingNoUrlConfigured": "Aucune URL configurée", + "streamingAddDestination": "Ajouter une destination", + "streamingHttpWebhookTitle": "Webhook HTTP", + "streamingHttpWebhookDescription": "Envoyez des événements à n'importe quel point de terminaison HTTP avec une authentification flexible et un template.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Flux d'événements vers un compartiment de stockage d'objet compatible S3. Bientôt.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.", + "streamingTypePickerDescription": "Choisissez un type de destination pour commencer.", + "streamingFailedToLoad": "Impossible de charger les destinations", + "streamingUnexpectedError": "Une erreur inattendue s'est produite.", + "streamingFailedToUpdate": "Impossible de mettre à jour la destination", + "streamingDeletedSuccess": "Destination supprimée avec succès", + "streamingFailedToDelete": "Impossible de supprimer la destination", + "streamingDeleteTitle": "Supprimer la destination", + "streamingDeleteButtonText": "Supprimer la destination", + "streamingDeleteDialogAreYouSure": "Êtes-vous sûr de vouloir supprimer", + "streamingDeleteDialogThisDestination": "cette destination", + "streamingDeleteDialogPermanentlyRemoved": "? Toutes les configurations seront définitivement supprimées.", + "httpDestEditTitle": "Modifier la destination", + "httpDestAddTitle": "Ajouter une destination HTTP", + "httpDestEditDescription": "Mettre à jour la configuration pour cette destination de streaming d'événements HTTP.", + "httpDestAddDescription": "Configurez un nouveau point de terminaison HTTP pour recevoir les événements de votre organisation.", + "httpDestTabSettings": "Réglages", + "httpDestTabHeaders": "En-têtes", + "httpDestTabBody": "Corps", + "httpDestTabLogs": "Journaux", + "httpDestNamePlaceholder": "Ma destination HTTP", + "httpDestUrlLabel": "URL de destination", + "httpDestUrlErrorHttpRequired": "L'URL doit utiliser http ou https", + "httpDestUrlErrorHttpsRequired": "HTTPS est requis pour les déploiements du cloud", + "httpDestUrlErrorInvalid": "Entrez une URL valide (par exemple https://example.com/webhook)", + "httpDestAuthTitle": "Authentification", + "httpDestAuthDescription": "Choisissez comment les requêtes à votre terminaison sont authentifiées.", + "httpDestAuthNoneTitle": "Aucune authentification", + "httpDestAuthNoneDescription": "Envoie des requêtes sans en-tête d'autorisation.", + "httpDestAuthBearerTitle": "Jeton de Porteur", + "httpDestAuthBearerDescription": "Ajoute un en-tête Authorization: Bearer '' à chaque requête.", + "httpDestAuthBearerPlaceholder": "Votre clé API ou votre jeton", + "httpDestAuthBasicTitle": "Authentification basique", + "httpDestAuthBasicDescription": "Ajoute un en-tête Authorization: Basic ''. Fournissez les identifiants sous la forme nom d'utilisateur:mot de passe.", + "httpDestAuthBasicPlaceholder": "nom d'utilisateur:mot de passe", + "httpDestAuthCustomTitle": "En-tête personnalisé", + "httpDestAuthCustomDescription": "Spécifiez un nom d'en-tête HTTP personnalisé et une valeur pour l'authentification (par exemple X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Nom de l'en-tête (par exemple X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Valeur de l'en-tête", + "httpDestCustomHeadersTitle": "En-têtes HTTP personnalisés", + "httpDestCustomHeadersDescription": "Ajouter des en-têtes personnalisés à chaque requête sortante. Utile pour les jetons statiques ou un type de contenu personnalisé. Par défaut, Content-Type: application/json est envoyé.", + "httpDestNoHeadersConfigured": "Aucun en-tête personnalisé configuré. Cliquez sur \"Ajouter un en-tête\" pour en ajouter un.", + "httpDestHeaderNamePlaceholder": "Nom de l'en-tête", + "httpDestHeaderValuePlaceholder": "Valeur", + "httpDestAddHeader": "Ajouter un en-tête", + "httpDestBodyTemplateTitle": "Modèle de corps personnalisé", + "httpDestBodyTemplateDescription": "Contrôle la structure de charge utile JSON envoyée à votre terminal. Si désactivé, un objet JSON par défaut est envoyé pour chaque événement.", + "httpDestEnableBodyTemplate": "Activer le modèle de corps personnalisé", + "httpDestBodyTemplateLabel": "Modèle de corps (JSON)", + "httpDestBodyTemplateHint": "Utilisez les variables de modèle pour référencer les champs d'événement dans votre charge utile.", + "httpDestPayloadFormatTitle": "Format de la charge utile", + "httpDestPayloadFormatDescription": "Comment les événements sont sérialisés dans chaque corps de requête.", + "httpDestFormatJsonArrayTitle": "Tableau JSON", + "httpDestFormatJsonArrayDescription": "Une requête par lot, le corps est un tableau JSON. Compatible avec la plupart des webhooks génériques et des datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Une requête par lot, body est un JSON délimité par une nouvelle ligne — un objet par ligne, pas de tableau extérieur. Requis par Splunk HEC, Elastic / OpenSearch, et Grafana Loki.", + "httpDestFormatSingleTitle": "Un événement par demande", + "httpDestFormatSingleDescription": "Envoie un POST HTTP séparé pour chaque événement individuel. Utilisé uniquement pour les terminaux qui ne peuvent pas gérer des lots.", + "httpDestLogTypesTitle": "Types de logs", + "httpDestLogTypesDescription": "Choisissez quels types de journaux sont envoyés à cette destination. Seuls les types de journaux activés seront diffusés.", + "httpDestAccessLogsTitle": "Journaux d'accès", + "httpDestAccessLogsDescription": "Tentatives d'accès aux ressources, y compris les demandes authentifiées et refusées.", + "httpDestActionLogsTitle": "Journaux des actions", + "httpDestActionLogsDescription": "Actions administratives effectuées par les utilisateurs au sein de l'organisation.", + "httpDestConnectionLogsTitle": "Journaux de connexion", + "httpDestConnectionLogsDescription": "Événements de connexion du site et du tunnel, y compris les connexions et les déconnexions.", + "httpDestRequestLogsTitle": "Journal des requêtes", + "httpDestRequestLogsDescription": "Journaux des requêtes HTTP pour les ressources proxiées, y compris la méthode, le chemin et le code de réponse.", + "httpDestSaveChanges": "Enregistrer les modifications", + "httpDestCreateDestination": "Créer une destination", + "httpDestUpdatedSuccess": "Destination mise à jour avec succès", + "httpDestCreatedSuccess": "Destination créée avec succès", + "httpDestUpdateFailed": "Impossible de mettre à jour la destination", + "httpDestCreateFailed": "Impossible de créer la destination" } diff --git a/messages/it-IT.json b/messages/it-IT.json index 6891cd290..5e0f13a7e 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1,19 +1,19 @@ { "setupCreate": "Creare l'organizzazione, il sito e le risorse", - "headerAuthCompatibilityInfo": "Abilita questo per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.", + "headerAuthCompatibilityInfo": "Abilita questa funzionalità per forzare una risposta 401 Unauthorized quando manca un token di autenticazione. Questo è richiesto per browser o librerie HTTP specifiche che non inviano credenziali senza una sfida del server.", "headerAuthCompatibility": "Compatibilità estesa", "setupNewOrg": "Nuova Organizzazione", "setupCreateOrg": "Crea Organizzazione", "setupCreateResources": "Crea Risorse", - "setupOrgName": "Nome Dell'Organizzazione", + "setupOrgName": "Nome dell'Organizzazione", "orgDisplayName": "Questo è il nome visualizzato dell'organizzazione.", "orgId": "Id Organizzazione", "setupIdentifierMessage": "Questo è l'identificatore univoco per l'organizzazione.", "setupErrorIdentifier": "L'ID dell'organizzazione è già utilizzato. Si prega di sceglierne uno diverso.", "componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.", "componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.", - "welcome": "Benvenuti a Pangolin", - "welcomeTo": "Benvenuto a", + "welcome": "Benvenuto su Pangolin!", + "welcomeTo": "Benvenuto su Pangolin!", "componentsCreateOrg": "Crea un'organizzazione", "componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.", "componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.", @@ -27,7 +27,7 @@ "inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.", "inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.", "inviteCreateUser": "Si prega di creare un account prima.", - "goHome": "Vai A Home", + "goHome": "Vai alla Home", "inviteLogInOtherUser": "Accedi come utente diverso", "createAnAccount": "Crea un account", "inviteNotAccepted": "Invito Non Accettato", @@ -51,7 +51,7 @@ "edit": "Modifica", "siteConfirmDelete": "Conferma Eliminazione Sito", "siteDelete": "Elimina Sito", - "siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli obiettivi associati al sito verranno rimossi.", + "siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.", "siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?", "siteManageSites": "Gestisci Siti", "siteDescription": "Creare e gestire siti per abilitare la connettività a reti private", @@ -75,9 +75,9 @@ "siteLoadWGConfig": "Caricamento configurazione WireGuard...", "siteDocker": "Espandi per i dettagli di distribuzione Docker", "toggle": "Attiva/disattiva", - "dockerCompose": "Composizione Docker", + "dockerCompose": "Docker Compose", "dockerRun": "Corsa Docker", - "siteLearnLocal": "I siti locali non tunnel, saperne di più", + "siteLearnLocal": "I siti locali non effettuano il tunnel, per saperne di più", "siteConfirmCopy": "Ho copiato la configurazione", "searchSitesProgress": "Cerca siti...", "siteAdd": "Aggiungi Sito", @@ -88,29 +88,29 @@ "operatingSystem": "Sistema Operativo", "commands": "Comandi", "recommended": "Consigliato", - "siteNewtDescription": "Per la migliore esperienza utente, utilizzare Newt. Utilizza WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.", + "siteNewtDescription": "Per la migliore esperienza utente utilizzare Newt, che usa WireGuard sotto il cofano e ti permette di indirizzare le tue risorse private tramite il loro indirizzo LAN sulla tua rete privata dall'interno della dashboard Pangolin.", "siteRunsInDocker": "Esegue nel Docker", "siteRunsInShell": "Esegue in shell su macOS, Linux e Windows", - "siteErrorDelete": "Errore nell'eliminare il sito", + "siteErrorDelete": "Errore nella eliminazione del sito", "siteErrorUpdate": "Impossibile aggiornare il sito", "siteErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento del sito.", "siteUpdated": "Sito aggiornato", "siteUpdatedDescription": "Il sito è stato aggiornato.", "siteGeneralDescription": "Configura le impostazioni generali per questo sito", "siteSettingDescription": "Configura le impostazioni del sito", - "siteSetting": "Impostazioni {siteName}", + "siteSetting": "Impostazioni del sito {siteName}", "siteNewtTunnel": "Nuovo Sito (Consigliato)", "siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.", "siteWg": "WireGuard Base", - "siteWgDescription": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", - "siteWgDescriptionSaas": "Usa qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta. FUNZIONA SOLO SU NODI AUTO-OSPITATI", + "siteWgDescription": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", + "siteWgDescriptionSaas": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.", "siteLocalDescription": "Solo risorse locali. Nessun tunneling.", "siteLocalDescriptionSaas": "Solo risorse locali. Nessun tunneling. Disponibile solo su nodi remoti.", "siteSeeAll": "Vedi Tutti I Siti", - "siteTunnelDescription": "Determinare come si desidera connettersi al sito", + "siteTunnelDescription": "Selezionare la modalità con la quale si desidera connettersi al sito", "siteNewtCredentials": "Credenziali", - "siteNewtCredentialsDescription": "Questo è come il sito si autenticerà con il server", - "remoteNodeCredentialsDescription": "Questo è come il nodo remoto si autenticherà con il server", + "siteNewtCredentialsDescription": "Questo è come il sito si autenticherà con il server", + "remoteNodeCredentialsDescription": "Questo è il modo in cui il nodo remoto si autenticherà con il server", "siteCredentialsSave": "Salva le credenziali", "siteCredentialsSaveDescription": "Potrai vederlo solo una volta. Assicurati di copiarlo in un luogo sicuro.", "siteInfo": "Informazioni Sito", @@ -140,14 +140,19 @@ "shareCreateDescription": "Chiunque con questo link può accedere alla risorsa", "shareTitleOptional": "Titolo (facoltativo)", "expireIn": "Scadenza In", - "neverExpire": "Mai scadere", - "shareExpireDescription": "Il tempo di scadenza è per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.", + "neverExpire": "Nessuna scadenza", + "shareExpireDescription": "Il tempo di scadenza indica per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.", "shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.", "shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.", "shareTokenUsage": "Vedi Utilizzo Token Di Accesso", "createLink": "Crea Collegamento", "resourcesNotFound": "Nessuna risorsa trovata", "resourceSearch": "Cerca risorse", + "machineSearch": "Ricerca macchine", + "machinesSearch": "Cerca client macchina...", + "machineNotFound": "Nessuna macchina trovata", + "userDeviceSearch": "Cerca dispositivi utente", + "userDevicesSearch": "Cerca dispositivi utente...", "openMenu": "Apri menu", "resource": "Risorsa", "title": "Titolo", @@ -156,9 +161,9 @@ "never": "Mai", "shareErrorSelectResource": "Seleziona una risorsa", "proxyResourceTitle": "Gestisci Risorse Pubbliche", - "proxyResourceDescription": "Creare e gestire risorse accessibili al pubblico tramite un browser web", + "proxyResourceDescription": "Creare e gestire risorse pubbliche accessibili tramite un browser web", "proxyResourcesBannerTitle": "Accesso Pubblico Basato sul Web", - "proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili a chiunque su Internet tramite un browser web. A differenza delle risorse private, non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.", + "proxyResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS o TCP/UDP accessibili da chiunque tramite Internet da un browser web. A differenza delle risorse private non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.", "clientResourceTitle": "Gestisci Risorse Private", "clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso", "privateResourcesBannerTitle": "Accesso Privato Zero-Trust", @@ -169,12 +174,12 @@ "authentication": "Autenticazione", "protected": "Protetto", "notProtected": "Non Protetto", - "resourceMessageRemove": "Una volta rimossa, la risorsa non sarà più accessibile. Tutti gli obiettivi associati alla risorsa saranno rimossi.", + "resourceMessageRemove": "Una volta rimossa la risorsa non sarà più accessibile. Tutti gli oggetti target associati alla risorsa saranno rimossi.", "resourceQuestionRemove": "Sei sicuro di voler rimuovere la risorsa dall'organizzazione?", "resourceHTTP": "Risorsa HTTPS", "resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.", "resourceRaw": "Risorsa Raw TCP/UDP", - "resourceRawDescription": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta.", + "resourceRawDescription": "Richieste proxy su TCP/UDP raw utilizzando un numero di porta.", "resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.", "resourceCreate": "Crea Risorsa", "resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa", @@ -187,7 +192,7 @@ "selectCountry": "Seleziona paese", "searchCountries": "Cerca paesi...", "noCountryFound": "Nessun paese trovato.", - "siteSelectionDescription": "Questo sito fornirà connettività all'obiettivo.", + "siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.", "resourceType": "Tipo Di Risorsa", "resourceTypeDescription": "Determinare come accedere alla risorsa", "resourceHTTPSSettings": "Impostazioni HTTPS", @@ -201,13 +206,13 @@ "protocol": "Protocollo", "protocolSelect": "Seleziona un protocollo", "resourcePortNumber": "Numero Porta", - "resourcePortNumberDescription": "Il numero di porta esterna per le richieste di proxy.", + "resourcePortNumberDescription": "Il numero di porta esterna per le richieste proxy.", "back": "Indietro", "cancel": "Annulla", "resourceConfig": "Snippet Di Configurazione", "resourceConfigDescription": "Copia e incolla questi snippet di configurazione per configurare la risorsa TCP/UDP", - "resourceAddEntrypoints": "Traefik: Aggiungi Ingresso", - "resourceExposePorts": "Gerbil: espone le porte in Docker componi", + "resourceAddEntrypoints": "Traefik: Aggiungi Entrypoint", + "resourceExposePorts": "Gerbil: espone le porte in Docker Compose", "resourceLearnRaw": "Scopri come configurare le risorse TCP/UDP", "resourceBack": "Torna alle risorse", "resourceGoTo": "Vai alla Risorsa", @@ -223,7 +228,7 @@ "rules": "Regole", "resourceSettingDescription": "Configura le impostazioni sulla risorsa", "resourceSetting": "Impostazioni {resourceName}", - "alwaysAllow": "Autenticazione Bypass", + "alwaysAllow": "Bypass Autenticazione", "alwaysDeny": "Blocca Accesso", "passToAuth": "Passa all'autenticazione", "orgSettingsDescription": "Configura le impostazioni dell'organizzazione", @@ -232,11 +237,11 @@ "saveGeneralSettings": "Salva Impostazioni Generali", "saveSettings": "Salva Impostazioni", "orgDangerZone": "Zona Pericolosa", - "orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.", + "orgDangerZoneDescription": "Una volta che si elimina questa org non sarà possibile tornare indietro, assicurarsi quindi di essere certi della decisione.", "orgDelete": "Elimina Organizzazione", "orgDeleteConfirm": "Conferma Elimina Organizzazione", "orgMessageRemove": "Questa azione è irreversibile e cancellerà tutti i dati associati.", - "orgMessageConfirm": "Per confermare, digita il nome dell'organizzazione qui sotto.", + "orgMessageConfirm": "Per confermare digita il nome dell'organizzazione qui sotto.", "orgQuestionRemove": "Sei sicuro di voler rimuovere l'organizzazione?", "orgUpdated": "Organizzazione aggiornata", "orgUpdatedDescription": "L'organizzazione è stata aggiornata.", @@ -249,10 +254,10 @@ "orgDeleted": "Organizzazione eliminata", "orgDeletedMessage": "L'organizzazione e i suoi dati sono stati eliminati.", "deleteAccount": "Elimina Account", - "deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.", + "deleteAccountDescription": "Elimina definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.", "deleteAccountButton": "Elimina Account", "deleteAccountConfirmTitle": "Elimina Account", - "deleteAccountConfirmMessage": "Questo cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questo non può essere annullato.", + "deleteAccountConfirmMessage": "Questa operazione cancellerà definitivamente il tuo account, tutte le organizzazioni che possiedi e tutti i dati all'interno di tali organizzazioni. Questa operazione non può essere annullata.", "deleteAccountConfirmString": "elimina account", "deleteAccountSuccess": "Account Eliminato", "deleteAccountSuccessMessage": "Il tuo account è stato eliminato.", @@ -267,7 +272,7 @@ "accessUserCreate": "Crea Utente", "accessUserRemove": "Rimuovi Utente", "username": "Nome utente", - "identityProvider": "Provider Di Identità", + "identityProvider": "Provider Identità", "role": "Ruolo", "nameRequired": "Il nome è obbligatorio", "accessRolesManage": "Gestisci Ruoli", @@ -323,6 +328,54 @@ "apiKeysDelete": "Elimina Chiave API", "apiKeysManage": "Gestisci Chiavi API", "apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione", + "provisioningKeysTitle": "Chiave di provisioning", + "provisioningKeysManage": "Gestisci Chiavi di provisioning", + "provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.", + "provisioningManage": "Accantonamento", + "provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.", + "pendingSites": "Siti In Attesa", + "siteApproveSuccess": "Sito approvato con successo", + "siteApproveError": "Errore nell'approvazione del sito", + "provisioningKeys": "Chiavi Di Provvedimento", + "searchProvisioningKeys": "Cerca le chiavi di provisioning...", + "provisioningKeysAdd": "Genera Chiave di provisioning", + "provisioningKeysErrorDelete": "Errore nell'eliminazione della chiave di provisioning", + "provisioningKeysErrorDeleteMessage": "Errore nell'eliminazione della chiave di provisioning", + "provisioningKeysQuestionRemove": "Sei sicuro di voler rimuovere questa chiave di provisioning dall'organizzazione?", + "provisioningKeysMessageRemove": "Una volta rimossa, la chiave non può più essere utilizzata per il provisioning.", + "provisioningKeysDeleteConfirm": "Conferma Eliminazione della chiave di provisioning", + "provisioningKeysDelete": "Elimina chiave di provisioning", + "provisioningKeysCreate": "Genera Chiave di provisioning", + "provisioningKeysCreateDescription": "Genera una nuova chiave di provisioning per l'organizzazione", + "provisioningKeysSeeAll": "Vedi tutte le chiavi di provisioning", + "provisioningKeysSave": "Salva la chiave di provisioning", + "provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.", + "provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning", + "provisioningKeysList": "Nuova chiave di provisioning", + "provisioningKeysMaxBatchSize": "Dimensione massima batch", + "provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)", + "provisioningKeysMaxBatchUnlimited": "Illimitato", + "provisioningKeysMaxBatchSizeInvalid": "Inserisci una dimensione massima valida del batch (1–1.000.000).", + "provisioningKeysValidUntil": "Valido fino al", + "provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.", + "provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.", + "provisioningKeysNumUsed": "Volte usate", + "provisioningKeysLastUsed": "Ultimo utilizzo", + "provisioningKeysNoExpiry": "Nessuna scadenza", + "provisioningKeysNeverUsed": "Mai", + "provisioningKeysEdit": "Modifica Chiave di provisioning", + "provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.", + "provisioningKeysApproveNewSites": "Approva nuovi siti", + "provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.", + "provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning", + "provisioningKeysUpdated": "Chiave di provisioning aggiornata", + "provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.", + "provisioningKeysBannerTitle": "Chiavi di provisioning del Sito", + "provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.", + "provisioningKeysBannerButtonText": "Scopri di più", + "pendingSitesBannerTitle": "Siti In Attesa", + "pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning vengono visualizzati qui per la revisione.", + "pendingSitesBannerButtonText": "Scopri di più", "apiKeysSettings": "Impostazioni {apiKeyName}", "userTitle": "Gestisci Tutti Gli Utenti", "userDescription": "Visualizza e gestisci tutti gli utenti del sistema", @@ -333,7 +386,7 @@ "userErrorDelete": "Errore nell'eliminare l'utente", "userDeleteConfirm": "Conferma Eliminazione Utente", "userDeleteServer": "Elimina utente dal server", - "userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni ed essere completamente rimosso dal server.", + "userMessageRemove": "L'utente verrà rimosso da tutte le organizzazioni e verrà completamente rimosso dal server.", "userQuestionRemove": "Sei sicuro di voler eliminare definitivamente l'utente dal server?", "licenseKey": "Chiave Di Licenza", "valid": "Valido", @@ -351,9 +404,13 @@ "licenseKeyDeletedDescription": "La chiave di licenza è stata eliminata.", "licenseErrorKeyActivate": "Attivazione della chiave di licenza non riuscita", "licenseErrorKeyActivateDescription": "Si è verificato un errore nell'attivazione della chiave di licenza.", - "licenseAbout": "Informazioni Su Licenze", + "licenseAbout": "Informazioni sul Licensing", + "licenseBannerTitle": "Attiva la tua Licenza Enterprise", + "licenseBannerDescription": "Sblocca le funzionalità enterprise per la tua istanza Pangolin auto-ospitata. Acquista una chiave di licenza per attivare le capacità premium e poi aggiungila qui sotto.", + "licenseBannerGetLicense": "Ottieni una Licenza", + "licenseBannerViewDocs": "Visualizza Documentazione", "communityEdition": "Edizione Community", - "licenseAboutDescription": "Questo è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.", + "licenseAboutDescription": "Questa sezione è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.", "licenseKeyActivated": "Chiave di licenza attivata", "licenseKeyActivatedDescription": "La chiave di licenza è stata attivata correttamente.", "licenseErrorKeyRecheck": "Impossibile ricontrollare le chiavi di licenza", @@ -376,7 +433,7 @@ "licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.", "licensedNot": "Non Licenziato", "hostId": "ID Host", - "licenseReckeckAll": "Ricontrolla Tutte Le Tasti", + "licenseReckeckAll": "Ricontrolla Tutte le chiavi", "licenseSiteUsage": "Utilizzo Siti", "licenseSiteUsageDecsription": "Visualizza il numero di siti che utilizzano questa licenza.", "licenseNoSiteLimit": "Non c'è alcun limite al numero di siti che utilizzano un host senza licenza.", @@ -427,7 +484,7 @@ "userOrgRemoved": "Utente rimosso", "userOrgRemovedDescription": "L'utente {email} è stato rimosso dall'organizzazione.", "userQuestionOrgRemove": "Sei sicuro di voler rimuovere questo utente dall'organizzazione?", - "userMessageOrgRemove": "Una volta rimosso, questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.", + "userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.", "userRemoveOrgConfirm": "Conferma Rimozione Utente", "userRemoveOrg": "Rimuovi Utente dall'Organizzazione", "users": "Utenti", @@ -479,13 +536,13 @@ "approve": "Approva", "approved": "Approvato", "denied": "Negato", - "deniedApproval": "Omologazione Negata", + "deniedApproval": "Approvazione Negata", "all": "Tutti", "deny": "Nega", "viewDetails": "Visualizza Dettagli", "requestingNewDeviceApproval": "ha richiesto un nuovo dispositivo", "resetFilters": "Ripristina Filtri", - "totalBlocked": "Richieste Bloccate Da Pangolino", + "totalBlocked": "Richieste Bloccate Da Pangolin", "totalRequests": "Totale Richieste", "requestsByCountry": "Richieste Per Paese", "requestsByDay": "Richieste Per Giorno", @@ -493,7 +550,7 @@ "allowed": "Consentito", "topCountries": "Paesi Principali", "accessRoleSelect": "Seleziona ruolo", - "inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.", + "inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. L'utente deve accedere al link per accettare l'invito.", "inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.", "inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.", "idpTitle": "Informazioni Generali", @@ -509,9 +566,12 @@ "userSaved": "Utente salvato", "userSavedDescription": "L'utente è stato aggiornato.", "autoProvisioned": "Auto Provisioned", + "autoProvisionSettings": "Impostazioni Automatiche di provisioning", "autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità", "accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione", "accessControlsSubmit": "Salva Controlli di Accesso", + "singleRolePerUserPlanNotice": "Il tuo piano supporta solo un ruolo per utente.", + "singleRolePerUserEditionNotice": "Questa edizione supporta solo un ruolo per utente.", "roles": "Ruoli", "accessUsersRoles": "Gestisci Utenti e Ruoli", "accessUsersRolesDescription": "Invita gli utenti e aggiungili ai ruoli per gestire l'accesso all'organizzazione", @@ -520,9 +580,9 @@ "proxyErrorInvalidHeader": "Valore dell'intestazione Host personalizzata non valido. Usa il formato nome dominio o salva vuoto per rimuovere l'intestazione Host personalizzata.", "proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.", "proxyEnableSSL": "Abilita SSL", - "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure agli obiettivi.", + "proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.", "target": "Target", - "configureTarget": "Configura Obiettivi", + "configureTarget": "Configura Risorse Interne", "targetErrorFetch": "Impossibile recuperare i target", "targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target", "siteErrorFetch": "Impossibile recuperare la risorsa", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Inserisci un numero di porta valido", "targetErrorNoSite": "Nessun sito selezionato", "targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo", + "targetTargetsCleared": "Obiettivi cancellati", + "targetTargetsClearedDescription": "Tutti gli obiettivi sono stati rimossi da questa risorsa", "targetCreated": "Destinazione creata", "targetCreatedDescription": "L'obiettivo è stato creato con successo", "targetErrorCreate": "Impossibile creare l'obiettivo", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Inserisci il token di configurazione dalla console del server.", "setupTokenRequired": "Il token di configurazione è richiesto", "actionUpdateSite": "Aggiorna Sito", + "actionResetSiteBandwidth": "Reimposta Larghezza Banda Dell'Organizzazione", "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", "actionCreateResource": "Crea Risorsa", "actionDeleteResource": "Elimina Risorsa", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Rimuovi Utente", "actionListUsers": "Elenca Utenti", "actionAddUserRole": "Aggiungi Ruolo Utente", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Imposta Ruoli Utente", "actionGenerateAccessToken": "Genera Token di Accesso", "actionDeleteAccessToken": "Elimina Token di Accesso", "actionListAccessTokens": "Elenca Token di Accesso", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Ruoli", "sidebarShareableLinks": "Collegamenti", "sidebarApiKeys": "Chiavi API", + "sidebarProvisioning": "Accantonamento", "sidebarSettings": "Impostazioni", "sidebarAllUsers": "Tutti Gli Utenti", "sidebarIdentityProviders": "Fornitori Di Identità", @@ -1890,6 +1954,40 @@ "exitNode": "Nodo di Uscita", "country": "Paese", "rulesMatchCountry": "Attualmente basato sull'IP di origine", + "region": "Regione", + "selectRegion": "Seleziona regione", + "searchRegions": "Cerca regioni...", + "noRegionFound": "Nessuna regione trovata.", + "rulesMatchRegion": "Seleziona un raggruppamento regionale di paesi", + "rulesErrorInvalidRegion": "Regione non valida", + "rulesErrorInvalidRegionDescription": "Seleziona una regione valida.", + "regionAfrica": "Africa", + "regionNorthernAfrica": "Africa Settentrionale", + "regionEasternAfrica": "Africa Orientale", + "regionMiddleAfrica": "Africa Centrale", + "regionSouthernAfrica": "Africa Meridionale", + "regionWesternAfrica": "Africa Occidentale", + "regionAmericas": "Americhe", + "regionCaribbean": "Caraibi", + "regionCentralAmerica": "America Centrale", + "regionSouthAmerica": "America Del Sud", + "regionNorthernAmerica": "America Del Nord", + "regionAsia": "Asia", + "regionCentralAsia": "Asia Centrale", + "regionEasternAsia": "Asia Orientale", + "regionSouthEasternAsia": "Asia Sudorientale", + "regionSouthernAsia": "Asia Meridionale", + "regionWesternAsia": "Asia Occidentale", + "regionEurope": "Europa", + "regionEasternEurope": "Europa Orientale", + "regionNorthernEurope": "Europa Settentrionale", + "regionSouthernEurope": "Europa Meridionale", + "regionWesternEurope": "Europa Occidentale", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australia e Nuova Zelanda", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Gestito Auto-Ospitato", "description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra", @@ -1938,6 +2036,25 @@ "invalidValue": "Valore non valido", "idpTypeLabel": "Tipo Provider Identità", "roleMappingExpressionPlaceholder": "es. contiene(gruppi, 'admin') && 'Admin' 'Membro'", + "roleMappingModeFixedRoles": "Ruoli Fissi", + "roleMappingModeMappingBuilder": "Mapping Builder", + "roleMappingModeRawExpression": "Espressione Raw", + "roleMappingFixedRolesPlaceholderSelect": "Seleziona uno o più ruoli", + "roleMappingFixedRolesPlaceholderFreeform": "Digita nomi dei ruoli (corrispondenza esatta per organizzazione)", + "roleMappingFixedRolesDescriptionSameForAll": "Assegna lo stesso ruolo impostato a ogni utente auto-provisioned.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Per i criteri predefiniti, digita i nomi dei ruoli che esistono in ogni organizzazione in cui gli utenti sono forniti. I nomi devono corrispondere esattamente.", + "roleMappingClaimPath": "Richiedi Percorso", + "roleMappingClaimPathPlaceholder": "gruppi", + "roleMappingClaimPathDescription": "Percorso nel payload del token che contiene valori sorgente (ad esempio, gruppi).", + "roleMappingMatchValue": "Valore Della Partita", + "roleMappingAssignRoles": "Assegna Ruoli", + "roleMappingAddMappingRule": "Aggiungi Regola Mappatura", + "roleMappingRawExpressionResultDescription": "Espressione deve essere valutata in una stringa o array di stringhe.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Espressione deve valutare in una stringa (un singolo nome ruolo).", + "roleMappingMatchValuePlaceholder": "Valore della corrispondenza (per esempio: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Digita i nomi dei ruoli (esatto per org)", + "roleMappingBuilderFreeformRowHint": "I nomi dei ruoli devono corrispondere a un ruolo in ogni organizzazione di destinazione.", + "roleMappingRemoveRule": "Rimuovi", "idpGoogleConfiguration": "Configurazione Google", "idpGoogleConfigurationDescription": "Configura le credenziali di Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione", "domainPickerProvidedDomain": "Dominio Fornito", "domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito", + "domainPickerFreeDomainsPaidFeature": "I domini forniti sono una funzionalità a pagamento. Abbonati per ricevere un dominio incluso con il tuo piano — non è necessario portare il proprio.", "domainPickerVerified": "Verificato", "domainPickerUnverified": "Non Verificato", + "domainPickerManual": "Manuale", "domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.", "domainPickerError": "Errore", "domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione", @@ -2235,7 +2354,7 @@ "description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario." } }, - "personalUseOnly": "Solo uso personale (licenza gratuita — nessun checkout)", + "personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)", "buttons": { "continueToCheckout": "Continua al Checkout" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso", "logRetentionActionLabel": "Ritenzione Registro Azioni", "logRetentionActionDescription": "Per quanto tempo conservare i log delle azioni", + "logRetentionConnectionLabel": "Ritenzione Registro Di Connessione", + "logRetentionConnectionDescription": "Per quanto tempo conservare i log di connessione", "logRetentionDisabled": "Disabilitato", "logRetention3Days": "3 giorni", "logRetention7Days": "7 giorni", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Fine dell'anno successivo", "actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione", "accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione", + "connectionLogs": "Log Di Connessione", + "connectionLogsDescription": "Visualizza i log di connessione per i tunnel in questa organizzazione", + "sidebarLogsConnection": "Log Di Connessione", + "sidebarLogsStreaming": "Streaming", + "sourceAddress": "Indirizzo Di Origine", + "destinationAddress": "Indirizzo Di Destinazione", + "duration": "Durata", "licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza Enterprise Edition o Pangolin Cloud . Prenota una demo o una prova POC.", "ossEnterpriseEditionRequired": "L' Enterprise Edition è necessaria per utilizzare questa funzione. Questa funzione è disponibile anche in Pangolin Cloud. Prenota una demo o una prova POC.", "certResolver": "Risolutore Di Certificato", @@ -2487,6 +2615,9 @@ "machineClients": "Machine Clients", "install": "Installa", "run": "Esegui", + "envFile": "File di ambiente", + "serviceFile": "File di servizio", + "enableAndStart": "Abilita e avvia", "clientNameDescription": "Il nome visualizzato del client che può essere modificato in seguito.", "clientAddress": "Indirizzo Client (Avanzato)", "setupFailedToFetchSubnet": "Recupero della sottorete predefinita non riuscito", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Modifica un ruolo e abilita l'opzione 'Richiedi l'approvazione del dispositivo'. Gli utenti con questo ruolo avranno bisogno dell'approvazione dell'amministratore per i nuovi dispositivi.", "approvalsEmptyStatePreviewDescription": "Anteprima: quando abilitato, le richieste di dispositivo in attesa appariranno qui per la revisione", "approvalsEmptyStateButtonText": "Gestisci Ruoli", - "domainErrorTitle": "Stiamo avendo problemi a verificare il tuo dominio" + "domainErrorTitle": "Stiamo avendo problemi a verificare il tuo dominio", + "idpAdminAutoProvisionPoliciesTabHint": "Configura la mappatura dei ruoli e le politiche di organizzazione nella scheda Auto Provision Settings.", + "streamingTitle": "Streaming Eventi", + "streamingDescription": "Trasmetti eventi dalla tua organizzazione a destinazioni esterne in tempo reale.", + "streamingUnnamedDestination": "Destinazione senza nome", + "streamingNoUrlConfigured": "Nessun URL configurato", + "streamingAddDestination": "Aggiungi Destinazione", + "streamingHttpWebhookTitle": "Webhook HTTP", + "streamingHttpWebhookDescription": "Invia eventi a qualsiasi endpoint HTTP con autenticazione e template flessibili.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Trasmetti eventi su un contenitore di archiviazione per oggetti compatibile con S3. Presto in arrivo.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.", + "streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.", + "streamingFailedToLoad": "Impossibile caricare le destinazioni", + "streamingUnexpectedError": "Si è verificato un errore imprevisto.", + "streamingFailedToUpdate": "Impossibile aggiornare la destinazione", + "streamingDeletedSuccess": "Destinazione eliminata con successo", + "streamingFailedToDelete": "Impossibile eliminare la destinazione", + "streamingDeleteTitle": "Elimina Destinazione", + "streamingDeleteButtonText": "Elimina Destinazione", + "streamingDeleteDialogAreYouSure": "Sei sicuro di voler eliminare", + "streamingDeleteDialogThisDestination": "questa destinazione", + "streamingDeleteDialogPermanentlyRemoved": "? Tutta la configurazione verrà definitivamente rimossa.", + "httpDestEditTitle": "Modifica Destinazione", + "httpDestAddTitle": "Aggiungi Destinazione HTTP", + "httpDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming di eventi HTTP.", + "httpDestAddDescription": "Configura un nuovo endpoint HTTP per ricevere gli eventi della tua organizzazione.", + "httpDestTabSettings": "Impostazioni", + "httpDestTabHeaders": "Intestazioni", + "httpDestTabBody": "Corpo", + "httpDestTabLogs": "Registri", + "httpDestNamePlaceholder": "La mia destinazione HTTP", + "httpDestUrlLabel": "Url Di Destinazione", + "httpDestUrlErrorHttpRequired": "L'URL deve usare http o https", + "httpDestUrlErrorHttpsRequired": "HTTPS è richiesto sulle distribuzioni cloud", + "httpDestUrlErrorInvalid": "Inserisci un URL valido (es. https://example.com/webhook)", + "httpDestAuthTitle": "Autenticazione", + "httpDestAuthDescription": "Scegli come vengono autenticate le richieste al tuo endpoint.", + "httpDestAuthNoneTitle": "Nessuna Autenticazione", + "httpDestAuthNoneDescription": "Invia richieste senza intestazione autorizzazione.", + "httpDestAuthBearerTitle": "Token Del Portatore", + "httpDestAuthBearerDescription": "Aggiunge un'intestazione Authorization: Bearer '' a ogni richiesta.", + "httpDestAuthBearerPlaceholder": "La tua chiave API o token", + "httpDestAuthBasicTitle": "Autenticazione Base", + "httpDestAuthBasicDescription": "Aggiunge un'intestazione Authorization: Basic ''. Fornire le credenziali come username:password.", + "httpDestAuthBasicPlaceholder": "username:password", + "httpDestAuthCustomTitle": "Intestazione Personalizzata", + "httpDestAuthCustomDescription": "Specifica un nome e un valore di intestazione HTTP personalizzati per l'autenticazione (ad esempio X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Nome intestazione (es. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Valore intestazione", + "httpDestCustomHeadersTitle": "Intestazioni Http Personalizzate", + "httpDestCustomHeadersDescription": "Aggiungi intestazioni personalizzate ad ogni richiesta in uscita. Utile per token statici o un tipo di contenuto personalizzato. Come impostazione predefinita, viene inviato il tipo di contenuto/json.", + "httpDestNoHeadersConfigured": "Nessuna intestazione personalizzata configurata. Fare clic su \"Aggiungi intestazione\" per aggiungerne una.", + "httpDestHeaderNamePlaceholder": "Nome intestazione", + "httpDestHeaderValuePlaceholder": "Valore", + "httpDestAddHeader": "Aggiungi Intestazione", + "httpDestBodyTemplateTitle": "Modello Corpo Personalizzato", + "httpDestBodyTemplateDescription": "Controlla la struttura JSON payload inviata al tuo endpoint. Se disabilitata, viene inviato un oggetto JSON predefinito per ogni evento.", + "httpDestEnableBodyTemplate": "Abilita modello corpo personalizzato", + "httpDestBodyTemplateLabel": "Modello Corpo (JSON)", + "httpDestBodyTemplateHint": "Usa le variabili del modello per fare riferimento ai campi dell'evento nel tuo payload.", + "httpDestPayloadFormatTitle": "Formato Payload", + "httpDestPayloadFormatDescription": "Come gli eventi sono serializzati in ogni organismo di richiesta.", + "httpDestFormatJsonArrayTitle": "JSON Array", + "httpDestFormatJsonArrayDescription": "Una richiesta per lotto, corpo è un array JSON. Compatibile con la maggior parte dei webhooks generici e Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Una richiesta per lotto, corpo è newline-delimited JSON — un oggetto per linea, nessun array esterno. Richiesto da Splunk HEC, Elastic / OpenSearch, e Grafana Loki.", + "httpDestFormatSingleTitle": "Un Evento Per Richiesta", + "httpDestFormatSingleDescription": "Invia un HTTP POST separato per ogni singolo evento. Usa solo per gli endpoint che non possono gestire i batch.", + "httpDestLogTypesTitle": "Tipi Di Log", + "httpDestLogTypesDescription": "Scegli quali tipi di log vengono inoltrati a questa destinazione. Verranno trasmessi solo i tipi di log abilitati.", + "httpDestAccessLogsTitle": "Log Accesso", + "httpDestAccessLogsDescription": "Tentativi di accesso alle risorse, comprese le richieste autenticate e negate.", + "httpDestActionLogsTitle": "Log Azioni", + "httpDestActionLogsDescription": "Azioni amministrative eseguite dagli utenti all'interno dell'organizzazione.", + "httpDestConnectionLogsTitle": "Log Di Connessione", + "httpDestConnectionLogsDescription": "Eventi di connessione al sito e al tunnel, inclusi collegamenti e disconnessioni.", + "httpDestRequestLogsTitle": "Log Richiesta", + "httpDestRequestLogsDescription": "Registri di richiesta HTTP per le risorse proxy, inclusi metodo, percorso e codice di risposta.", + "httpDestSaveChanges": "Salva Modifiche", + "httpDestCreateDestination": "Crea Destinazione", + "httpDestUpdatedSuccess": "Destinazione aggiornata con successo", + "httpDestCreatedSuccess": "Destinazione creata con successo", + "httpDestUpdateFailed": "Impossibile aggiornare la destinazione", + "httpDestCreateFailed": "Impossibile creare la destinazione" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 02915abd7..ccf1f2ca8 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -148,6 +148,11 @@ "createLink": "링크 생성", "resourcesNotFound": "리소스가 발견되지 않았습니다.", "resourceSearch": "리소스 검색", + "machineSearch": "기계 검색", + "machinesSearch": "기계 클라이언트 검색...", + "machineNotFound": "기계를 찾을 수 없습니다", + "userDeviceSearch": "사용자 장치 검색", + "userDevicesSearch": "사용자 장치 검색...", "openMenu": "메뉴 열기", "resource": "리소스", "title": "제목", @@ -323,6 +328,54 @@ "apiKeysDelete": "API 키 삭제", "apiKeysManage": "API 키 관리", "apiKeysDescription": "API 키는 통합 API와 인증하는 데 사용됩니다.", + "provisioningKeysTitle": "프로비저닝 키", + "provisioningKeysManage": "프로비저닝 키 관리", + "provisioningKeysDescription": "프로비저닝 키는 조직의 자동 사이트 프로비저닝 인증에 사용됩니다.", + "provisioningManage": "프로비저닝", + "provisioningDescription": "프로비저닝 키를 관리하고 승인을 기다리는 사이트를 검토합니다.", + "pendingSites": "대기중인 사이트", + "siteApproveSuccess": "사이트가 성공적으로 승인되었습니다", + "siteApproveError": "사이트 승인 오류", + "provisioningKeys": "프로비저닝 키", + "searchProvisioningKeys": "프로비저닝 키 검색...", + "provisioningKeysAdd": "프로비저닝 키 생성", + "provisioningKeysErrorDelete": "프로비저닝 키 삭제 오류", + "provisioningKeysErrorDeleteMessage": "프로비저닝 키 삭제 오류", + "provisioningKeysQuestionRemove": "이 프로비저닝 키를 조직에서 제거하시겠습니까?", + "provisioningKeysMessageRemove": "제거 후에는 이 키를 사이트 프로비저닝에 사용할 수 없습니다.", + "provisioningKeysDeleteConfirm": "프로비저닝 키 삭제 확인", + "provisioningKeysDelete": "프로비저닝 키 삭제", + "provisioningKeysCreate": "프로비저닝 키 생성", + "provisioningKeysCreateDescription": "조직을 위한 새로운 프로비저닝 키 생성", + "provisioningKeysSeeAll": "모든 프로비저닝 키 보기", + "provisioningKeysSave": "프로비저닝 키 저장", + "provisioningKeysSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", + "provisioningKeysErrorCreate": "프로비저닝 키 생성 오류", + "provisioningKeysList": "새 프로비저닝 키", + "provisioningKeysMaxBatchSize": "최대 배치 크기", + "provisioningKeysUnlimitedBatchSize": "무제한 배치 크기 (제한 없음)", + "provisioningKeysMaxBatchUnlimited": "무제한", + "provisioningKeysMaxBatchSizeInvalid": "유효한 최대 배치 크기를 입력하세요 (1–1,000,000).", + "provisioningKeysValidUntil": "유효 기간", + "provisioningKeysValidUntilHint": "만료 날짜를 설정하지 않을 경우 빈칸으로 남겨 두세요.", + "provisioningKeysValidUntilInvalid": "유효한 날짜와 시간을 입력하세요.", + "provisioningKeysNumUsed": "사용 횟수", + "provisioningKeysLastUsed": "마지막 사용", + "provisioningKeysNoExpiry": "만료 없음", + "provisioningKeysNeverUsed": "절대", + "provisioningKeysEdit": "프로비저닝 키 수정", + "provisioningKeysEditDescription": "이 키의 최대 배치 크기 및 만료 시간을 업데이트하세요.", + "provisioningKeysApproveNewSites": "새로운 사이트 승인", + "provisioningKeysApproveNewSitesDescription": "이 키를 등록하는 사이트를 자동으로 승인합니다.", + "provisioningKeysUpdateError": "프로비저닝 키 업데이트 오류", + "provisioningKeysUpdated": "프로비저닝 키가 업데이트되었습니다", + "provisioningKeysUpdatedDescription": "변경 사항이 저장되었습니다.", + "provisioningKeysBannerTitle": "사이트 프로비저닝 키", + "provisioningKeysBannerDescription": "프로비저닝 키를 생성하고 Newt 커넥터와 함께 사용하여 첫 시작 시 사이트를 자동 생성 - 각 사이트에 대한 별도 자격 증명이 필요 없습니다.", + "provisioningKeysBannerButtonText": "자세히 알아보기", + "pendingSitesBannerTitle": "대기중인 사이트", + "pendingSitesBannerDescription": "프로비저닝 키를 사용하여 연결된 사이트가 검토를 위해 여기에 표시됩니다.", + "pendingSitesBannerButtonText": "자세히 알아보기", "apiKeysSettings": "{apiKeyName} 설정", "userTitle": "모든 사용자 관리", "userDescription": "시스템의 모든 사용자를 보고 관리합니다", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "라이센스 키 활성화에 실패했습니다.", "licenseErrorKeyActivateDescription": "라이센스 키를 활성화하는 동안 오류가 발생했습니다", "licenseAbout": "라이센스에 대한 정보", + "licenseBannerTitle": "기업 라이선스 활성화", + "licenseBannerDescription": "자체 호스팅된 Pangolin 인스턴스에서 기업 기능을 잠금 해제하십시오. 라이선스 키를 구입하여 프리미엄 기능을 활성화하고 아래에 추가하십시오.", + "licenseBannerGetLicense": "라이선스 획득", + "licenseBannerViewDocs": "문서 보기", "communityEdition": "커뮤니티 에디션", "licenseAboutDescription": "이것은 상업적 환경에서 Pangolin을 사용하는 비즈니스 및 기업 사용자용입니다. 개인 용도로 Pangolin을 사용하는 경우 이 섹션을 무시할 수 있습니다.", "licenseKeyActivated": "라이센스 키가 활성화되었습니다", @@ -509,9 +566,12 @@ "userSaved": "사용자 저장됨", "userSavedDescription": "사용자가 업데이트되었습니다.", "autoProvisioned": "자동 프로비저닝됨", + "autoProvisionSettings": "자동 프로비저닝 설정", "autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다", "accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요", "accessControlsSubmit": "접근 제어 저장", + "singleRolePerUserPlanNotice": "계획에는 사용자당 한 가지 역할만 지원됩니다.", + "singleRolePerUserEditionNotice": "이 판에는 사용자당 한 가지 역할만 지원됩니다.", "roles": "역할", "accessUsersRoles": "사용자 및 역할 관리", "accessUsersRolesDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.", "targetErrorNoSite": "선택된 사이트 없음", "targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.", + "targetTargetsCleared": "대상이 제거됨", + "targetTargetsClearedDescription": "이 리소스에서 모든 대상이 제거되었습니다", "targetCreated": "대상 생성", "targetCreatedDescription": "대상이 성공적으로 생성되었습니다.", "targetErrorCreate": "대상 생성 실패", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.", "setupTokenRequired": "설정 토큰이 필요합니다", "actionUpdateSite": "사이트 업데이트", + "actionResetSiteBandwidth": "조직 대역폭 재설정", "actionListSiteRoles": "허용된 사이트 역할 목록", "actionCreateResource": "리소스 생성", "actionDeleteResource": "리소스 삭제", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "사용자 제거", "actionListUsers": "사용자 목록", "actionAddUserRole": "사용자 역할 추가", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "사용자 역할 설정", "actionGenerateAccessToken": "액세스 토큰 생성", "actionDeleteAccessToken": "액세스 토큰 삭제", "actionListAccessTokens": "액세스 토큰 목록", @@ -1265,6 +1328,7 @@ "sidebarRoles": "역할", "sidebarShareableLinks": "링크", "sidebarApiKeys": "API 키", + "sidebarProvisioning": "프로비저닝", "sidebarSettings": "설정", "sidebarAllUsers": "모든 사용자", "sidebarIdentityProviders": "신원 공급자", @@ -1890,6 +1954,40 @@ "exitNode": "종단 노드", "country": "국가", "rulesMatchCountry": "현재 소스 IP를 기반으로 합니다", + "region": "지역", + "selectRegion": "지역 선택", + "searchRegions": "지역 검색...", + "noRegionFound": "지역을 찾을 수 없습니다.", + "rulesMatchRegion": "국가의 지역 구성을 선택합니다", + "rulesErrorInvalidRegion": "잘못된 지역", + "rulesErrorInvalidRegionDescription": "유효한 지역을 선택하세요.", + "regionAfrica": "아프리카", + "regionNorthernAfrica": "북부 아프리카", + "regionEasternAfrica": "동부 아프리카", + "regionMiddleAfrica": "중부 아프리카", + "regionSouthernAfrica": "남부 아프리카", + "regionWesternAfrica": "서부 아프리카", + "regionAmericas": "아메리카", + "regionCaribbean": "카리브", + "regionCentralAmerica": "중앙 아메리카", + "regionSouthAmerica": "남아메리카", + "regionNorthernAmerica": "북미", + "regionAsia": "아시아", + "regionCentralAsia": "중앙 아시아", + "regionEasternAsia": "동아시아", + "regionSouthEasternAsia": "동남아시아", + "regionSouthernAsia": "남아시아", + "regionWesternAsia": "서아시아", + "regionEurope": "유럽", + "regionEasternEurope": "동부 유럽", + "regionNorthernEurope": "북부 유럽", + "regionSouthernEurope": "남부 유럽", + "regionWesternEurope": "서부 유럽", + "regionOceania": "오세아니아", + "regionAustraliaAndNewZealand": "호주와 뉴질랜드", + "regionMelanesia": "멜라네시아", + "regionMicronesia": "미크로네시아", + "regionPolynesia": "폴리네시아", "managedSelfHosted": { "title": "관리 자체 호스팅", "description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함", @@ -1938,6 +2036,25 @@ "invalidValue": "잘못된 값", "idpTypeLabel": "신원 공급자 유형", "roleMappingExpressionPlaceholder": "예: contains(groups, 'admin') && 'Admin' || 'Member'", + "roleMappingModeFixedRoles": "고정 역할", + "roleMappingModeMappingBuilder": "매핑 빌더", + "roleMappingModeRawExpression": "원시 표현식", + "roleMappingFixedRolesPlaceholderSelect": "하나 이상의 역할을 선택하세요", + "roleMappingFixedRolesPlaceholderFreeform": "역할 이름 입력 (조직마다 정확히 일치)", + "roleMappingFixedRolesDescriptionSameForAll": "모든 자동 프로비전 사용자에게 동일한 역할 세트를 할당합니다.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "기본 정책의 경우 사용자가 프로비저닝된 조직의 역할 이름을 입력하세요. 이름은 정확히 일치해야 합니다.", + "roleMappingClaimPath": "클레임 경로", + "roleMappingClaimPathPlaceholder": "그룹", + "roleMappingClaimPathDescription": "토큰 페이로드에서 소스 값을 포함하는 경로 (예: 그룹).", + "roleMappingMatchValue": "매치 값", + "roleMappingAssignRoles": "역할 할당", + "roleMappingAddMappingRule": "매핑 규칙 추가", + "roleMappingRawExpressionResultDescription": "표현식은 문자열 또는 문자열 배열로 평가되어야 합니다.", + "roleMappingRawExpressionResultDescriptionSingleRole": "표현식은 문자열 (단일 역할 이름)로 평가되어야 합니다.", + "roleMappingMatchValuePlaceholder": "매치 값 (예: 관리자)", + "roleMappingAssignRolesPlaceholderFreeform": "역할 이름 입력 (조직마다 정확히)", + "roleMappingBuilderFreeformRowHint": "역할 이름은 각 대상 조직의 역할과 일치해야 합니다.", + "roleMappingRemoveRule": "제거", "idpGoogleConfiguration": "Google 구성", "idpGoogleConfigurationDescription": "Google OAuth2 자격 증명을 구성합니다.", "idpGoogleClientIdDescription": "Google OAuth2 클라이언트 ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.", "domainPickerProvidedDomain": "제공된 도메인", "domainPickerFreeProvidedDomain": "무료 제공된 도메인", + "domainPickerFreeDomainsPaidFeature": "제공된 도메인은 유료 기능입니다. 요금제에 도메인이 포함되도록 구독하세요. — 별도로 도메인을 준비할 필요 없습니다.", "domainPickerVerified": "검증됨", "domainPickerUnverified": "검증되지 않음", + "domainPickerManual": "수동", "domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.", "domainPickerError": "오류", "domainPickerErrorLoadDomains": "조직 도메인 로드 실패", @@ -2235,7 +2354,7 @@ "description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원." } }, - "personalUseOnly": "개인 사용 전용 (무료 라이센스 — 체크아웃 없음)", + "personalUseOnly": "개인용으로만 사용 (무료 라이선스 - 결제 없음)", "buttons": { "continueToCheckout": "결제로 진행" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지", "logRetentionActionLabel": "작업 로그 보관", "logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지", + "logRetentionConnectionLabel": "연결 로그 보유 기간", + "logRetentionConnectionDescription": "연결 로그를 얼마나 오래 보유할지", "logRetentionDisabled": "비활성화됨", "logRetention3Days": "3 일", "logRetention7Days": "7 일", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "다음 연도 말", "actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다", "accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다", + "connectionLogs": "연결 로그", + "connectionLogsDescription": "이 조직의 터널 연결 로그 보기", + "sidebarLogsConnection": "연결 로그", + "sidebarLogsStreaming": "스트리밍", + "sourceAddress": "소스 주소", + "destinationAddress": "대상 주소", + "duration": "지속 시간", "licenseRequiredToUse": "이 기능을 사용하려면 엔터프라이즈 에디션 라이선스가 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다. 데모 또는 POC 체험을 예약하세요.", "ossEnterpriseEditionRequired": "이 기능을 사용하려면 엔터프라이즈 에디션이(가) 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다. 데모 또는 POC 체험을 예약하세요.", "certResolver": "인증서 해결사", @@ -2487,6 +2615,9 @@ "machineClients": "기계 클라이언트", "install": "설치", "run": "실행", + "envFile": "환경 파일", + "serviceFile": "서비스 파일", + "enableAndStart": "활성화 및 시작", "clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.", "clientAddress": "클라이언트 주소(고급)", "setupFailedToFetchSubnet": "기본값 로드 실패", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "역할을 편집하고 '장치 승인 요구' 옵션을 활성화하세요. 이 역할을 가진 사용자는 새 장치에 대해 관리자의 승인이 필요합니다.", "approvalsEmptyStatePreviewDescription": "미리 보기: 활성화된 경우, 승인 대기 중인 장치 요청이 검토용으로 여기에 표시됩니다.", "approvalsEmptyStateButtonText": "역할 관리", - "domainErrorTitle": "도메인 확인에 문제가 발생했습니다." + "domainErrorTitle": "도메인 확인에 문제가 발생했습니다.", + "idpAdminAutoProvisionPoliciesTabHint": "자동 프로비저닝 설정 탭에서 역할 매핑 및 조직 정책을 구성합니다.", + "streamingTitle": "이벤트 스트리밍", + "streamingDescription": "조직의 이벤트를 외부 목적지로 실시간 전송합니다.", + "streamingUnnamedDestination": "이름이 없는 대상지", + "streamingNoUrlConfigured": "설정된 URL이 없습니다", + "streamingAddDestination": "대상지 추가", + "streamingHttpWebhookTitle": "HTTP 웹훅", + "streamingHttpWebhookDescription": "유연한 인증 및 템플릿 작성 기능을 갖춘 HTTP 엔드포인트에 이벤트를 전송합니다.", + "streamingS3Title": "아마존 S3", + "streamingS3Description": "S3 호환 객체 스토리지 버킷에 이벤트를 스트리밍합니다. 곧 제공됩니다.", + "streamingDatadogTitle": "데이터독", + "streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.", + "streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.", + "streamingFailedToLoad": "대상 로드에 실패했습니다", + "streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.", + "streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다", + "streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다", + "streamingFailedToDelete": "대상지 삭제 실패", + "streamingDeleteTitle": "대상지 삭제", + "streamingDeleteButtonText": "대상지 삭제", + "streamingDeleteDialogAreYouSure": "삭제하시겠습니까", + "streamingDeleteDialogThisDestination": "이 대상지", + "streamingDeleteDialogPermanentlyRemoved": "? 모든 구성은 영구적으로 제거됩니다.", + "httpDestEditTitle": "대상지 수정", + "httpDestAddTitle": "HTTP 대상지 추가", + "httpDestEditDescription": "이 HTTP 이벤트 스트리밍 대상지의 구성을 업데이트하세요.", + "httpDestAddDescription": "조직의 이벤트 수신을 위한 새로운 HTTP 엔드포인트를 구성하세요.", + "httpDestTabSettings": "설정", + "httpDestTabHeaders": "헤더", + "httpDestTabBody": "본문", + "httpDestTabLogs": "로그", + "httpDestNamePlaceholder": "내 HTTP 대상", + "httpDestUrlLabel": "대상 URL", + "httpDestUrlErrorHttpRequired": "URL은 http 또는 https를 사용해야 합니다", + "httpDestUrlErrorHttpsRequired": "클라우드 배포에는 HTTPS가 필요합니다", + "httpDestUrlErrorInvalid": "유효한 URL을 입력하세요 (예: https://example.com/webhook)", + "httpDestAuthTitle": "인증", + "httpDestAuthDescription": "엔드포인트에 대한 요청 인증 방법을 선택하세요.", + "httpDestAuthNoneTitle": "인증 없음", + "httpDestAuthNoneDescription": "Authorization 헤더 없이 요청을 보냅니다.", + "httpDestAuthBearerTitle": "Bearer 토큰", + "httpDestAuthBearerDescription": "각 요청에 Authorization: Bearer '' 헤더를 추가합니다.", + "httpDestAuthBearerPlaceholder": "API 키 또는 토큰", + "httpDestAuthBasicTitle": "기본 인증", + "httpDestAuthBasicDescription": "Authorization: Basic '' 헤더를 추가합니다. 자격 증명은 사용자 이름:비밀번호로 제공합니다.", + "httpDestAuthBasicPlaceholder": "사용자 이름:비밀번호", + "httpDestAuthCustomTitle": "사용자 정의 헤더", + "httpDestAuthCustomDescription": "인증을 위한 사용자 정의 HTTP 헤더 이름 및 값을 지정하세요 (예: X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "헤더 이름 (예: X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "헤더 값", + "httpDestCustomHeadersTitle": "사용자 정의 HTTP 헤더", + "httpDestCustomHeadersDescription": "모든 발신 요청에 사용자 정의 헤더를 추가합니다. 정적 토큰 또는 사용자 정의 Content-Type에 유용합니다. 기본적으로 Content-Type: application/json이 전송됩니다.", + "httpDestNoHeadersConfigured": "구성된 사용자 정의 헤더가 없습니다. \"헤더 추가\"를 클릭하여 추가하세요.", + "httpDestHeaderNamePlaceholder": "헤더 이름", + "httpDestHeaderValuePlaceholder": "값", + "httpDestAddHeader": "헤더 추가", + "httpDestBodyTemplateTitle": "사용자 정의 본문 템플릿", + "httpDestBodyTemplateDescription": "엔드포인트에 전송되는 JSON 페이로드 구조를 제어합니다. 비활성화된 경우 각 이벤트에 대해 기본 JSON 객체가 전송됩니다.", + "httpDestEnableBodyTemplate": "사용자 정의 본문 템플릿 활성화", + "httpDestBodyTemplateLabel": "본문 템플릿 (JSON)", + "httpDestBodyTemplateHint": "템플릿 변수를 사용하여 페이로드에서 이벤트 필드를 참조하세요.", + "httpDestPayloadFormatTitle": "페이로드 형식", + "httpDestPayloadFormatDescription": "각 요청 본문에 이벤트가 시리얼라이즈되는 방식입니다.", + "httpDestFormatJsonArrayTitle": "JSON 배열", + "httpDestFormatJsonArrayDescription": "각 배치마다 요청 하나씩, 본문은 JSON 배열입니다. 대부분의 일반 웹훅 및 Datadog과 호환됩니다.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "각 배치마다 요청 하나씩, 본문은 줄 구분 JSON — 한 라인에 하나의 객체가 있으며 외부 배열이 없습니다. Splunk HEC, Elastic / OpenSearch, Grafana Loki에 필요합니다.", + "httpDestFormatSingleTitle": "각 요청 당 하나의 이벤트", + "httpDestFormatSingleDescription": "각 개별 이벤트에 대해 별도의 HTTP POST를 전송합니다. 배치를 처리할 수 없는 엔드포인트에만 사용하세요.", + "httpDestLogTypesTitle": "로그 유형", + "httpDestLogTypesDescription": "이 대상지에 전달될 로그 유형을 선택하세요. 활성화된 로그 유형만 스트리밍 됩니다.", + "httpDestAccessLogsTitle": "접근 로그", + "httpDestAccessLogsDescription": "인증 및 거부된 요청을 포함한 리소스 접근 시도.", + "httpDestActionLogsTitle": "작업 로그", + "httpDestActionLogsDescription": "조직 내에서 사용자가 수행한 관리 작업.", + "httpDestConnectionLogsTitle": "연결 로그", + "httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.", + "httpDestRequestLogsTitle": "요청 로그", + "httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.", + "httpDestSaveChanges": "변경 사항 저장", + "httpDestCreateDestination": "대상지 생성", + "httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다", + "httpDestCreatedSuccess": "대상지가 성공적으로 생성되었습니다", + "httpDestUpdateFailed": "대상지를 업데이트하는 데 실패했습니다", + "httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다" } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 50ec9a717..8e864f5b7 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -148,6 +148,11 @@ "createLink": "Opprett lenke", "resourcesNotFound": "Ingen ressurser funnet", "resourceSearch": "Søk i ressurser", + "machineSearch": "Søk etter maskiner", + "machinesSearch": "Søk etter maskinklienter...", + "machineNotFound": "Ingen maskiner funnet", + "userDeviceSearch": "Søk etter brukerenheter", + "userDevicesSearch": "Søk etter brukerenheter...", "openMenu": "Åpne meny", "resource": "Ressurs", "title": "Tittel", @@ -323,6 +328,54 @@ "apiKeysDelete": "Slett API-nøkkel", "apiKeysManage": "Administrer API-nøkler", "apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API", + "provisioningKeysTitle": "Foreløpig nøkkel", + "provisioningKeysManage": "Behandle bestemmende nøkler", + "provisioningKeysDescription": "Bestemmelsesnøkler brukes til å godkjenne automatisert nettstedsløsning for din organisasjon.", + "provisioningManage": "Levering", + "provisioningDescription": "Administrer foreløpig nøkler og gjennomgå ventende nettsteder som venter på godkjenning.", + "pendingSites": "Ventende nettsteder", + "siteApproveSuccess": "Vellykket godkjenning av nettsted", + "siteApproveError": "Feil ved godkjenning av side", + "provisioningKeys": "Foreløpig nøkler", + "searchProvisioningKeys": "Søk varer i lagrings nøkler...", + "provisioningKeysAdd": "Generer fremvisende nøkkel", + "provisioningKeysErrorDelete": "Feil under sletting av foreløpig nøkkel", + "provisioningKeysErrorDeleteMessage": "Feil under sletting av foreløpig nøkkel", + "provisioningKeysQuestionRemove": "Er du sikker på at du vil fjerne denne midlertidig nøkkelen fra organisasjonen?", + "provisioningKeysMessageRemove": "Når nøkkelen er fjernet, kan den ikke lenger brukes til anleggsavsetning.", + "provisioningKeysDeleteConfirm": "Bekreft sletting av bestemmelsesnøkkel", + "provisioningKeysDelete": "Slett bestemmelsesnøkkel", + "provisioningKeysCreate": "Generer fremvisende nøkkel", + "provisioningKeysCreateDescription": "Generer en ny foreløpig nøkkel til organisasjonen", + "provisioningKeysSeeAll": "Se alle foreløpig nøkler", + "provisioningKeysSave": "Lagre den midlertidig nøkkelen", + "provisioningKeysSaveDescription": "Du kan bare se denne én gang. Kopier det til et sikkert sted.", + "provisioningKeysErrorCreate": "Feil under oppretting av foreløpig nøkkel", + "provisioningKeysList": "Ny provisorisk nøkkel", + "provisioningKeysMaxBatchSize": "Maks størrelse på bunt", + "provisioningKeysUnlimitedBatchSize": "Ubegrenset mengde bunt (ingen begrensning)", + "provisioningKeysMaxBatchUnlimited": "Ubegrenset", + "provisioningKeysMaxBatchSizeInvalid": "Angi en gyldig sjakkstørrelse (1–1 000.000).", + "provisioningKeysValidUntil": "Gyldig til", + "provisioningKeysValidUntilHint": "La stå tomt for ingen utløp.", + "provisioningKeysValidUntilInvalid": "Angi en gyldig dato og klokkeslett.", + "provisioningKeysNumUsed": "Antall ganger brukt", + "provisioningKeysLastUsed": "Sist brukt", + "provisioningKeysNoExpiry": "Ingen utløpsdato", + "provisioningKeysNeverUsed": "Aldri", + "provisioningKeysEdit": "Rediger bestemmelsesnøkkel", + "provisioningKeysEditDescription": "Oppdater maksimal størrelse for bunt og utløpstid for denne nøkkelen.", + "provisioningKeysApproveNewSites": "Godkjenn nye nettsteder", + "provisioningKeysApproveNewSitesDescription": "Godkjenn automatisk nettsteder som registrerer deg med denne nøkkelen.", + "provisioningKeysUpdateError": "Feil under oppdatering av foreløpig nøkkel", + "provisioningKeysUpdated": "Foreslå nøkkel oppdatert", + "provisioningKeysUpdatedDescription": "Dine endringer er lagret.", + "provisioningKeysBannerTitle": "Sidens bestemmende nøkler", + "provisioningKeysBannerDescription": "Generer en provisjonsnøkkel og bruk den med Newt-kontakten for automatisk opprettelse av nettsteder ved første oppstart - ingen behov for å sette opp separate legitimasjoner for hvert nettsted.", + "provisioningKeysBannerButtonText": "Lær mer", + "pendingSitesBannerTitle": "Ventende nettsteder", + "pendingSitesBannerDescription": "Nettsteder som kobler seg til ved bruk av en provisjonsnøkkel vises her for vurdering.", + "pendingSitesBannerButtonText": "Lær mer", "apiKeysSettings": "{apiKeyName} Innstillinger", "userTitle": "Administrer alle brukere", "userDescription": "Vis og administrer alle brukere i systemet", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Aktivering av lisensnøkkel feilet", "licenseErrorKeyActivateDescription": "Det oppstod en feil under aktivering av lisensnøkkelen.", "licenseAbout": "Om Lisensiering", + "licenseBannerTitle": "Aktiver din bedriftslisens", + "licenseBannerDescription": "Lås opp bedriftsfunksjoner for din egenvertede Pangolin-instans. Kjøp en lisensnøkkel for å aktivere premium-funksjoner og legg den inn nedenfor.", + "licenseBannerGetLicense": "Få en lisens", + "licenseBannerViewDocs": "Vis dokumentasjon", "communityEdition": "Fellesskapsutgave", "licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.", "licenseKeyActivated": "Lisensnøkkel aktivert", @@ -509,9 +566,12 @@ "userSaved": "Bruker lagret", "userSavedDescription": "Brukeren har blitt oppdatert.", "autoProvisioned": "Auto avlyst", + "autoProvisionSettings": "Auto leveringsinnstillinger", "autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør", "accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen", "accessControlsSubmit": "Lagre tilgangskontroller", + "singleRolePerUserPlanNotice": "Din plan støtter bare én rolle per bruker.", + "singleRolePerUserEditionNotice": "Denne utgaven støtter bare én rolle per bruker.", "roles": "Roller", "accessUsersRoles": "Administrer brukere og roller", "accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer", "targetErrorNoSite": "Ingen nettsted valgt", "targetErrorNoSiteDescription": "Velg et nettsted for målet", + "targetTargetsCleared": "Mål ryddet", + "targetTargetsClearedDescription": "Alle mål har blitt fjernet fra denne ressursen", "targetCreated": "Mål opprettet", "targetCreatedDescription": "Målet har blitt opprettet", "targetErrorCreate": "Kunne ikke opprette målet", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.", "setupTokenRequired": "Oppsetttoken er nødvendig", "actionUpdateSite": "Oppdater område", + "actionResetSiteBandwidth": "Tilbakestill organisasjons-båndbredde", "actionListSiteRoles": "List opp tillatte områderoller", "actionCreateResource": "Opprett ressurs", "actionDeleteResource": "Slett ressurs", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Fjern bruker", "actionListUsers": "List opp brukere", "actionAddUserRole": "Legg til brukerrolle", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Angi brukerroller", "actionGenerateAccessToken": "Generer tilgangstoken", "actionDeleteAccessToken": "Slett tilgangstoken", "actionListAccessTokens": "List opp tilgangstokener", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Roller", "sidebarShareableLinks": "Lenker", "sidebarApiKeys": "API-nøkler", + "sidebarProvisioning": "Levering", "sidebarSettings": "Innstillinger", "sidebarAllUsers": "Alle brukere", "sidebarIdentityProviders": "Identitetsleverandører", @@ -1890,6 +1954,40 @@ "exitNode": "Utgangsnode", "country": "Land", "rulesMatchCountry": "For tiden basert på kilde IP", + "region": "Fylke", + "selectRegion": "Velg region", + "searchRegions": "Søk etter områder...", + "noRegionFound": "Ingen region funnet.", + "rulesMatchRegion": "Velg en regional gruppering av land", + "rulesErrorInvalidRegion": "Ugyldig område", + "rulesErrorInvalidRegionDescription": "Vennligst velg et gyldig område.", + "regionAfrica": "Afrika", + "regionNorthernAfrica": "[country name] Nord-Afrika", + "regionEasternAfrica": "Øst-Afrika", + "regionMiddleAfrica": "Middle Africa", + "regionSouthernAfrica": "Sør-Afrika", + "regionWesternAfrica": "[country name] Vest-Afrika", + "regionAmericas": "Amerika", + "regionCaribbean": "Karibia", + "regionCentralAmerica": "Sentral-Amerika", + "regionSouthAmerica": "Sør-Amerika", + "regionNorthernAmerica": "Nord-Amerika", + "regionAsia": "Asia", + "regionCentralAsia": "Sentral-Asia", + "regionEasternAsia": "Øst-Asia", + "regionSouthEasternAsia": "Sørøst-Asia", + "regionSouthernAsia": "Sørlige Asia", + "regionWesternAsia": "Vest-Asia", + "regionEurope": "Europa", + "regionEasternEurope": "Øst-Europa", + "regionNorthernEurope": "Nord-Europa", + "regionSouthernEurope": "Sørlige Europa", + "regionWesternEurope": "Vest-Europa", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australia og New Zealand", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Administrert selv-hostet", "description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell", @@ -1938,6 +2036,25 @@ "invalidValue": "Ugyldig verdi", "idpTypeLabel": "Identitet leverandør type", "roleMappingExpressionPlaceholder": "F.eks. inneholder(grupper, 'admin') && 'Admin' ⋅'Medlem'", + "roleMappingModeFixedRoles": "Fast roller", + "roleMappingModeMappingBuilder": "Kartlegger bygger", + "roleMappingModeRawExpression": "Rå uttrykk", + "roleMappingFixedRolesPlaceholderSelect": "Velg en eller flere roller", + "roleMappingFixedRolesPlaceholderFreeform": "Skriv inn rollenavn (eksakt treff per organisasjon)", + "roleMappingFixedRolesDescriptionSameForAll": "Tilordne den samme rollen som er satt til hver automatisk midlertidig bruker.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "For standard policyer, type rollenavn som eksisterer i hver organisasjon der brukerne tilbys. Navn må stemmer nøyaktig.", + "roleMappingClaimPath": "Krev sti", + "roleMappingClaimPathPlaceholder": "grupper", + "roleMappingClaimPathDescription": "Sti i i token nyttelast som inneholder kildeverdier (for eksempel grupper).", + "roleMappingMatchValue": "Treff verdi", + "roleMappingAssignRoles": "Tilordne roller", + "roleMappingAddMappingRule": "Legg til tilordningsregel", + "roleMappingRawExpressionResultDescription": "Uttrykk skal vurderes til en streng eller en tekststreng.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Uttrykk må evaluere til en streng (en rollenavn).", + "roleMappingMatchValuePlaceholder": "Match verdi (for eksempel: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Angi rollenavn (eksakt per org)", + "roleMappingBuilderFreeformRowHint": "Rollenavn må samsvare med en rolle i hver målorganisasjon.", + "roleMappingRemoveRule": "Fjern", "idpGoogleConfiguration": "Google Konfigurasjon", "idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Velg et domene for organisasjonens autentiseringsside", "domainPickerProvidedDomain": "Gitt domene", "domainPickerFreeProvidedDomain": "Gratis oppgitt domene", + "domainPickerFreeDomainsPaidFeature": "Angitte domener er en betalingsfunksjon. Abonner for å få et domene inkludert i din plan – ingen behov for å ta med ditt eget.", "domainPickerVerified": "Bekreftet", "domainPickerUnverified": "Uverifisert", + "domainPickerManual": "Manuell", "domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.", "domainPickerError": "Feil", "domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener", @@ -2235,7 +2354,7 @@ "description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte." } }, - "personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)", + "personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)", "buttons": { "continueToCheckout": "Fortsett til kassen" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger", "logRetentionActionLabel": "Handlings logg nytt", "logRetentionActionDescription": "Hvor lenge handlingen skal lagres", + "logRetentionConnectionLabel": "Logg nyhet", + "logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger", "logRetentionDisabled": "Deaktivert", "logRetention3Days": "3 dager", "logRetention7Days": "7 dager", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Slutt på neste år", "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen", "accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen", + "connectionLogs": "Loggfiler for tilkobling", + "connectionLogsDescription": "Vis tilkoblingslogger for tunneler i denne organisasjonen", + "sidebarLogsConnection": "Loggfiler for tilkobling", + "sidebarLogsStreaming": "Strømming", + "sourceAddress": "Kilde adresse", + "destinationAddress": "Måladresse (Automatic Translation)", + "duration": "Varighet", "licenseRequiredToUse": "En Enterprise Edition lisens eller Pangolin Cloud er påkrevd for å bruke denne funksjonen. Bestill en demo eller POC prøveversjon.", "ossEnterpriseEditionRequired": "Enterprise Edition er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i Pangolin Cloud. Bestill en demo eller POC studie.", "certResolver": "Sertifikat løser", @@ -2487,6 +2615,9 @@ "machineClients": "Maskinklienter", "install": "Installer", "run": "Kjør", + "envFile": "Miljøfil", + "serviceFile": "Tjenestefil", + "enableAndStart": "Aktiver og start", "clientNameDescription": "Visningsnavnet til klienten som kan endres senere.", "clientAddress": "Klientadresse (avansert)", "setupFailedToFetchSubnet": "Kunne ikke hente standard undernett", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.", "approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering", "approvalsEmptyStateButtonText": "Administrer Roller", - "domainErrorTitle": "Vi har problemer med å verifisere domenet ditt" + "domainErrorTitle": "Vi har problemer med å verifisere domenet ditt", + "idpAdminAutoProvisionPoliciesTabHint": "Konfigurer rollegartlegging og organisasjonspolicyer på Auto leveringsinnstillinger fanen.", + "streamingTitle": "Hendelse Strømming", + "streamingDescription": "Stream hendelser fra din organisasjon til eksterne destinasjoner i sanntid.", + "streamingUnnamedDestination": "Plassering uten navn", + "streamingNoUrlConfigured": "Ingen URL konfigurert", + "streamingAddDestination": "Legg til mål", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Send hendelser til alle HTTP-endepunkter med fleksibel autentisering og maling.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Strøm hendelser til en S3-kompatibel objektlagringskjøt. Kommer snart.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.", + "streamingTypePickerDescription": "Velg en måltype for å komme i gang.", + "streamingFailedToLoad": "Kan ikke laste inn destinasjoner", + "streamingUnexpectedError": "En uventet feil oppstod.", + "streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon", + "streamingDeletedSuccess": "Målet ble slettet", + "streamingFailedToDelete": "Kunne ikke slette destinasjon", + "streamingDeleteTitle": "Slett mål", + "streamingDeleteButtonText": "Slett mål", + "streamingDeleteDialogAreYouSure": "Er du sikker på at du vil slette", + "streamingDeleteDialogThisDestination": "denne destinasjonen", + "streamingDeleteDialogPermanentlyRemoved": "? Alle konfigurasjoner vil bli slettet permanent.", + "httpDestEditTitle": "Rediger mål", + "httpDestAddTitle": "Legg til HTTP-destinasjon", + "httpDestEditDescription": "Oppdater konfigurasjonen for denne HTTP-hendelsesstrømmedestinasjonen.", + "httpDestAddDescription": "Konfigurer et nytt HTTP endepunkt for å motta organisasjonens hendelser.", + "httpDestTabSettings": "Innstillinger", + "httpDestTabHeaders": "Overskrifter", + "httpDestTabBody": "Innhold", + "httpDestTabLogs": "Logger", + "httpDestNamePlaceholder": "Min HTTP destinasjon", + "httpDestUrlLabel": "Destinasjons URL", + "httpDestUrlErrorHttpRequired": "URL-adressen må bruke httpp eller https", + "httpDestUrlErrorHttpsRequired": "HTTPS er nødvendig for distribusjon av sky", + "httpDestUrlErrorInvalid": "Skriv inn en gyldig nettadresse (f.eks. https://eksempel.com/webhook)", + "httpDestAuthTitle": "Autentisering", + "httpDestAuthDescription": "Velg hvordan ønsker til sluttpunktet ditt er autentisert.", + "httpDestAuthNoneTitle": "Ingen godkjenning", + "httpDestAuthNoneDescription": "Sender forespørsler uten autorisasjonsoverskrift.", + "httpDestAuthBearerTitle": "Bærer Symbol", + "httpDestAuthBearerDescription": "Legger til en Autorisasjon: Bearer '' header til hver forespørsel.", + "httpDestAuthBearerPlaceholder": "Din API-nøkkel eller token", + "httpDestAuthBasicTitle": "Standard Auth", + "httpDestAuthBasicDescription": "Legger til en Autorisasjon: Basic '' header. Gi legitimasjon som brukernavn:passord.", + "httpDestAuthBasicPlaceholder": "brukernavn:passord", + "httpDestAuthCustomTitle": "Egendefinert topptekst", + "httpDestAuthCustomDescription": "Angi et egendefinert HTTP headers navn og verdi for autentisering (f.eks X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Topptekst navn (f.eks X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Header verdi", + "httpDestCustomHeadersTitle": "Egendefinerte HTTP-overskrifter", + "httpDestCustomHeadersDescription": "Legg til egendefinerte overskrifter til hver utgående forespørsel. Nyttig for statisk tokens eller en egendefinert innholdstype. Som standard blir innholdstype: applikasjon/json sendt.", + "httpDestNoHeadersConfigured": "Ingen egendefinerte overskrifter konfigurert. Klikk \"Legg til topptekst\" for å legge til en.", + "httpDestHeaderNamePlaceholder": "Navn på topptekst", + "httpDestHeaderValuePlaceholder": "Verdi", + "httpDestAddHeader": "Legg til topptekst", + "httpDestBodyTemplateTitle": "Egendefinert hovedmal", + "httpDestBodyTemplateDescription": "Kontroller JSON nyttelaststrukturen sendt til ditt endepunkt. Hvis deaktivert, sendes et standard JSON-objekt for hver hendelse.", + "httpDestEnableBodyTemplate": "Aktiver egendefinert meldingsmal", + "httpDestBodyTemplateLabel": "Kroppsmal (JSON)", + "httpDestBodyTemplateHint": "Bruk designmal variabler for å referere til eventfelt i din betaling.", + "httpDestPayloadFormatTitle": "Mål format", + "httpDestPayloadFormatDescription": "Hvordan blir hendelser serialisert inn i hver forespørselsorgan.", + "httpDestFormatJsonArrayTitle": "JSON liste", + "httpDestFormatJsonArrayDescription": "Én forespørsel per batch, innholdet er en JSON-liste. Kompatibel med de mest generiske webhooks og Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON — et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.", + "httpDestFormatSingleTitle": "En hendelse per forespørsel", + "httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hendelse. Bruk bare for endepunkter som ikke kan håndtere batcher.", + "httpDestLogTypesTitle": "Logg typer", + "httpDestLogTypesDescription": "Velg hvilke loggtyper som blir videresendt til dette målet. Bare aktiverte loggtyper vil bli strømmet.", + "httpDestAccessLogsTitle": "Tilgangslogger (Automatic Translation)", + "httpDestAccessLogsDescription": "Adgangsforsøk for ressurser, inkludert godkjente og nektet forespørsler.", + "httpDestActionLogsTitle": "Handlingslogger", + "httpDestActionLogsDescription": "Administrative tiltak som utføres av brukere innenfor organisasjonen.", + "httpDestConnectionLogsTitle": "Loggfiler for tilkobling", + "httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.", + "httpDestRequestLogsTitle": "Forespørselslogger (Automatic Translation)", + "httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.", + "httpDestSaveChanges": "Lagre endringer", + "httpDestCreateDestination": "Opprett mål", + "httpDestUpdatedSuccess": "Målet er oppdatert", + "httpDestCreatedSuccess": "Målet er opprettet", + "httpDestUpdateFailed": "Kunne ikke oppdatere destinasjon", + "httpDestCreateFailed": "Kan ikke opprette mål" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index f0eaff3fd..d7d64abc1 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -148,6 +148,11 @@ "createLink": "Koppeling aanmaken", "resourcesNotFound": "Geen bronnen gevonden", "resourceSearch": "Zoek bronnen", + "machineSearch": "Zoek machines", + "machinesSearch": "Zoek machine-clients...", + "machineNotFound": "Geen machines gevonden", + "userDeviceSearch": "Gebruikersapparaten zoeken", + "userDevicesSearch": "Gebruikersapparaten zoeken...", "openMenu": "Menu openen", "resource": "Bron", "title": "Aanspreektitel", @@ -323,6 +328,54 @@ "apiKeysDelete": "API-sleutel verwijderen", "apiKeysManage": "API-sleutels beheren", "apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API", + "provisioningKeysTitle": "Vertrekkende sleutel", + "provisioningKeysManage": "Beheren van Provisioning Sleutels", + "provisioningKeysDescription": "Provisionerende sleutels worden gebruikt om geautomatiseerde sitebepaling voor uw organisatie te verifiëren.", + "provisioningManage": "Provisie", + "provisioningDescription": "Voorzieningssleutels beheren en sites beoordelen in afwachting van goedkeuring.", + "pendingSites": "Openstaande sites", + "siteApproveSuccess": "Site succesvol goedgekeurd", + "siteApproveError": "Fout bij goedkeuren website", + "provisioningKeys": "Verhelderende sleutels", + "searchProvisioningKeys": "Zoek provisioningsleutels ...", + "provisioningKeysAdd": "Genereer Provisioning Sleutel", + "provisioningKeysErrorDelete": "Fout bij verwijderen provisioning sleutel", + "provisioningKeysErrorDeleteMessage": "Fout bij verwijderen provisioning sleutel", + "provisioningKeysQuestionRemove": "Weet u zeker dat u deze proefsleutel van de organisatie wilt verwijderen?", + "provisioningKeysMessageRemove": "Eenmaal verwijderd, kan de sleutel niet meer worden gebruikt voor site-instructie.", + "provisioningKeysDeleteConfirm": "Bevestig Verwijderen Provisione-sleutel", + "provisioningKeysDelete": "Provisione-sleutel verwijderen", + "provisioningKeysCreate": "Genereer Provisioning Sleutel", + "provisioningKeysCreateDescription": "Een nieuwe provisioningsleutel voor de organisatie genereren", + "provisioningKeysSeeAll": "Bekijk alle provisioning sleutels", + "provisioningKeysSave": "Sla de provisioning sleutel op", + "provisioningKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plaats.", + "provisioningKeysErrorCreate": "Fout bij aanmaken provisioning sleutel", + "provisioningKeysList": "Nieuwe provisioning sleutel", + "provisioningKeysMaxBatchSize": "Maximale batchgrootte", + "provisioningKeysUnlimitedBatchSize": "Onbeperkte batchgrootte (geen limiet)", + "provisioningKeysMaxBatchUnlimited": "Onbeperkt", + "provisioningKeysMaxBatchSizeInvalid": "Voer een geldige maximale batchgrootte in (1–1.000,000).", + "provisioningKeysValidUntil": "Geldig tot", + "provisioningKeysValidUntilHint": "Laat leeg voor geen vervaldatum.", + "provisioningKeysValidUntilInvalid": "Voer een geldige datum en tijd in.", + "provisioningKeysNumUsed": "Aantal keer gebruikt", + "provisioningKeysLastUsed": "Laatst gebruikt", + "provisioningKeysNoExpiry": "Geen vervaldatum", + "provisioningKeysNeverUsed": "Nooit", + "provisioningKeysEdit": "Wijzig Provisioning Sleutel", + "provisioningKeysEditDescription": "Werk de maximale batchgrootte en verlooptijd voor deze sleutel bij.", + "provisioningKeysApproveNewSites": "Goedkeuren van nieuwe sites", + "provisioningKeysApproveNewSitesDescription": "Automatisch sites goedkeuren die zich registreren met deze sleutel.", + "provisioningKeysUpdateError": "Fout tijdens bijwerken provisioning sleutel", + "provisioningKeysUpdated": "Provisie sleutel bijgewerkt", + "provisioningKeysUpdatedDescription": "Uw wijzigingen zijn opgeslagen.", + "provisioningKeysBannerTitle": "Bewerkingssleutels voor websites", + "provisioningKeysBannerDescription": "Genereer een inrichtingssleutel en gebruik deze met de Newt-connector om automatisch sites te maken bij de eerste opstart - er is geen behoefte om aparte inloggegevens voor elke site in te stellen.", + "provisioningKeysBannerButtonText": "Meer informatie", + "pendingSitesBannerTitle": "Openstaande sites", + "pendingSitesBannerDescription": "Sites die verbinding maken met een inrichtingssleutel verschijnen hier voor beoordeling.", + "pendingSitesBannerButtonText": "Meer informatie", "apiKeysSettings": "{apiKeyName} instellingen", "userTitle": "Alle gebruikers beheren", "userDescription": "Bekijk en beheer alle gebruikers in het systeem", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Licentiesleutel activeren mislukt", "licenseErrorKeyActivateDescription": "Er is een fout opgetreden tijdens het activeren van de licentiesleutel.", "licenseAbout": "Over licenties", + "licenseBannerTitle": "Activeer Uw Enterprise Licentie", + "licenseBannerDescription": "Ontgrendel enterprise-functies voor uw zelf-gehoste Pangolin-instantie. Koop een licentiesleutel om premium mogelijkheden te activeren, voeg deze vervolgens hieronder toe.", + "licenseBannerGetLicense": "Koop een Licentie", + "licenseBannerViewDocs": "Bekijk Documentatie", "communityEdition": "Community editie", "licenseAboutDescription": "Dit geldt voor gebruikers van bedrijven en ondernemingen die Pangolin in gebruiken in een commerciële omgeving. Als u Pangolin gebruikt voor persoonlijk gebruik, kunt u dit gedeelte negeren.", "licenseKeyActivated": "Licentiesleutel geactiveerd", @@ -509,9 +566,12 @@ "userSaved": "Gebruiker opgeslagen", "userSavedDescription": "De gebruiker is bijgewerkt.", "autoProvisioned": "Automatisch bevestigen", + "autoProvisionSettings": "Auto Provisie Instellingen", "autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider", "accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie", "accessControlsSubmit": "Bewaar Toegangsbesturing", + "singleRolePerUserPlanNotice": "Uw plan ondersteunt slechts één rol per gebruiker.", + "singleRolePerUserEditionNotice": "Deze editie ondersteunt slechts één rol per gebruiker.", "roles": "Rollen", "accessUsersRoles": "Beheer Gebruikers & Rollen", "accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot de organisatie te beheren", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Voer een geldig poortnummer in", "targetErrorNoSite": "Geen site geselecteerd", "targetErrorNoSiteDescription": "Selecteer een site voor het doel", + "targetTargetsCleared": "Doelen gewist", + "targetTargetsClearedDescription": "Alle doelen zijn verwijderd van deze bron", "targetCreated": "Doel aangemaakt", "targetCreatedDescription": "Doel is succesvol aangemaakt", "targetErrorCreate": "Kan doel niet aanmaken", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.", "setupTokenRequired": "Setup-token is vereist", "actionUpdateSite": "Site bijwerken", + "actionResetSiteBandwidth": "Reset organisatieschandbreedte", "actionListSiteRoles": "Toon toegestane sitenollen", "actionCreateResource": "Bron maken", "actionDeleteResource": "Document verwijderen", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Gebruiker verwijderen", "actionListUsers": "Gebruikers weergeven", "actionAddUserRole": "Gebruikersrol toevoegen", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Stel gebruikersrollen in", "actionGenerateAccessToken": "Genereer Toegangstoken", "actionDeleteAccessToken": "Verwijder toegangstoken", "actionListAccessTokens": "Lijst toegangstokens", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Rollen", "sidebarShareableLinks": "Koppelingen", "sidebarApiKeys": "API sleutels", + "sidebarProvisioning": "Provisie", "sidebarSettings": "Instellingen", "sidebarAllUsers": "Alle gebruikers", "sidebarIdentityProviders": "Identiteit aanbieders", @@ -1890,6 +1954,40 @@ "exitNode": "Exit Node", "country": "Land", "rulesMatchCountry": "Momenteel gebaseerd op bron IP", + "region": "Regio", + "selectRegion": "Selecteer regio", + "searchRegions": "Zoek regio's...", + "noRegionFound": "Geen regio gevonden.", + "rulesMatchRegion": "Selecteer een regionale groepering van landen", + "rulesErrorInvalidRegion": "Ongeldige regio", + "rulesErrorInvalidRegionDescription": "Selecteer een geldige regio.", + "regionAfrica": "Afrika", + "regionNorthernAfrica": "Noord-Afrika", + "regionEasternAfrica": "Oost Afrika", + "regionMiddleAfrica": "Midden Afrika", + "regionSouthernAfrica": "Zuidelijk Afrika", + "regionWesternAfrica": "Westelijk Afrika", + "regionAmericas": "Amerika's", + "regionCaribbean": "Caraïben", + "regionCentralAmerica": "Midden-Amerika", + "regionSouthAmerica": "Zuid Amerika", + "regionNorthernAmerica": "Noord-Amerika", + "regionAsia": "Azië", + "regionCentralAsia": "Centraal-Azië", + "regionEasternAsia": "Oost-Azië", + "regionSouthEasternAsia": "Zuid-Oost-Azië", + "regionSouthernAsia": "Zuid-Azië", + "regionWesternAsia": "Westelijk Azië", + "regionEurope": "Europa", + "regionEasternEurope": "Oost-Europa", + "regionNorthernEurope": "Noord-Europa", + "regionSouthernEurope": "Zuid-Europa", + "regionWesternEurope": "West-Europa", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australië en Nieuw-Zeeland", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Beheerde Self-Hosted", "description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders", @@ -1938,6 +2036,25 @@ "invalidValue": "Ongeldige waarde", "idpTypeLabel": "Identiteit provider type", "roleMappingExpressionPlaceholder": "bijvoorbeeld bevat (groepen, 'admin') && 'Admin' ½ 'Member'", + "roleMappingModeFixedRoles": "Vaste rollen", + "roleMappingModeMappingBuilder": "Toewijzing Bouwer", + "roleMappingModeRawExpression": "Ruwe expressie", + "roleMappingFixedRolesPlaceholderSelect": "Selecteer één of meer rollen", + "roleMappingFixedRolesPlaceholderFreeform": "Typ rolnamen (exacte overeenkomst per organisatie)", + "roleMappingFixedRolesDescriptionSameForAll": "Wijs dezelfde rolset toe aan elke auto-provisioned gebruiker.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Voor standaardbeleid, typ rolnamen die bestaan in elke organisatie waar gebruikers worden opgegeven. Namen moeten exact overeenkomen.", + "roleMappingClaimPath": "Claim pad", + "roleMappingClaimPathPlaceholder": "Groepen", + "roleMappingClaimPathDescription": "Pad in de token payload die bronwaarden bevat (bijvoorbeeld groepen).", + "roleMappingMatchValue": "Kies een waarde", + "roleMappingAssignRoles": "Rollen toewijzen", + "roleMappingAddMappingRule": "Toewijzingsregel toevoegen", + "roleMappingRawExpressionResultDescription": "Expressie moet een tekenreeks of tekenreeks evalueren.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Expressie moet evalueren naar een tekenreeks (een naam met één rol).", + "roleMappingMatchValuePlaceholder": "Overeenkomende waarde (bijvoorbeeld: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Typ rolnamen (exact per org)", + "roleMappingBuilderFreeformRowHint": "Rol namen moeten overeenkomen met een rol in elke doelorganisatie.", + "roleMappingRemoveRule": "Verwijderen", "idpGoogleConfiguration": "Google Configuratie", "idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie", "domainPickerProvidedDomain": "Opgegeven domein", "domainPickerFreeProvidedDomain": "Gratis verstrekt domein", + "domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen — je hoeft er zelf geen mee te brengen.", "domainPickerVerified": "Geverifieerd", "domainPickerUnverified": "Ongeverifieerd", + "domainPickerManual": "Handleiding", "domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.", "domainPickerError": "Foutmelding", "domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen", @@ -2235,7 +2354,7 @@ "description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning." } }, - "personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)", + "personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)", "buttons": { "continueToCheckout": "Doorgaan naar afrekenen" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven", "logRetentionActionLabel": "Actie log bewaring", "logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven", + "logRetentionConnectionLabel": "Connectie log bewaring", + "logRetentionConnectionDescription": "Hoe lang de verbindingslogs onderhouden", "logRetentionDisabled": "Uitgeschakeld", "logRetention3Days": "3 dagen", "logRetention7Days": "7 dagen", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Einde van volgend jaar", "actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie", "accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken", + "connectionLogs": "Connectie Logs", + "connectionLogsDescription": "Toon verbindingslogs voor tunnels in deze organisatie", + "sidebarLogsConnection": "Connectie Logs", + "sidebarLogsStreaming": "Streamen", + "sourceAddress": "Bron adres", + "destinationAddress": "Adres bestemming", + "duration": "Duur", "licenseRequiredToUse": "Een Enterprise Edition licentie of Pangolin Cloud is vereist om deze functie te gebruiken. Boek een demo of POC trial.", "ossEnterpriseEditionRequired": "De Enterprise Edition is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in Pangolin Cloud. Boek een demo of POC trial.", "certResolver": "Certificaat Resolver", @@ -2487,6 +2615,9 @@ "machineClients": "Machine Clienten", "install": "Installeren", "run": "Uitvoeren", + "envFile": "Omgevingsbestand", + "serviceFile": "Servicebestand", + "enableAndStart": "Inschakelen en Starten", "clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.", "clientAddress": "Klant adres (Geavanceerd)", "setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.", "approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen", "approvalsEmptyStateButtonText": "Rollen beheren", - "domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein" + "domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein", + "idpAdminAutoProvisionPoliciesTabHint": "Configureer rolverrekening en organisatie beleid in het Auto Provision Settings tab.", + "streamingTitle": "Event streaming", + "streamingDescription": "Stream events van uw organisatie naar externe bestemmingen in realtime.", + "streamingUnnamedDestination": "Naamloze bestemming", + "streamingNoUrlConfigured": "Geen URL ingesteld", + "streamingAddDestination": "Bestemming toevoegen", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Stuur gebeurtenissen naar elk HTTP eindpunt met flexibele authenticatie en template.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Stream events naar een S3-compatibele object-opslagemmer. Binnenkort beschikbaar.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.", + "streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.", + "streamingFailedToLoad": "Laden van bestemmingen mislukt", + "streamingUnexpectedError": "Er is een onverwachte fout opgetreden.", + "streamingFailedToUpdate": "Bijwerken bestemming mislukt", + "streamingDeletedSuccess": "Bestemming succesvol verwijderd", + "streamingFailedToDelete": "Verwijderen van bestemming mislukt", + "streamingDeleteTitle": "Verwijder bestemming", + "streamingDeleteButtonText": "Verwijder bestemming", + "streamingDeleteDialogAreYouSure": "Weet u zeker dat u wilt verwijderen", + "streamingDeleteDialogThisDestination": "deze bestemming", + "streamingDeleteDialogPermanentlyRemoved": "? Alle configuratie zal permanent worden verwijderd.", + "httpDestEditTitle": "Bewerk bestemming", + "httpDestAddTitle": "Voeg HTTP bestemming toe", + "httpDestEditDescription": "Werk de configuratie voor deze HTTP-event streaming bestemming bij.", + "httpDestAddDescription": "Configureer een nieuw HTTP-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.", + "httpDestTabSettings": "Instellingen", + "httpDestTabHeaders": "Kopteksten", + "httpDestTabBody": "Lichaam", + "httpDestTabLogs": "Logboeken", + "httpDestNamePlaceholder": "Mijn HTTP-bestemming", + "httpDestUrlLabel": "Bestemming URL", + "httpDestUrlErrorHttpRequired": "URL moet http of https gebruiken", + "httpDestUrlErrorHttpsRequired": "HTTPS is vereist op cloud implementaties", + "httpDestUrlErrorInvalid": "Voer een geldige URL in (bijv. https://example.com/webhook)", + "httpDestAuthTitle": "Authenticatie", + "httpDestAuthDescription": "Kies hoe verzoeken voor uw eindpunt zijn geverifieerd.", + "httpDestAuthNoneTitle": "Geen authenticatie", + "httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.", + "httpDestAuthBearerTitle": "Betere Token", + "httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '' header toe aan elk verzoek.", + "httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token", + "httpDestAuthBasicTitle": "Basis authenticatie", + "httpDestAuthBasicDescription": "Voegt een Authorization: Basic '' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.", + "httpDestAuthBasicPlaceholder": "Gebruikersnaam:wachtwoord", + "httpDestAuthCustomTitle": "Aangepaste koptekst", + "httpDestAuthCustomDescription": "Specificeer een aangepaste HTTP header naam en waarde voor authenticatie (bijv. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Header naam (bijv. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Header waarde", + "httpDestCustomHeadersTitle": "Aangepaste HTTP Headers", + "httpDestCustomHeadersDescription": "Voeg aangepaste headers toe aan elk uitgaande verzoek. Handig voor statische tokens of een aangepast Content-Type. Standaard Content-Type: application/json wordt verzonden.", + "httpDestNoHeadersConfigured": "Geen aangepaste headers geconfigureerd. Klik op \"Header\" om er een toe te voegen.", + "httpDestHeaderNamePlaceholder": "Naam koptekst", + "httpDestHeaderValuePlaceholder": "Waarde", + "httpDestAddHeader": "Koptekst toevoegen", + "httpDestBodyTemplateTitle": "Aangepaste Body Sjabloon", + "httpDestBodyTemplateDescription": "Bestuur de JSON payload structuur verzonden naar uw eindpunt. Indien uitgeschakeld, wordt een standaard JSON object verzonden voor elke event.", + "httpDestEnableBodyTemplate": "Aangepaste lichaam sjabloon inschakelen", + "httpDestBodyTemplateLabel": "Body sjabloon (JSON)", + "httpDestBodyTemplateHint": "Gebruik sjabloonvariabelen om te verwijzen naar gebeurtenisvelden in uw payload.", + "httpDestPayloadFormatTitle": "Payload formaat", + "httpDestPayloadFormatDescription": "Hoe evenementen worden geserialiseerd in elk verzoeklichaam.", + "httpDestFormatJsonArrayTitle": "JSON matrix", + "httpDestFormatJsonArrayDescription": "Eén verzoek per batch, lichaam is een JSON-array. Compatibel met de meeste algemene webhooks en Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Eén aanvraag per batch, lichaam is nieuwe JSON gescheiden - één object per regel, geen buitenste array. Vereist door Splunk HEC, Elastic / OpenSearch, en Grafana Loki.", + "httpDestFormatSingleTitle": "Eén afspraak per verzoek", + "httpDestFormatSingleDescription": "Stuurt een aparte HTTP POST voor elk individueel event. Gebruik alleen voor eindpunten die geen batches kunnen verwerken.", + "httpDestLogTypesTitle": "Log soorten", + "httpDestLogTypesDescription": "Kies welke log types doorgestuurd worden naar deze bestemming. Alleen ingeschakelde log types worden gestreden.", + "httpDestAccessLogsTitle": "Toegang tot logboek", + "httpDestAccessLogsDescription": "Hulpbrontoegangspogingen, inclusief geauthenticeerde en weigerde aanvragen.", + "httpDestActionLogsTitle": "Actie logs", + "httpDestActionLogsDescription": "Administratieve acties uitgevoerd door gebruikers binnen de organisatie.", + "httpDestConnectionLogsTitle": "Connectie Logs", + "httpDestConnectionLogsDescription": "Verbinding met de Site en tunnel maken verbroken, inclusief verbindingen en verbindingen.", + "httpDestRequestLogsTitle": "Logboeken aanvragen", + "httpDestRequestLogsDescription": "HTTP request logs voor proxied hulpmiddelen, waaronder methode, pad en response code.", + "httpDestSaveChanges": "Wijzigingen opslaan", + "httpDestCreateDestination": "Maak bestemming aan", + "httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt", + "httpDestCreatedSuccess": "Bestemming succesvol aangemaakt", + "httpDestUpdateFailed": "Bijwerken bestemming mislukt", + "httpDestCreateFailed": "Aanmaken bestemming mislukt" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 998fcc880..e58aafda1 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -148,6 +148,11 @@ "createLink": "Utwórz link", "resourcesNotFound": "Nie znaleziono zasobów", "resourceSearch": "Szukaj zasobów", + "machineSearch": "Wyszukiwarki", + "machinesSearch": "Szukaj klientów maszyn...", + "machineNotFound": "Nie znaleziono maszyn", + "userDeviceSearch": "Szukaj urządzeń użytkownika", + "userDevicesSearch": "Szukaj urządzeń użytkownika...", "openMenu": "Otwórz menu", "resource": "Zasoby", "title": "Tytuł", @@ -323,6 +328,54 @@ "apiKeysDelete": "Usuń klucz API", "apiKeysManage": "Zarządzaj kluczami API", "apiKeysDescription": "Klucze API służą do uwierzytelniania z API integracji", + "provisioningKeysTitle": "Klucz Zaopatrzenia", + "provisioningKeysManage": "Zarządzaj kluczami zaopatrzenia", + "provisioningKeysDescription": "Klucze zaopatrzenia są używane do uwierzytelniania zautomatyzowanego zaopatrzenia twojej organizacji.", + "provisioningManage": "Dostarczanie", + "provisioningDescription": "Zarządzaj kluczami rezerwacji i sprawdzaj oczekujące strony oczekujące na zatwierdzenie.", + "pendingSites": "Witryny oczekujące", + "siteApproveSuccess": "Witryna została pomyślnie zatwierdzona", + "siteApproveError": "Błąd zatwierdzania witryny", + "provisioningKeys": "Klucze Zaopatrzenia", + "searchProvisioningKeys": "Szukaj kluczy zaopatrzenia...", + "provisioningKeysAdd": "Wygeneruj klucz zaopatrzenia", + "provisioningKeysErrorDelete": "Błąd podczas usuwania klucza zaopatrzenia", + "provisioningKeysErrorDeleteMessage": "Błąd podczas usuwania klucza zaopatrzenia", + "provisioningKeysQuestionRemove": "Czy na pewno chcesz usunąć ten klucz rezerwacji z organizacji?", + "provisioningKeysMessageRemove": "Po usunięciu, klucz nie może być już używany do tworzenia witryny.", + "provisioningKeysDeleteConfirm": "Potwierdź usunięcie klucza zaopatrzenia", + "provisioningKeysDelete": "Usuń klucz zaopatrzenia", + "provisioningKeysCreate": "Wygeneruj klucz zaopatrzenia", + "provisioningKeysCreateDescription": "Wygeneruj nowy klucz tworzenia rezerw dla organizacji", + "provisioningKeysSeeAll": "Zobacz wszystkie klucze rezerwacji", + "provisioningKeysSave": "Zapisz klucz zaopatrzenia", + "provisioningKeysSaveDescription": "Możesz to zobaczyć tylko raz. Skopiuj je do bezpiecznego miejsca.", + "provisioningKeysErrorCreate": "Błąd podczas tworzenia klucza zaopatrzenia", + "provisioningKeysList": "Nowy klucz rezerwacji", + "provisioningKeysMaxBatchSize": "Maksymalny rozmiar partii", + "provisioningKeysUnlimitedBatchSize": "Nieograniczony rozmiar partii (bez limitu)", + "provisioningKeysMaxBatchUnlimited": "Nieograniczona", + "provisioningKeysMaxBatchSizeInvalid": "Wprowadź poprawny maksymalny rozmiar partii (1–1 000,000).", + "provisioningKeysValidUntil": "Ważny do", + "provisioningKeysValidUntilHint": "Pozostaw puste, aby nie wygasnąć.", + "provisioningKeysValidUntilInvalid": "Wprowadź prawidłową datę i godzinę.", + "provisioningKeysNumUsed": "Używane czasy", + "provisioningKeysLastUsed": "Ostatnio używane", + "provisioningKeysNoExpiry": "Brak wygaśnięcia", + "provisioningKeysNeverUsed": "Nigdy", + "provisioningKeysEdit": "Edytuj klucz zaopatrzenia", + "provisioningKeysEditDescription": "Zaktualizuj maksymalny rozmiar partii i czas wygaśnięcia dla tego klucza.", + "provisioningKeysApproveNewSites": "Zatwierdź nowe witryny", + "provisioningKeysApproveNewSitesDescription": "Automatycznie zatwierdzaj witryny, które rejestrują się za pomocą tego klucza.", + "provisioningKeysUpdateError": "Błąd podczas aktualizacji klucza zaopatrzenia", + "provisioningKeysUpdated": "Klucz zaopatrzenia zaktualizowany", + "provisioningKeysUpdatedDescription": "Twoje zmiany zostały zapisane.", + "provisioningKeysBannerTitle": "Klucze Zaopatrzenia witryny", + "provisioningKeysBannerDescription": "Wygeneruj klucz provisioning i użyj go z konektorem Newt do automatycznego tworzenia witryn przy pierwszym uruchomieniu - nie ma potrzeby konfigurowania oddzielnych poświadczeń dla każdej witryny.", + "provisioningKeysBannerButtonText": "Dowiedz się więcej", + "pendingSitesBannerTitle": "Witryny oczekujące", + "pendingSitesBannerDescription": "Witryny, które łączą się za pomocą klucza provisioning, pojawią się tutaj do przeglądu.", + "pendingSitesBannerButtonText": "Dowiedz się więcej", "apiKeysSettings": "Ustawienia {apiKeyName}", "userTitle": "Zarządzaj wszystkimi użytkownikami", "userDescription": "Zobacz i zarządzaj wszystkimi użytkownikami w systemie", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Nie udało się aktywować klucza licencji", "licenseErrorKeyActivateDescription": "Wystąpił błąd podczas aktywacji klucza licencyjnego.", "licenseAbout": "O licencjonowaniu", + "licenseBannerTitle": "Aktywuj swoją licencję Enterprise", + "licenseBannerDescription": "Odblokuj funkcje korporacyjne dla swojego autonomicznego wdrożenia Pangolin. Kup klucz licencyjny, aby aktywować możliwości premium, a następnie wprowadź go poniżej.", + "licenseBannerGetLicense": "Uzyskaj licencję", + "licenseBannerViewDocs": "Zobacz dokumentację", "communityEdition": "Edycja Społecznościowa", "licenseAboutDescription": "Dotyczy to przedsiębiorstw i przedsiębiorstw, którzy stosują Pangolin w środowisku handlowym. Jeśli używasz Pangolin do użytku osobistego, możesz zignorować tę sekcję.", "licenseKeyActivated": "Klucz licencyjny aktywowany", @@ -509,9 +566,12 @@ "userSaved": "Użytkownik zapisany", "userSavedDescription": "Użytkownik został zaktualizowany.", "autoProvisioned": "Przesłane automatycznie", + "autoProvisionSettings": "Ustawienia automatycznego dostarczania", "autoProvisionedDescription": "Pozwól temu użytkownikowi na automatyczne zarządzanie przez dostawcę tożsamości", "accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji", "accessControlsSubmit": "Zapisz kontrole dostępu", + "singleRolePerUserPlanNotice": "Twój plan obsługuje tylko jedną rolę na użytkownika.", + "singleRolePerUserEditionNotice": "Ta edycja obsługuje tylko jedną rolę na użytkownika.", "roles": "Role", "accessUsersRoles": "Zarządzaj użytkownikami i rolami", "accessUsersRolesDescription": "Zaproś użytkowników i dodaj je do ról do zarządzania dostępem do organizacji", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu", "targetErrorNoSite": "Nie wybrano witryny", "targetErrorNoSiteDescription": "Wybierz witrynę docelową", + "targetTargetsCleared": "Cele wyczyszczone", + "targetTargetsClearedDescription": "Wszystkie cele zostały usunięte z tego zasobu", "targetCreated": "Cel utworzony", "targetCreatedDescription": "Cel został utworzony pomyślnie", "targetErrorCreate": "Nie udało się utworzyć celu", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.", "setupTokenRequired": "Wymagany jest token konfiguracji", "actionUpdateSite": "Aktualizuj witrynę", + "actionResetSiteBandwidth": "Zresetuj przepustowość organizacji", "actionListSiteRoles": "Lista dozwolonych ról witryny", "actionCreateResource": "Utwórz zasób", "actionDeleteResource": "Usuń zasób", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Usuń użytkownika", "actionListUsers": "Lista użytkowników", "actionAddUserRole": "Dodaj rolę użytkownika", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Ustaw role użytkownika", "actionGenerateAccessToken": "Wygeneruj token dostępu", "actionDeleteAccessToken": "Usuń token dostępu", "actionListAccessTokens": "Lista tokenów dostępu", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Role", "sidebarShareableLinks": "Linki", "sidebarApiKeys": "Klucze API", + "sidebarProvisioning": "Dostarczanie", "sidebarSettings": "Ustawienia", "sidebarAllUsers": "Wszyscy użytkownicy", "sidebarIdentityProviders": "Dostawcy tożsamości", @@ -1890,6 +1954,40 @@ "exitNode": "Węzeł Wyjściowy", "country": "Kraj", "rulesMatchCountry": "Obecnie bazuje na adresie IP źródła", + "region": "Region", + "selectRegion": "Wybierz region", + "searchRegions": "Szukaj regionów...", + "noRegionFound": "Nie znaleziono regionu.", + "rulesMatchRegion": "Wybierz regionalną grupę krajów", + "rulesErrorInvalidRegion": "Nieprawidłowy region", + "rulesErrorInvalidRegionDescription": "Proszę wybrać prawidłowy region.", + "regionAfrica": "Afryka", + "regionNorthernAfrica": "Afryka Północna", + "regionEasternAfrica": "Afryka Wschodnia", + "regionMiddleAfrica": "Afryka Środkowa", + "regionSouthernAfrica": "Afryka Południowa", + "regionWesternAfrica": "Afryka Zachodnia", + "regionAmericas": "Ameryka", + "regionCaribbean": "Karaiby", + "regionCentralAmerica": "Ameryka Środkowa", + "regionSouthAmerica": "Ameryka Południowej", + "regionNorthernAmerica": "Ameryka Północna", + "regionAsia": "Akwakultura", + "regionCentralAsia": "Azja Środkowa", + "regionEasternAsia": "Azja Wschodnia", + "regionSouthEasternAsia": "Azja Południowo-Wschodnia", + "regionSouthernAsia": "Azja Południowa", + "regionWesternAsia": "Azja Zachodnia", + "regionEurope": "Europa", + "regionEasternEurope": "Europa Wschodnia", + "regionNorthernEurope": "Europa Północna", + "regionSouthernEurope": "Europa Południowa", + "regionWesternEurope": "Europa Zachodnia", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Australia i Nowa Zelandia", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Zarządzane Samodzielnie-Hostingowane", "description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami", @@ -1938,6 +2036,25 @@ "invalidValue": "Nieprawidłowa wartość", "idpTypeLabel": "Typ dostawcy tożsamości", "roleMappingExpressionPlaceholder": "np. zawiera(grupy, 'admin') && 'Admin' || 'Członek'", + "roleMappingModeFixedRoles": "Stałe role", + "roleMappingModeMappingBuilder": "Konstruktor mapowania", + "roleMappingModeRawExpression": "Surowe wyrażenie", + "roleMappingFixedRolesPlaceholderSelect": "Wybierz jedną lub więcej ról", + "roleMappingFixedRolesPlaceholderFreeform": "Wpisz nazwy ról (dopasowanie na organizację)", + "roleMappingFixedRolesDescriptionSameForAll": "Przypisz tę samą rolę do każdego automatycznie udostępnionego użytkownika.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "W przypadku domyślnych zasad nazwy ról typu które istnieją w każdej organizacji, gdzie użytkownicy są zapisywani. Nazwy muszą się dokładnie zgadzać.", + "roleMappingClaimPath": "Ścieżka przejęcia", + "roleMappingClaimPathPlaceholder": "grupy", + "roleMappingClaimPathDescription": "Ścieżka w payloadzie tokenów, która zawiera wartości źródłowe (np. grupy).", + "roleMappingMatchValue": "Wartość dopasowania", + "roleMappingAssignRoles": "Przypisz role", + "roleMappingAddMappingRule": "Dodaj regułę mapowania", + "roleMappingRawExpressionResultDescription": "Wyrażenie musi ocenić do tablicy ciągów lub ciągów.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Wyrażenie musi oceniać ciąg znaków (pojedyncza nazwa).", + "roleMappingMatchValuePlaceholder": "Wartość dopasowania (na przykład: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Wpisz nazwy ról (aktywizacja na org)", + "roleMappingBuilderFreeformRowHint": "Nazwy ról muszą pasować do roli w każdej organizacji docelowej.", + "roleMappingRemoveRule": "Usuń", "idpGoogleConfiguration": "Konfiguracja Google", "idpGoogleConfigurationDescription": "Skonfiguruj dane logowania Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji", "domainPickerProvidedDomain": "Dostarczona domena", "domainPickerFreeProvidedDomain": "Darmowa oferowana domena", + "domainPickerFreeDomainsPaidFeature": "Dostarczane domeny to funkcja płatna. Subskrybuj, aby uzyskać domenę w ramach swojego planu — nie ma potrzeby przynoszenia własnej.", "domainPickerVerified": "Zweryfikowano", "domainPickerUnverified": "Niezweryfikowane", + "domainPickerManual": "Podręcznik", "domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.", "domainPickerError": "Błąd", "domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji", @@ -2235,7 +2354,7 @@ "description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe." } }, - "personalUseOnly": "Wyłącznie do użytku osobistego (bezpłatna licencja – brak zamówień)", + "personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)", "buttons": { "continueToCheckout": "Przejdź do zamówienia" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Jak długo zachować dzienniki dostępu", "logRetentionActionLabel": "Zachowanie dziennika akcji", "logRetentionActionDescription": "Jak długo zachować dzienniki akcji", + "logRetentionConnectionLabel": "Zachowanie dziennika połączeń", + "logRetentionConnectionDescription": "Jak długo zachować dzienniki połączeń", "logRetentionDisabled": "Wyłączone", "logRetention3Days": "3 dni", "logRetention7Days": "7 dni", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Koniec następnego roku", "actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji", "accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji", + "connectionLogs": "Dzienniki połączeń", + "connectionLogsDescription": "Wyświetl dzienniki połączeń dla tuneli w tej organizacji", + "sidebarLogsConnection": "Dzienniki połączeń", + "sidebarLogsStreaming": "Strumieniowanie", + "sourceAddress": "Adres źródłowy", + "destinationAddress": "Adres docelowy", + "duration": "Czas trwania", "licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja Enterprise Edition lub Pangolin Cloud . Zarezerwuj wersję demonstracyjną lub wersję próbną POC.", "ossEnterpriseEditionRequired": "Enterprise Edition jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w Pangolin Cloud. Zarezerwuj demo lub okres próbny POC.", "certResolver": "Rozwiązywanie certyfikatów", @@ -2487,6 +2615,9 @@ "machineClients": "Klienci maszyn", "install": "Zainstaluj", "run": "Uruchom", + "envFile": "Plik środowiska", + "serviceFile": "Plik serwisu", + "enableAndStart": "Włącz i Uruchom", "clientNameDescription": "Wyświetlana nazwa klienta, która może zostać zmieniona później.", "clientAddress": "Adres klienta (Zaawansowany)", "setupFailedToFetchSubnet": "Nie udało się pobrać domyślnej podsieci", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Edytuj rolę i włącz opcję \"Wymagaj zatwierdzenia urządzenia\". Użytkownicy z tą rolą będą potrzebowali zatwierdzenia administratora dla nowych urządzeń.", "approvalsEmptyStatePreviewDescription": "Podgląd: Gdy włączone, oczekujące prośby o sprawdzenie pojawią się tutaj", "approvalsEmptyStateButtonText": "Zarządzaj rolami", - "domainErrorTitle": "Mamy problem z weryfikacją Twojej domeny" + "domainErrorTitle": "Mamy problem z weryfikacją Twojej domeny", + "idpAdminAutoProvisionPoliciesTabHint": "Skonfiguruj mapowanie ról i zasady organizacji na karcie Auto Provivision Settings.", + "streamingTitle": "Strumieniowanie wydarzeń", + "streamingDescription": "Wydarzenia strumieniowe z Twojej organizacji do zewnętrznych miejsc przeznaczenia w czasie rzeczywistym.", + "streamingUnnamedDestination": "Miejsce przeznaczenia bez nazwy", + "streamingNoUrlConfigured": "Brak skonfigurowanego adresu URL", + "streamingAddDestination": "Dodaj cel", + "streamingHttpWebhookTitle": "Webhook HTTP", + "streamingHttpWebhookDescription": "Wyślij zdarzenia do dowolnego punktu końcowego HTTP z elastycznym uwierzytelnianiem i szablonem.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Zdarzenia strumieniowe do magazynu obiektów kompatybilnych z S3. Już wkrótce.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.", + "streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.", + "streamingFailedToLoad": "Nie udało się załadować miejsc docelowych", + "streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.", + "streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego", + "streamingDeletedSuccess": "Cel usunięty pomyślnie", + "streamingFailedToDelete": "Nie udało się usunąć miejsca docelowego", + "streamingDeleteTitle": "Usuń cel", + "streamingDeleteButtonText": "Usuń cel", + "streamingDeleteDialogAreYouSure": "Czy na pewno chcesz usunąć", + "streamingDeleteDialogThisDestination": "ten cel", + "streamingDeleteDialogPermanentlyRemoved": "? Wszystkie konfiguracje zostaną trwale usunięte.", + "httpDestEditTitle": "Edytuj cel", + "httpDestAddTitle": "Dodaj cel HTTP", + "httpDestEditDescription": "Aktualizuj konfigurację dla tego celu przesyłania strumieniowego zdarzeń HTTP.", + "httpDestAddDescription": "Skonfiguruj nowy punkt końcowy HTTP, aby otrzymywać wydarzenia organizacji.", + "httpDestTabSettings": "Ustawienia", + "httpDestTabHeaders": "Nagłówki", + "httpDestTabBody": "Ciało", + "httpDestTabLogs": "Logi", + "httpDestNamePlaceholder": "Mój cel HTTP", + "httpDestUrlLabel": "Adres docelowy", + "httpDestUrlErrorHttpRequired": "Adres URL musi używać http lub https", + "httpDestUrlErrorHttpsRequired": "HTTPS jest wymagany dla wdrożenia w chmurze", + "httpDestUrlErrorInvalid": "Wprowadź poprawny adres URL (np. https://example.com/webhook)", + "httpDestAuthTitle": "Uwierzytelnianie", + "httpDestAuthDescription": "Wybierz sposób uwierzytelniania żądań do Twojego punktu końcowego.", + "httpDestAuthNoneTitle": "Brak uwierzytelniania", + "httpDestAuthNoneDescription": "Wysyła żądania bez nagłówka autoryzacji.", + "httpDestAuthBearerTitle": "Token Bearer", + "httpDestAuthBearerDescription": "Dodaje nagłówek Authorization: Bearer '' do każdego żądania.", + "httpDestAuthBearerPlaceholder": "Twój klucz API lub token", + "httpDestAuthBasicTitle": "Podstawowa Autoryzacja", + "httpDestAuthBasicDescription": "Dodaje nagłówek Authorization: Basic ''. Podaj poświadczenia w formacie użytkownik:hasło.", + "httpDestAuthBasicPlaceholder": "Nazwa użytkownika:hasło", + "httpDestAuthCustomTitle": "Niestandardowy nagłówek", + "httpDestAuthCustomDescription": "Określ niestandardową nazwę nagłówka HTTP i wartość dla uwierzytelniania (np. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Nazwa nagłówka (np. klucz X-API)", + "httpDestAuthCustomHeaderValuePlaceholder": "Wartość nagłówka", + "httpDestCustomHeadersTitle": "Niestandardowe nagłówki HTTP", + "httpDestCustomHeadersDescription": "Dodaj własne nagłówki do każdego wychodzącego żądania. Przydatne dla tokenów statycznych lub niestandardowego typu zawartości. Domyślnie Content-Type: aplikacja/json jest wysyłane.", + "httpDestNoHeadersConfigured": "Nie skonfigurowano nagłówków niestandardowych. Kliknij \"Dodaj nagłówek\", aby go dodać.", + "httpDestHeaderNamePlaceholder": "Nazwa nagłówka", + "httpDestHeaderValuePlaceholder": "Wartość", + "httpDestAddHeader": "Dodaj nagłówek", + "httpDestBodyTemplateTitle": "Własny szablon ciała", + "httpDestBodyTemplateDescription": "Kontroluj strukturę JSON wysyłaną do Twojego punktu końcowego. Jeśli wyłączone, dla każdego zdarzenia wysyłany jest domyślny obiekt JSON.", + "httpDestEnableBodyTemplate": "Włącz niestandardowy szablon ciała", + "httpDestBodyTemplateLabel": "Szablon ciała (JSON)", + "httpDestBodyTemplateHint": "Użyj zmiennych szablonu do odniesienia pól zdarzeń w twoim payloadzie.", + "httpDestPayloadFormatTitle": "Format obciążenia", + "httpDestPayloadFormatDescription": "Jak zdarzenia są serializowane w każdym organie żądania.", + "httpDestFormatJsonArrayTitle": "Tablica JSON", + "httpDestFormatJsonArrayDescription": "Jedna prośba na partię, treść jest tablicą JSON. Kompatybilna z najbardziej ogólnymi webhookami i Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Jedno żądanie na partię, ciałem jest plik JSON rozdzielony na newline-delimited — jeden obiekt na wiersz, bez tablicy zewnętrznej. Wymagane przez Splunk HEC, Elastic / OpenSesearch i Grafana Loki.", + "httpDestFormatSingleTitle": "Jedno wydarzenie na żądanie", + "httpDestFormatSingleDescription": "Wysyła oddzielny POST HTTP dla każdego zdarzenia. Użyj tylko dla punktów końcowych, które nie mogą obsługiwać partii.", + "httpDestLogTypesTitle": "Typy logów", + "httpDestLogTypesDescription": "Wybierz, które typy logów są przekazywane do tego miejsca docelowego. Tylko włączone typy logów będą strumieniowane.", + "httpDestAccessLogsTitle": "Logi dostępu", + "httpDestAccessLogsDescription": "Próby dostępu do zasobów, w tym uwierzytelnione i odrzucone żądania.", + "httpDestActionLogsTitle": "Dzienniki działań", + "httpDestActionLogsDescription": "Działania administracyjne wykonywane przez użytkowników w organizacji.", + "httpDestConnectionLogsTitle": "Dzienniki połączeń", + "httpDestConnectionLogsDescription": "Zdarzenia związane z miejscem i tunelem, w tym połączenia i rozłączenia.", + "httpDestRequestLogsTitle": "Dzienniki żądań", + "httpDestRequestLogsDescription": "Logi żądań HTTP dla zasobów proxy, w tym metody, ścieżki i kodu odpowiedzi.", + "httpDestSaveChanges": "Zapisz zmiany", + "httpDestCreateDestination": "Utwórz cel", + "httpDestUpdatedSuccess": "Cel został pomyślnie zaktualizowany", + "httpDestCreatedSuccess": "Cel został utworzony pomyślnie", + "httpDestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego", + "httpDestCreateFailed": "Nie udało się utworzyć miejsca docelowego" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index b121f4b16..8b36732d3 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -148,6 +148,11 @@ "createLink": "Criar Link", "resourcesNotFound": "Nenhum recurso encontrado", "resourceSearch": "Recursos de pesquisa", + "machineSearch": "Procurar máquinas", + "machinesSearch": "Pesquisar clientes de máquina...", + "machineNotFound": "Nenhuma máquina encontrada", + "userDeviceSearch": "Procurar dispositivos do usuário", + "userDevicesSearch": "Pesquisar dispositivos do usuário...", "openMenu": "Abrir menu", "resource": "Recurso", "title": "Título", @@ -323,6 +328,54 @@ "apiKeysDelete": "Excluir Chave API", "apiKeysManage": "Gerir Chaves API", "apiKeysDescription": "As chaves API são usadas para autenticar com a API de integração", + "provisioningKeysTitle": "Chave de provisionamento", + "provisioningKeysManage": "Gerenciar chaves de provisionamento", + "provisioningKeysDescription": "Chaves de provisionamento são usadas para autenticar o provisionamento automatizado do site para sua organização.", + "provisioningManage": "Provisionamento", + "provisioningDescription": "Gerenciar chaves de provisionamento e revisar sites pendentes aguardando aprovação.", + "pendingSites": "Sites pendentes", + "siteApproveSuccess": "Site aprovado com sucesso", + "siteApproveError": "Erro ao aprovar site", + "provisioningKeys": "Posicionando chaves", + "searchProvisioningKeys": "Pesquisar chaves de provisionamento...", + "provisioningKeysAdd": "Gerar chave de provisionamento", + "provisioningKeysErrorDelete": "Erro ao excluir chave de provisionamento", + "provisioningKeysErrorDeleteMessage": "Erro ao excluir chave de provisionamento", + "provisioningKeysQuestionRemove": "Tem certeza de que deseja remover esta chave de provisionamento da organização?", + "provisioningKeysMessageRemove": "Uma vez removida, a chave não pode mais ser usada para o provisionamento do site.", + "provisioningKeysDeleteConfirm": "Confirmar chave de exclusão", + "provisioningKeysDelete": "Apagar chave de provisionamento", + "provisioningKeysCreate": "Gerar chave de provisionamento", + "provisioningKeysCreateDescription": "Gerar uma nova chave de provisionamento para a organização", + "provisioningKeysSeeAll": "Ver todas as chaves provisionadas", + "provisioningKeysSave": "Salvar a chave de provisionamento", + "provisioningKeysSaveDescription": "Você só será capaz de ver esta vez. Copiá-lo para um lugar seguro.", + "provisioningKeysErrorCreate": "Erro ao criar chave de provisionamento", + "provisioningKeysList": "Nova chave de aprovisionamento", + "provisioningKeysMaxBatchSize": "Tamanho máximo do lote", + "provisioningKeysUnlimitedBatchSize": "Tamanho ilimitado em lote (sem limite)", + "provisioningKeysMaxBatchUnlimited": "Ilimitado", + "provisioningKeysMaxBatchSizeInvalid": "Informe um tamanho máximo válido em lote (1–1,000,000).", + "provisioningKeysValidUntil": "Valido ate", + "provisioningKeysValidUntilHint": "Deixe em branco para nenhuma expiração.", + "provisioningKeysValidUntilInvalid": "Informe uma data e hora válidas.", + "provisioningKeysNumUsed": "Use percentual", + "provisioningKeysLastUsed": "Última utilização", + "provisioningKeysNoExpiry": "Sem vencimento", + "provisioningKeysNeverUsed": "nunca", + "provisioningKeysEdit": "Editar chave de provisionamento", + "provisioningKeysEditDescription": "Atualizar o tamanho máximo do lote e tempo de expiração para esta chave.", + "provisioningKeysApproveNewSites": "Aprovar novos sites", + "provisioningKeysApproveNewSitesDescription": "Aprovar automaticamente sites que se registram com esta chave.", + "provisioningKeysUpdateError": "Erro ao atualizar chave de provisionamento", + "provisioningKeysUpdated": "Chave de provisionamento atualizada", + "provisioningKeysUpdatedDescription": "Suas alterações foram salvas.", + "provisioningKeysBannerTitle": "Chaves de provisionamento do site", + "provisioningKeysBannerDescription": "Gere uma chave de provisionamento e use-a com o conector Newt para criar sites automaticamente na primeira inicialização - sem necessidade de configurar credenciais separadas para cada site.", + "provisioningKeysBannerButtonText": "Saiba mais", + "pendingSitesBannerTitle": "Sites pendentes", + "pendingSitesBannerDescription": "Sites que se conectam usando uma chave de provisionamento aparecem aqui para revisão.", + "pendingSitesBannerButtonText": "Saiba mais", "apiKeysSettings": "Configurações de {apiKeyName}", "userTitle": "Gerir Todos os Utilizadores", "userDescription": "Visualizar e gerir todos os utilizadores no sistema", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Falha ao ativar a chave de licença", "licenseErrorKeyActivateDescription": "Ocorreu um erro ao ativar a chave da licença.", "licenseAbout": "Sobre Licenciamento", + "licenseBannerTitle": "Ative Sua Licença Corporativa", + "licenseBannerDescription": "Desbloqueie recursos empresariais para sua instância de Pangolin autohospedada. Compre uma chave de licença para ativar recursos premium e adicione-a abaixo.", + "licenseBannerGetLicense": "Obter Licença", + "licenseBannerViewDocs": "Ver Documentação", "communityEdition": "Edição da Comunidade", "licenseAboutDescription": "Isto destina-se aos utilizadores empresariais e empresariais que estão a usar o Pangolin num ambiente comercial. Se você estiver usando o Pangolin para uso pessoal, você pode ignorar esta seção.", "licenseKeyActivated": "Chave de licença ativada", @@ -509,9 +566,12 @@ "userSaved": "Usuário salvo", "userSavedDescription": "O utilizador foi atualizado.", "autoProvisioned": "Auto provisionado", + "autoProvisionSettings": "Configurações de provisão automática", "autoProvisionedDescription": "Permitir que este utilizador seja gerido automaticamente pelo provedor de identidade", "accessControlsDescription": "Gerir o que este utilizador pode aceder e fazer na organização", "accessControlsSubmit": "Guardar Controlos de Acesso", + "singleRolePerUserPlanNotice": "Seu plano suporta apenas uma função por usuário.", + "singleRolePerUserEditionNotice": "Esta edição suporta apenas uma função por usuário.", "roles": "Funções", "accessUsersRoles": "Gerir Utilizadores e Funções", "accessUsersRolesDescription": "Convidar usuários e adicioná-los a funções para gerenciar o acesso à organização", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido", "targetErrorNoSite": "Nenhum site selecionado", "targetErrorNoSiteDescription": "Selecione um site para o destino", + "targetTargetsCleared": "Alvos limpos", + "targetTargetsClearedDescription": "Todos os alvos foram removidos deste recurso", "targetCreated": "Destino criado", "targetCreatedDescription": "O alvo foi criado com sucesso", "targetErrorCreate": "Falha ao criar destino", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Digite o token de configuração do console do servidor.", "setupTokenRequired": "Token de configuração é necessário", "actionUpdateSite": "Atualizar Site", + "actionResetSiteBandwidth": "Redefinir banda da organização", "actionListSiteRoles": "Listar Funções Permitidas do Site", "actionCreateResource": "Criar Recurso", "actionDeleteResource": "Eliminar Recurso", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Remover Utilizador", "actionListUsers": "Listar Utilizadores", "actionAddUserRole": "Adicionar Função ao Utilizador", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Definir funções do usuário", "actionGenerateAccessToken": "Gerar Token de Acesso", "actionDeleteAccessToken": "Eliminar Token de Acesso", "actionListAccessTokens": "Listar Tokens de Acesso", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Papéis", "sidebarShareableLinks": "Links", "sidebarApiKeys": "Chaves API", + "sidebarProvisioning": "Provisionamento", "sidebarSettings": "Configurações", "sidebarAllUsers": "Todos os utilizadores", "sidebarIdentityProviders": "Provedores de identidade", @@ -1890,6 +1954,40 @@ "exitNode": "Nodo de Saída", "country": "País", "rulesMatchCountry": "Atualmente baseado no IP de origem", + "region": "Região", + "selectRegion": "Selecionar região", + "searchRegions": "Procurar regiões...", + "noRegionFound": "Nenhuma região encontrada.", + "rulesMatchRegion": "Selecione um grupo regional de países", + "rulesErrorInvalidRegion": "Região inválida", + "rulesErrorInvalidRegionDescription": "Por favor, selecione uma região válida.", + "regionAfrica": "África", + "regionNorthernAfrica": "África do Norte", + "regionEasternAfrica": "África Oriental", + "regionMiddleAfrica": "África Média", + "regionSouthernAfrica": "África Austral", + "regionWesternAfrica": "África Ocidental", + "regionAmericas": "Américas", + "regionCaribbean": "Caribe", + "regionCentralAmerica": "América Central", + "regionSouthAmerica": "América do Sul", + "regionNorthernAmerica": "América do Norte", + "regionAsia": "Ásia", + "regionCentralAsia": "Ásia Central", + "regionEasternAsia": "Ásia Oriental", + "regionSouthEasternAsia": "Sudeste da Ásia", + "regionSouthernAsia": "Sudeste da Ásia", + "regionWesternAsia": "Ásia Ocidental", + "regionEurope": "Europa", + "regionEasternEurope": "Europa Oriental", + "regionNorthernEurope": "Europa do Norte", + "regionSouthernEurope": "Europa do Sul", + "regionWesternEurope": "Europa Ocidental", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "Austrália e Nova Zelândia", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "Gerenciado Auto-Hospedado", "description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos", @@ -1938,6 +2036,25 @@ "invalidValue": "Valor Inválido", "idpTypeLabel": "Tipo de provedor de identidade", "roleMappingExpressionPlaceholder": "ex.: Contem (grupos, 'administrador') && 'Administrador' 「'Membro'", + "roleMappingModeFixedRoles": "Papéis fixos", + "roleMappingModeMappingBuilder": "Mapeando Construtor", + "roleMappingModeRawExpression": "Expressão Bruta", + "roleMappingFixedRolesPlaceholderSelect": "Selecione um ou mais papéis", + "roleMappingFixedRolesPlaceholderFreeform": "Digite o nome das funções (correspondência exata por organização)", + "roleMappingFixedRolesDescriptionSameForAll": "Atribuir o mesmo conjunto de funções a cada usuário auto-provisionado.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Para políticas padrão, nomes de funções de tipo que existem em cada organização onde os usuários são fornecidos. Nomes devem coincidir exatamente.", + "roleMappingClaimPath": "Caminho da Reivindicação", + "roleMappingClaimPathPlaceholder": "grupos", + "roleMappingClaimPathDescription": "Caminho no payload token que contém valores de origem (por exemplo, grupos).", + "roleMappingMatchValue": "Valor Correspondente", + "roleMappingAssignRoles": "Atribuir Papéis", + "roleMappingAddMappingRule": "Adicionar regra de mapeamento", + "roleMappingRawExpressionResultDescription": "Expressão deve retornar à matriz string ou string.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Expressão deve ser avaliada para uma string (um nome de função única).", + "roleMappingMatchValuePlaceholder": "Valor do jogo (por exemplo: administrador)", + "roleMappingAssignRolesPlaceholderFreeform": "Digite nomes de funções ((exact por org)", + "roleMappingBuilderFreeformRowHint": "Nomes de papéis devem corresponder a um papel em cada organizaçãoalvo.", + "roleMappingRemoveRule": "Remover", "idpGoogleConfiguration": "Configuração do Google", "idpGoogleConfigurationDescription": "Configurar as credenciais do Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Selecione um domínio para a página de autenticação da organização", "domainPickerProvidedDomain": "Domínio fornecido", "domainPickerFreeProvidedDomain": "Domínio fornecido grátis", + "domainPickerFreeDomainsPaidFeature": "Os domínios fornecidos são um recurso pago. Assine para obter um domínio incluído no seu plano — não há necessidade de trazer o seu próprio.", "domainPickerVerified": "Verificada", "domainPickerUnverified": "Não verificado", + "domainPickerManual": "Manual", "domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.", "domainPickerError": "ERRO", "domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização", @@ -2235,7 +2354,7 @@ "description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário." } }, - "personalUseOnly": "Apenas uso pessoal (licença gratuita — sem check-out)", + "personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)", "buttons": { "continueToCheckout": "Continuar com checkout" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Por quanto tempo manter os registros de acesso", "logRetentionActionLabel": "Ação de Retenção no Log", "logRetentionActionDescription": "Por quanto tempo manter os registros de ação", + "logRetentionConnectionLabel": "Retenção de registro de conexão", + "logRetentionConnectionDescription": "Por quanto tempo manter os registros de conexão", "logRetentionDisabled": "Desabilitado", "logRetention3Days": "3 dias", "logRetention7Days": "7 dias", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Fim do ano seguinte", "actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização", "accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização", + "connectionLogs": "Logs da conexão", + "connectionLogsDescription": "Ver logs de conexão para túneis nesta organização", + "sidebarLogsConnection": "Logs da conexão", + "sidebarLogsStreaming": "Transmitindo", + "sourceAddress": "Endereço de origem", + "destinationAddress": "Endereço de destino", + "duration": "Duração", "licenseRequiredToUse": "Uma licença Enterprise Edition ou Pangolin Cloud é necessária para usar este recurso. Reserve um teste de demonstração ou POC.", "ossEnterpriseEditionRequired": "O Enterprise Edition é necessário para usar este recurso. Este recurso também está disponível no Pangolin Cloud. Reserve uma demonstração ou avaliação POC.", "certResolver": "Resolvedor de Certificado", @@ -2487,6 +2615,9 @@ "machineClients": "Clientes de máquina", "install": "Instale", "run": "Executar", + "envFile": "Arquivo de Ambiente", + "serviceFile": "Arquivo de Serviço", + "enableAndStart": "Ativar e Iniciar", "clientNameDescription": "O nome de exibição do cliente que pode ser alterado mais tarde.", "clientAddress": "Endereço do Cliente (Avançado)", "setupFailedToFetchSubnet": "Falha ao buscar a subrede padrão", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Editar uma função e habilitar a opção 'Exigir aprovação de dispositivos'. Usuários com essa função precisarão de aprovação de administrador para novos dispositivos.", "approvalsEmptyStatePreviewDescription": "Pré-visualização: Quando ativado, solicitações de dispositivo pendentes aparecerão aqui para revisão", "approvalsEmptyStateButtonText": "Gerir Funções", - "domainErrorTitle": "Estamos tendo problemas ao verificar seu domínio" + "domainErrorTitle": "Estamos tendo problemas ao verificar seu domínio", + "idpAdminAutoProvisionPoliciesTabHint": "Configurar funções de mapeamento e políticas de organização na aba Auto Provision Settings.", + "streamingTitle": "Streaming do Evento", + "streamingDescription": "Transmita eventos de sua organização para destinos externos em tempo real.", + "streamingUnnamedDestination": "Destino sem nome", + "streamingNoUrlConfigured": "Nenhuma URL configurada", + "streamingAddDestination": "Adicionar destino", + "streamingHttpWebhookTitle": "Webhook HTTP", + "streamingHttpWebhookDescription": "Envie os eventos para qualquer endpoint HTTP com autenticação flexível e modelo.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Transmitir eventos para um balde de armazenamento de objetos compatível com S3. Em breve.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.", + "streamingTypePickerDescription": "Escolha um tipo de destino para começar.", + "streamingFailedToLoad": "Falha ao carregar destinos", + "streamingUnexpectedError": "Ocorreu um erro inesperado.", + "streamingFailedToUpdate": "Falha ao atualizar destino", + "streamingDeletedSuccess": "Destino apagado com sucesso", + "streamingFailedToDelete": "Falha ao excluir destino", + "streamingDeleteTitle": "Excluir destino", + "streamingDeleteButtonText": "Excluir destino", + "streamingDeleteDialogAreYouSure": "Tem certeza de que deseja excluir", + "streamingDeleteDialogThisDestination": "este destino", + "streamingDeleteDialogPermanentlyRemoved": "? Todas as configurações serão permanentemente removidas.", + "httpDestEditTitle": "Editar destino", + "httpDestAddTitle": "Adicionar Destino HTTP", + "httpDestEditDescription": "Atualizar a configuração para este destino de transmissão de eventos HTTP.", + "httpDestAddDescription": "Configure um novo ponto de extremidade HTTP para receber eventos da sua organização.", + "httpDestTabSettings": "Confirgurações", + "httpDestTabHeaders": "Cabeçalhos", + "httpDestTabBody": "Conteúdo", + "httpDestTabLogs": "Registros", + "httpDestNamePlaceholder": "Meu destino HTTP", + "httpDestUrlLabel": "URL de destino", + "httpDestUrlErrorHttpRequired": "A URL deve usar http ou https", + "httpDestUrlErrorHttpsRequired": "HTTPS é necessário em implantações em nuvem", + "httpDestUrlErrorInvalid": "Informe uma URL válida (por exemplo, https://example.com/webhook)", + "httpDestAuthTitle": "Autenticação", + "httpDestAuthDescription": "Escolha como os pedidos para seu endpoint são autenticados.", + "httpDestAuthNoneTitle": "Sem Autenticação", + "httpDestAuthNoneDescription": "Envia pedidos sem um cabeçalho de autorização.", + "httpDestAuthBearerTitle": "Token do portador", + "httpDestAuthBearerDescription": "Adiciona um cabeçalho Authorization: Bearer '' a cada solicitação.", + "httpDestAuthBearerPlaceholder": "Sua chave de API ou token", + "httpDestAuthBasicTitle": "Autenticação básica", + "httpDestAuthBasicDescription": "Adiciona um cabeçalho Authorization: Basic ''. Forneça as credenciais como username:password.", + "httpDestAuthBasicPlaceholder": "Usuário:password", + "httpDestAuthCustomTitle": "Cabeçalho personalizado", + "httpDestAuthCustomDescription": "Especifique um nome e valor de cabeçalho HTTP personalizado para autenticação (por exemplo, X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Nome do cabeçalho (ex: X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Valor do cabeçalho", + "httpDestCustomHeadersTitle": "Cabeçalhos HTTP personalizados", + "httpDestCustomHeadersDescription": "Adicionar cabeçalhos personalizados a todas as solicitações de saída. Útil para tokens estáticos ou um tipo de conteúdo personalizado. Por padrão, Content-Type: application/json é enviado.", + "httpDestNoHeadersConfigured": "Nenhum cabeçalho personalizado configurado. Clique em \"Adicionar Cabeçalho\" para adicionar um.", + "httpDestHeaderNamePlaceholder": "Nome do Cabeçalho", + "httpDestHeaderValuePlaceholder": "Valor", + "httpDestAddHeader": "Adicionar Cabeçalho", + "httpDestBodyTemplateTitle": "Modelo de corpo personalizado", + "httpDestBodyTemplateDescription": "Controla a estrutura de carga JSON enviada ao seu endpoint. Se desativado, um objeto JSON padrão é enviado para cada evento.", + "httpDestEnableBodyTemplate": "Ativar modelo personalizado de corpo", + "httpDestBodyTemplateLabel": "Modelo de corpo (JSON)", + "httpDestBodyTemplateHint": "Use variáveis de template para referenciar campos de evento em seu payload.", + "httpDestPayloadFormatTitle": "Formato de carga", + "httpDestPayloadFormatDescription": "Como os eventos são serializados em cada corpo do pedido.", + "httpDestFormatJsonArrayTitle": "Matriz JSON", + "httpDestFormatJsonArrayDescription": "Um pedido por lote, o corpo é um array JSON. Compatível com a maioria dos webhooks genéricos e Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Um pedido por lote, o corpo é um JSON delimitado por nova-linha — um objeto por linha, sem array exterior. Requerido pelo Splunk HEC, Elástico / OpenSearch, e Grafana Loki.", + "httpDestFormatSingleTitle": "Um Evento por Requisição", + "httpDestFormatSingleDescription": "Envia um POST HTTP separado para cada evento. Utilize apenas para endpoints que não podem manipular lotes.", + "httpDestLogTypesTitle": "Tipos de log", + "httpDestLogTypesDescription": "Escolha quais tipos de log são encaminhados para este destino. Somente serão racionalizados os tipos de logs habilitados.", + "httpDestAccessLogsTitle": "Logs de Acesso", + "httpDestAccessLogsDescription": "Tentativas de acesso a recursos, incluindo solicitações autenticadas e negadas.", + "httpDestActionLogsTitle": "Logs de Ações", + "httpDestActionLogsDescription": "Ações administrativas realizadas por usuários dentro da organização.", + "httpDestConnectionLogsTitle": "Logs da conexão", + "httpDestConnectionLogsDescription": "Eventos de conexão de site e túnel, incluindo conexões e desconexões.", + "httpDestRequestLogsTitle": "Registro de pedidos", + "httpDestRequestLogsDescription": "Logs de solicitação HTTP para recursos proxy incluindo o método, o caminho e o código de resposta.", + "httpDestSaveChanges": "Salvar as alterações", + "httpDestCreateDestination": "Criar destino", + "httpDestUpdatedSuccess": "Destino atualizado com sucesso", + "httpDestCreatedSuccess": "Destino criado com sucesso", + "httpDestUpdateFailed": "Falha ao atualizar destino", + "httpDestCreateFailed": "Falha ao criar destino" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 0d42245e4..12a285100 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -148,6 +148,11 @@ "createLink": "Создать ссылку", "resourcesNotFound": "Ресурсы не найдены", "resourceSearch": "Поиск ресурсов", + "machineSearch": "Поиск машин", + "machinesSearch": "Поиск клиентов машины...", + "machineNotFound": "Машины не найдены", + "userDeviceSearch": "Поиск устройств пользователя", + "userDevicesSearch": "Поиск устройств пользователя...", "openMenu": "Открыть меню", "resource": "Ресурс", "title": "Заголовок", @@ -323,6 +328,54 @@ "apiKeysDelete": "Удаление ключа API", "apiKeysManage": "Управление ключами API", "apiKeysDescription": "Ключи API используются для аутентификации в интеграционном API", + "provisioningKeysTitle": "Ключ подготовки", + "provisioningKeysManage": "Управление ключами подготовки", + "provisioningKeysDescription": "Ключи подготовки используются для аутентификации автоматического обеспечения сайта для вашей организации.", + "provisioningManage": "Подготовка", + "provisioningDescription": "Управляйте предоставленными ключами и проверять непроверенные сайты, ожидающие утверждения.", + "pendingSites": "Ожидающие сайты", + "siteApproveSuccess": "Сайт успешно утвержден", + "siteApproveError": "Ошибка при утверждении сайта", + "provisioningKeys": "Ключи подготовки", + "searchProvisioningKeys": "Поиск подготовительных ключей...", + "provisioningKeysAdd": "Сгенерировать ключ подготовки", + "provisioningKeysErrorDelete": "Ошибка при удалении подготовительного ключа", + "provisioningKeysErrorDeleteMessage": "Ошибка при удалении подготовительного ключа", + "provisioningKeysQuestionRemove": "Вы уверены, что хотите удалить этот ключ подготовки из организации?", + "provisioningKeysMessageRemove": "После удаления ключ больше не может быть использован для размещения сайта.", + "provisioningKeysDeleteConfirm": "Подтвердите удаление ключа подготовки", + "provisioningKeysDelete": "Удалить ключ подготовки", + "provisioningKeysCreate": "Сгенерировать ключ подготовки", + "provisioningKeysCreateDescription": "Создать новый подготовительный ключ для организации", + "provisioningKeysSeeAll": "Посмотреть все подготовительные ключи", + "provisioningKeysSave": "Сохранить ключ подготовки", + "provisioningKeysSaveDescription": "Вы сможете увидеть это только один раз. Скопируйте его в безопасное место.", + "provisioningKeysErrorCreate": "Ошибка при создании ключа подготовки", + "provisioningKeysList": "Новый подготовительный ключ", + "provisioningKeysMaxBatchSize": "Макс. размер партии", + "provisioningKeysUnlimitedBatchSize": "Неограниченный размер партии (без ограничений)", + "provisioningKeysMaxBatchUnlimited": "Неограниченный", + "provisioningKeysMaxBatchSizeInvalid": "Введите максимальный размер пакета (1–1,000,000).", + "provisioningKeysValidUntil": "Действителен до", + "provisioningKeysValidUntilHint": "Оставьте пустым для отсутствия срока действия.", + "provisioningKeysValidUntilInvalid": "Введите правильную дату и время.", + "provisioningKeysNumUsed": "Использовано раз", + "provisioningKeysLastUsed": "Последнее использованное", + "provisioningKeysNoExpiry": "Без истечения срока", + "provisioningKeysNeverUsed": "Никогда", + "provisioningKeysEdit": "Редактировать ключ подготовки", + "provisioningKeysEditDescription": "Обновить максимальный размер и срок действия этого ключа.", + "provisioningKeysApproveNewSites": "Одобрить новые сайты", + "provisioningKeysApproveNewSitesDescription": "Автоматически одобрять сайты, регистрирующиеся с этим ключом.", + "provisioningKeysUpdateError": "Ошибка при обновлении ключа подготовки", + "provisioningKeysUpdated": "Ключ подготовки обновлен", + "provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.", + "provisioningKeysBannerTitle": "Ключи подготовки сайта", + "provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.", + "provisioningKeysBannerButtonText": "Узнать больше", + "pendingSitesBannerTitle": "Ожидающие сайты", + "pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.", + "pendingSitesBannerButtonText": "Узнать больше", "apiKeysSettings": "Настройки {apiKeyName}", "userTitle": "Управление всеми пользователями", "userDescription": "Просмотр и управление всеми пользователями в системе", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Не удалось активировать лицензионный ключ", "licenseErrorKeyActivateDescription": "Произошла ошибка при активации лицензионного ключа.", "licenseAbout": "О лицензировании", + "licenseBannerTitle": "Активируйте вашу корпоративную лицензию", + "licenseBannerDescription": "Откройте доступ к корпоративным функциям для вашей локально размещаемой версии Pangolin. Приобретите лицензионный ключ, чтобы активировать премиум-функции, затем добавьте его ниже.", + "licenseBannerGetLicense": "Получить лицензию", + "licenseBannerViewDocs": "Посмотреть документацию", "communityEdition": "Community Edition", "licenseAboutDescription": "Это для бизнес и корпоративных пользователей, использующих Pangolin в коммерческой среде. Если вы используете Pangolin для личного использования, вы можете игнорировать этот раздел.", "licenseKeyActivated": "Лицензионный ключ активирован", @@ -509,9 +566,12 @@ "userSaved": "Пользователь сохранён", "userSavedDescription": "Пользователь был обновлён.", "autoProvisioned": "Автоподбор", + "autoProvisionSettings": "Настройки автоматического обеспечения", "autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем", "accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации", "accessControlsSubmit": "Сохранить контроль доступа", + "singleRolePerUserPlanNotice": "Ваш план поддерживает только одну роль каждого пользователя.", + "singleRolePerUserEditionNotice": "Эта редакция поддерживает только одну роль для каждого пользователя.", "roles": "Роли", "accessUsersRoles": "Управление пользователями и ролями", "accessUsersRolesDescription": "Пригласить пользователей и добавить их в роли для управления доступом к организации", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта", "targetErrorNoSite": "Сайт не выбран", "targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели", + "targetTargetsCleared": "Цели очищены", + "targetTargetsClearedDescription": "Все цели удалены из этого ресурса", "targetCreated": "Цель создана", "targetCreatedDescription": "Цель была успешно создана", "targetErrorCreate": "Не удалось создать цель", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Введите токен настройки из консоли сервера.", "setupTokenRequired": "Токен настройки обязателен", "actionUpdateSite": "Обновить сайт", + "actionResetSiteBandwidth": "Сброс пропускной способности организации", "actionListSiteRoles": "Список разрешенных ролей сайта", "actionCreateResource": "Создать ресурс", "actionDeleteResource": "Удалить ресурс", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Удалить пользователя", "actionListUsers": "Список пользователей", "actionAddUserRole": "Добавить роль пользователя", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Установка ролей пользователей", "actionGenerateAccessToken": "Сгенерировать токен доступа", "actionDeleteAccessToken": "Удалить токен доступа", "actionListAccessTokens": "Список токенов доступа", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Роли", "sidebarShareableLinks": "Ссылки", "sidebarApiKeys": "API ключи", + "sidebarProvisioning": "Подготовка", "sidebarSettings": "Настройки", "sidebarAllUsers": "Все пользователи", "sidebarIdentityProviders": "Поставщики удостоверений", @@ -1890,6 +1954,40 @@ "exitNode": "Узел выхода", "country": "Страна", "rulesMatchCountry": "В настоящее время основано на исходном IP", + "region": "Регион", + "selectRegion": "Выберите регион", + "searchRegions": "Поиск регионов...", + "noRegionFound": "Регион не найден.", + "rulesMatchRegion": "Выберите региональную группу стран", + "rulesErrorInvalidRegion": "Некорректный регион", + "rulesErrorInvalidRegionDescription": "Пожалуйста, выберите корректный регион.", + "regionAfrica": "Африка", + "regionNorthernAfrica": "Северная Африка", + "regionEasternAfrica": "Восточная Африка", + "regionMiddleAfrica": "Центральная Африка", + "regionSouthernAfrica": "Южная Африка", + "regionWesternAfrica": "Западная Африка", + "regionAmericas": "Америка", + "regionCaribbean": "Карибы", + "regionCentralAmerica": "Центральная Америка", + "regionSouthAmerica": "Южная Америка", + "regionNorthernAmerica": "Северная Америка", + "regionAsia": "Азия", + "regionCentralAsia": "Центральная Азия", + "regionEasternAsia": "Восточная Азия", + "regionSouthEasternAsia": "Юго-Восточная Азия", + "regionSouthernAsia": "Южная Азия", + "regionWesternAsia": "Западная Азия", + "regionEurope": "Европа", + "regionEasternEurope": "Восточная Европа", + "regionNorthernEurope": "Северная Европа", + "regionSouthernEurope": "Южная Европа", + "regionWesternEurope": "Западная Европа", + "regionOceania": "Океания", + "regionAustraliaAndNewZealand": "Австралия и Новая Зеландия", + "regionMelanesia": "Меланезия", + "regionMicronesia": "Микронезия", + "regionPolynesia": "Полинезия", "managedSelfHosted": { "title": "Управляемый с самовывоза", "description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками", @@ -1938,6 +2036,25 @@ "invalidValue": "Неверное значение", "idpTypeLabel": "Тип поставщика удостоверений", "roleMappingExpressionPlaceholder": "например, contains(groups, 'admin') && 'Admin' || 'Member'", + "roleMappingModeFixedRoles": "Фиксированные роли", + "roleMappingModeMappingBuilder": "Сопоставляющий конструктор", + "roleMappingModeRawExpression": "Сырое выражение", + "roleMappingFixedRolesPlaceholderSelect": "Выберите одну или несколько ролей", + "roleMappingFixedRolesPlaceholderFreeform": "Тип имен ролей (точное совпадение по организации)", + "roleMappingFixedRolesDescriptionSameForAll": "Назначить одну и ту же роль, которая установлена каждому автообеспеченному пользователю.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Для политик по умолчанию, введите имена ролей, которые существуют в каждой организации, где пользователи предоставлены. Имена должны соответствовать точно.", + "roleMappingClaimPath": "Путь к заявлению", + "roleMappingClaimPathPlaceholder": "группы", + "roleMappingClaimPathDescription": "Путь в полезной нагрузке токенов, который содержит исходные значения (например, группы).", + "roleMappingMatchValue": "Значение матча", + "roleMappingAssignRoles": "Назначить роли", + "roleMappingAddMappingRule": "Добавить правило сопоставления", + "roleMappingRawExpressionResultDescription": "Выражение должно быть оценено к строке или строковому массиву.", + "roleMappingRawExpressionResultDescriptionSingleRole": "Выражение должно быть оценено строке (название одной роли).", + "roleMappingMatchValuePlaceholder": "Значение совпадения (например: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Введите имена ролей (точное по организациям)", + "roleMappingBuilderFreeformRowHint": "Имена ролей должны соответствовать роли в каждой целевой организации.", + "roleMappingRemoveRule": "Удалить", "idpGoogleConfiguration": "Конфигурация Google", "idpGoogleConfigurationDescription": "Настройка учетных данных Google OAuth2", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации", "domainPickerProvidedDomain": "Домен предоставлен", "domainPickerFreeProvidedDomain": "Бесплатный домен", + "domainPickerFreeDomainsPaidFeature": "Предоставленные домены являются платной функцией. Подпишитесь, чтобы получить домен, включенный в ваш план — не нужно приносить свой собственный.", "domainPickerVerified": "Подтверждено", "domainPickerUnverified": "Не подтверждено", + "domainPickerManual": "Ручной", "domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.", "domainPickerError": "Ошибка", "domainPickerErrorLoadDomains": "Не удалось загрузить домены организации", @@ -2235,7 +2354,7 @@ "description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка." } }, - "personalUseOnly": "Только для личного пользования (бесплатная лицензия — без оформления)", + "personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)", "buttons": { "continueToCheckout": "Продолжить оформление заказа" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Как долго сохранять журналы доступа", "logRetentionActionLabel": "Сохранение журнала действий", "logRetentionActionDescription": "Как долго хранить журналы действий", + "logRetentionConnectionLabel": "Сохранение журнала подключений", + "logRetentionConnectionDescription": "Как долго хранить журналы подключений", "logRetentionDisabled": "Отключено", "logRetention3Days": "3 дня", "logRetention7Days": "7 дней", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Конец следующего года", "actionLogsDescription": "Просмотр истории действий, выполненных в этой организации", "accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации", + "connectionLogs": "Журнал подключений", + "connectionLogsDescription": "Просмотр журналов подключения туннелей в этой организации", + "sidebarLogsConnection": "Журнал подключений", + "sidebarLogsStreaming": "Вещание", + "sourceAddress": "Адрес источника", + "destinationAddress": "Адрес назначения", + "duration": "Продолжительность", "licenseRequiredToUse": "Требуется лицензия на Enterprise Edition или Pangolin Cloud для использования этой функции. Забронируйте демонстрацию или пробный POC.", "ossEnterpriseEditionRequired": "Enterprise Edition требуется для использования этой функции. Эта функция также доступна в Pangolin Cloud. Забронируйте демонстрацию или пробный POC.", "certResolver": "Резольвер сертификата", @@ -2487,6 +2615,9 @@ "machineClients": "Машинные клиенты", "install": "Установить", "run": "Запустить", + "envFile": "Файл окружения", + "serviceFile": "Сервисный файл", + "enableAndStart": "Включить и запустить", "clientNameDescription": "Отображаемое имя клиента, которое может быть изменено позже.", "clientAddress": "Адрес клиента (Дополнительно)", "setupFailedToFetchSubnet": "Не удалось получить подсеть по умолчанию", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Редактировать роль и включить опцию 'Требовать утверждения устройств'. Пользователям с этой ролью потребуется подтверждение администратора для новых устройств.", "approvalsEmptyStatePreviewDescription": "Предпросмотр: Если включено, ожидающие запросы на устройство появятся здесь для проверки", "approvalsEmptyStateButtonText": "Управление ролями", - "domainErrorTitle": "У нас возникли проблемы с проверкой вашего домена" + "domainErrorTitle": "У нас возникли проблемы с проверкой вашего домена", + "idpAdminAutoProvisionPoliciesTabHint": "Настройте сопоставление ролей и организационные политики на вкладке Настройки авто-предоставления.", + "streamingTitle": "Поток событий", + "streamingDescription": "Трансляция событий от вашей организации к внешним направлениям в режиме реального времени.", + "streamingUnnamedDestination": "Место назначения без имени", + "streamingNoUrlConfigured": "URL-адрес не настроен", + "streamingAddDestination": "Добавить место назначения", + "streamingHttpWebhookTitle": "HTTP вебхук", + "streamingHttpWebhookDescription": "Отправлять события на любую конечную точку HTTP с гибкой аутентификацией и шаблоном.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Потоковая передача событий к пакету хранения объектов, совместимому с S3.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.", + "streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.", + "streamingFailedToLoad": "Не удалось загрузить места назначения", + "streamingUnexpectedError": "Произошла непредвиденная ошибка.", + "streamingFailedToUpdate": "Не удалось обновить место назначения", + "streamingDeletedSuccess": "Адрес назначения успешно удален", + "streamingFailedToDelete": "Не удалось удалить место назначения", + "streamingDeleteTitle": "Удалить адрес назначения", + "streamingDeleteButtonText": "Удалить адрес назначения", + "streamingDeleteDialogAreYouSure": "Вы уверены, что хотите удалить", + "streamingDeleteDialogThisDestination": "это место назначения", + "streamingDeleteDialogPermanentlyRemoved": "? Все настройки будут удалены навсегда.", + "httpDestEditTitle": "Изменить адрес назначения", + "httpDestAddTitle": "Добавить HTTP адрес", + "httpDestEditDescription": "Обновление конфигурации для этого HTTP события потокового назначения.", + "httpDestAddDescription": "Настройте новую HTTP-конечную точку для получения событий вашей организации.", + "httpDestTabSettings": "Настройки", + "httpDestTabHeaders": "Заголовки", + "httpDestTabBody": "Тело", + "httpDestTabLogs": "Логи", + "httpDestNamePlaceholder": "Мой HTTP адрес назначения", + "httpDestUrlLabel": "URL назначения", + "httpDestUrlErrorHttpRequired": "URL должен использовать http или https", + "httpDestUrlErrorHttpsRequired": "Требуется HTTPS при развертывании облака", + "httpDestUrlErrorInvalid": "Введите действительный URL (например, https://example.com/webhook)", + "httpDestAuthTitle": "Аутентификация", + "httpDestAuthDescription": "Выберите, как запросы к вашей конечной точке аутентифицированы.", + "httpDestAuthNoneTitle": "Нет аутентификации", + "httpDestAuthNoneDescription": "Отправляет запросы без заголовка авторизации.", + "httpDestAuthBearerTitle": "Жетон носителя", + "httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer '' к каждому запросу.", + "httpDestAuthBearerPlaceholder": "Ваш ключ API или токен", + "httpDestAuthBasicTitle": "Базовая авторизация", + "httpDestAuthBasicDescription": "Добавляет заголовок Authorization: Basic ''. Укажите учетные данные в формате username:password.", + "httpDestAuthBasicPlaceholder": "имя пользователя:пароль", + "httpDestAuthCustomTitle": "Пользовательский заголовок", + "httpDestAuthCustomDescription": "Укажите пользовательское имя заголовка HTTP и значение для аутентификации (например, X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Имя заголовка (например, X-API-ключ)", + "httpDestAuthCustomHeaderValuePlaceholder": "Значение заголовка", + "httpDestCustomHeadersTitle": "Пользовательские HTTP-заголовки", + "httpDestCustomHeadersDescription": "Добавляет пользовательские заголовки к каждому исходящему запросу. Полезно для статических маркеров или пользовательского типа содержимого. По умолчанию отправляется Content-Type: application/json.", + "httpDestNoHeadersConfigured": "Пользовательские заголовки не настроены. Нажмите \"Добавить заголовок\", чтобы добавить их.", + "httpDestHeaderNamePlaceholder": "Название заголовка", + "httpDestHeaderValuePlaceholder": "Значение", + "httpDestAddHeader": "Добавить заголовок", + "httpDestBodyTemplateTitle": "Пользовательский шаблон тела", + "httpDestBodyTemplateDescription": "Контролируйте структуру JSON приложения, отправленную на вашу конечную точку. Если отключено, для каждого события отправляется JSON объект по умолчанию.", + "httpDestEnableBodyTemplate": "Включить настраиваемый шаблон тела", + "httpDestBodyTemplateLabel": "Шаблон тела (JSON)", + "httpDestBodyTemplateHint": "Использовать шаблонные переменные для ссылки поля событий в вашей полезной нагрузке.", + "httpDestPayloadFormatTitle": "Формат нагрузки", + "httpDestPayloadFormatDescription": "Как события сериализуются в каждый орган запроса.", + "httpDestFormatJsonArrayTitle": "JSON массив", + "httpDestFormatJsonArrayDescription": "По одному запросу на каждую партию, тело является JSON-массивом. Совместим с большинством общих вебхуков и Датадог.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "По одному запросу на каждую партию, тело - это JSON, разделённый новой строкой, по одному объекту на строку, без внешнего массива. Требуется в Splunk HEC, Elastic / OpenSearch, и Grafana Loki.", + "httpDestFormatSingleTitle": "Одно событие на запрос", + "httpDestFormatSingleDescription": "Отправляет отдельный HTTP POST для каждого отдельного события. Используйте только для конечных точек, которые не могут обрабатывать пакеты.", + "httpDestLogTypesTitle": "Типы журналов", + "httpDestLogTypesDescription": "Выберите, какие типы журналов пересылаются в этот пункт назначения. Только включенные типы журналов будут транслированы.", + "httpDestAccessLogsTitle": "Журналы доступа", + "httpDestAccessLogsDescription": "Попытки доступа к ресурсам, включая аутентифицированные и отклоненные запросы.", + "httpDestActionLogsTitle": "Журнал действий", + "httpDestActionLogsDescription": "Административные меры, осуществляемые пользователями в рамках организации.", + "httpDestConnectionLogsTitle": "Журнал подключений", + "httpDestConnectionLogsDescription": "События связи с сайтами и туннелями, включая соединения и отключения.", + "httpDestRequestLogsTitle": "Запросить журналы", + "httpDestRequestLogsDescription": "Журналы запросов HTTP для проксируемых ресурсов, включая метод, путь и код ответа.", + "httpDestSaveChanges": "Сохранить изменения", + "httpDestCreateDestination": "Создать адрес назначения", + "httpDestUpdatedSuccess": "Адрес назначения успешно обновлен", + "httpDestCreatedSuccess": "Адрес назначения успешно создан", + "httpDestUpdateFailed": "Не удалось обновить место назначения", + "httpDestCreateFailed": "Не удалось создать место назначения" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 2bfed7fb3..f13f6588b 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -148,6 +148,11 @@ "createLink": "Bağlantı Oluştur", "resourcesNotFound": "Hiçbir kaynak bulunamadı", "resourceSearch": "Kaynak ara", + "machineSearch": "Makinaları ara", + "machinesSearch": "Makina müşteri...", + "machineNotFound": "Hiçbir makine bulunamadı", + "userDeviceSearch": "Kullanıcı cihazlarını ara", + "userDevicesSearch": "Kullanıcı cihazlarını ara...", "openMenu": "Menüyü Aç", "resource": "Kaynak", "title": "Başlık", @@ -323,6 +328,54 @@ "apiKeysDelete": "API Anahtarını Sil", "apiKeysManage": "API Anahtarlarını Yönet", "apiKeysDescription": "API anahtarları entegrasyon API'sini doğrulamak için kullanılır", + "provisioningKeysTitle": "Tedarik Anahtarı", + "provisioningKeysManage": "Tedarik Anahtarlarını Yönet", + "provisioningKeysDescription": "Tedarik anahtarları, organizasyonunuz için otomatik site sağlama işlemini doğrulamak için kullanılır.", + "provisioningManage": "Tedarik", + "provisioningDescription": "Tedarik anahtarlarını yönetin ve onay bekleyen siteleri gözden geçirin.", + "pendingSites": "Bekleyen Siteler", + "siteApproveSuccess": "Site başarıyla onaylandı", + "siteApproveError": "Site onaylanırken hata oluştu", + "provisioningKeys": "Tedarik Anahtarları", + "searchProvisioningKeys": "Tedarik anahtarlarını ara...", + "provisioningKeysAdd": "Tedarik Anahtarı Üret", + "provisioningKeysErrorDelete": "Tedarik anahtarı silinirken hata oluştu", + "provisioningKeysErrorDeleteMessage": "Tedarik anahtarı silinirken hata oluştu", + "provisioningKeysQuestionRemove": "Bu tedarik anahtarını organizasyondan kaldırmak istediğinizden emin misiniz?", + "provisioningKeysMessageRemove": "Kaldırıldıktan sonra, anahtar site tedariki için artık kullanılamaz.", + "provisioningKeysDeleteConfirm": "Tedarik Anahtarını Silmeyi Onayla", + "provisioningKeysDelete": "Tedarik Anahtarını Sil", + "provisioningKeysCreate": "Tedarik Anahtarı Üret", + "provisioningKeysCreateDescription": "Organizasyon için yeni bir tedarik anahtarı oluşturun", + "provisioningKeysSeeAll": "Tüm tedarik anahtarlarını gör", + "provisioningKeysSave": "Tedarik anahtarını kaydet", + "provisioningKeysSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyalayın.", + "provisioningKeysErrorCreate": "Tedarik anahtarı oluşturulurken hata oluştu", + "provisioningKeysList": "Yeni tedarik anahtarı", + "provisioningKeysMaxBatchSize": "Maksimum toplu iş boyutu", + "provisioningKeysUnlimitedBatchSize": "Sınırsız toplu iş boyutu (sınırlama yok)", + "provisioningKeysMaxBatchUnlimited": "Sınırsız", + "provisioningKeysMaxBatchSizeInvalid": "Geçerli bir maksimum toplu iş boyutu girin (1–1,000,000).", + "provisioningKeysValidUntil": "Geçerlilik tarihi", + "provisioningKeysValidUntilHint": "Son kullanım tarihi için boş bırakın.", + "provisioningKeysValidUntilInvalid": "Geçerli bir tarih ve saat girin.", + "provisioningKeysNumUsed": "Kullanım Sayısı", + "provisioningKeysLastUsed": "Son kullanım", + "provisioningKeysNoExpiry": "Son kullanma tarihi yok", + "provisioningKeysNeverUsed": "Asla", + "provisioningKeysEdit": "Tedarik Anahtarını Düzenle", + "provisioningKeysEditDescription": "Bu anahtar için maksimum toplu iş boyutunu ve son kullanma zamanını güncelleyin.", + "provisioningKeysApproveNewSites": "Yeni siteleri onayla", + "provisioningKeysApproveNewSitesDescription": "Bu anahtar ile kayıt olan siteleri otomatik olarak onayla.", + "provisioningKeysUpdateError": "Tedarik anahtarı güncellenirken hata oluştu", + "provisioningKeysUpdated": "Tedarik anahtarı güncellendi", + "provisioningKeysUpdatedDescription": "Değişiklikleriniz kaydedildi.", + "provisioningKeysBannerTitle": "Site Tedarik Anahtarları", + "provisioningKeysBannerDescription": "Bir sağlama anahtarı oluşturun ve ilk başlangıçta siteleri otomatik olarak oluşturmak için Newt bağlayıcısını kullanın - her site için ayrı kimlik bilgileri ayarlamaya gerek yok.", + "provisioningKeysBannerButtonText": "Daha fazla bilgi", + "pendingSitesBannerTitle": "Bekleyen Siteler", + "pendingSitesBannerDescription": "Bir sağlama anahtarı kullanarak bağlanan siteler, inceleme için burada görünür.", + "pendingSitesBannerButtonText": "Daha fazla bilgi", "apiKeysSettings": "{apiKeyName} Ayarları", "userTitle": "Tüm Kullanıcıları Yönet", "userDescription": "Sistemdeki tüm kullanıcıları görün ve yönetin", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "Lisans anahtarı etkinleştirilemedi", "licenseErrorKeyActivateDescription": "Lisans anahtarı etkinleştirilirken bir hata oluştu.", "licenseAbout": "Lisans Hakkında", + "licenseBannerTitle": "Kurumsal Lisansınızı Etkinleştirin", + "licenseBannerDescription": "Kendi barındırdığınız Pangolin örneğiniz için kurumsal özelliklerin kilidini açın. Premium yetenekleri etkinleştirmek için bir lisans anahtarı satın alın, ardından aşağıya ekleyin.", + "licenseBannerGetLicense": "Lisans Alın", + "licenseBannerViewDocs": "Dokümantasyonu Görüntüleyin", "communityEdition": "Topluluk Sürümü", "licenseAboutDescription": "Bu, Pangolin'i ticari bir ortamda kullanan işletme ve kurumsal kullanıcılar içindir. Pangolin'i kişisel kullanım için kullanıyorsanız, bu bölümü görmezden gelebilirsiniz.", "licenseKeyActivated": "Lisans anahtarı etkinleştirildi", @@ -509,9 +566,12 @@ "userSaved": "Kullanıcı kaydedildi", "userSavedDescription": "Kullanıcı güncellenmiştir.", "autoProvisioned": "Otomatik Sağlandı", + "autoProvisionSettings": "Otomatik Tedarik Ayarları", "autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver", "accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin", "accessControlsSubmit": "Erişim Kontrollerini Kaydet", + "singleRolePerUserPlanNotice": "Planınız yalnızca kullanıcı başına bir rol desteler.", + "singleRolePerUserEditionNotice": "Bu sürüm yalnızca kullanıcı başına bir rol destekler.", "roles": "Roller", "accessUsersRoles": "Kullanıcılar ve Roller Yönetin", "accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyona erişimi yönetmek için rollere ekleyin", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin", "targetErrorNoSite": "Hiçbir site seçili değil", "targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin", + "targetTargetsCleared": "Hedefler temizlendi", + "targetTargetsClearedDescription": "Bu kaynaktan tüm hedefler kaldırıldı", "targetCreated": "Hedef oluşturuldu", "targetCreatedDescription": "Hedef başarıyla oluşturuldu", "targetErrorCreate": "Hedef oluşturma başarısız oldu", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.", "setupTokenRequired": "Kurulum simgesi gerekli", "actionUpdateSite": "Siteyi Güncelle", + "actionResetSiteBandwidth": "Organizasyon Bant Genişliğini Sıfırla", "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", "actionCreateResource": "Kaynak Oluştur", "actionDeleteResource": "Kaynağı Sil", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "Kullanıcıyı Kaldır", "actionListUsers": "Kullanıcıları Listele", "actionAddUserRole": "Kullanıcı Rolü Ekle", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "Kullanıcı Rolleri Belirle", "actionGenerateAccessToken": "Erişim Jetonu Oluştur", "actionDeleteAccessToken": "Erişim Jetonunu Sil", "actionListAccessTokens": "Erişim Jetonlarını Listele", @@ -1265,6 +1328,7 @@ "sidebarRoles": "Roller", "sidebarShareableLinks": "Bağlantılar", "sidebarApiKeys": "API Anahtarları", + "sidebarProvisioning": "Tedarik", "sidebarSettings": "Ayarlar", "sidebarAllUsers": "Tüm Kullanıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar", @@ -1890,6 +1954,40 @@ "exitNode": "Çıkış Düğümü", "country": "Ülke", "rulesMatchCountry": "Şu anda kaynak IP'ye dayanarak", + "region": "Bölge", + "selectRegion": "Bölgeyi seçin", + "searchRegions": "Bölgeleri ara...", + "noRegionFound": "Bölge bulunamadı.", + "rulesMatchRegion": "Başka ülkelerin bölgesel gruplandırmasını seçin", + "rulesErrorInvalidRegion": "Geçersiz bölge", + "rulesErrorInvalidRegionDescription": "Lütfen geçerli bir bölge seçin.", + "regionAfrica": "Afrika", + "regionNorthernAfrica": "Kuzey Afrika", + "regionEasternAfrica": "Doğu Afrika", + "regionMiddleAfrica": "Orta Afrika", + "regionSouthernAfrica": "Güney Afrika", + "regionWesternAfrica": "Batı Afrika", + "regionAmericas": "Amerika", + "regionCaribbean": "Karayipler", + "regionCentralAmerica": "Orta Amerika", + "regionSouthAmerica": "Güney Amerika", + "regionNorthernAmerica": "Kuzey Amerika", + "regionAsia": "Asya", + "regionCentralAsia": "Orta Asya", + "regionEasternAsia": "Doğu Asya", + "regionSouthEasternAsia": "Güneydoğu Asya", + "regionSouthernAsia": "Güney Asya", + "regionWesternAsia": "Batı Asya", + "regionEurope": "Avrupa", + "regionEasternEurope": "Doğu Avrupa", + "regionNorthernEurope": "Kuzey Avrupa", + "regionSouthernEurope": "Güney Avrupa", + "regionWesternEurope": "Batı Avrupa", + "regionOceania": "Okyanusya", + "regionAustraliaAndNewZealand": "Avustralya ve Yeni Zelanda", + "regionMelanesia": "Melanezya", + "regionMicronesia": "Mikronezya", + "regionPolynesia": "Polinezya", "managedSelfHosted": { "title": "Yönetilen Self-Hosted", "description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu", @@ -1938,6 +2036,25 @@ "invalidValue": "Geçersiz değer", "idpTypeLabel": "Kimlik Sağlayıcı Türü", "roleMappingExpressionPlaceholder": "örn., contains(gruplar, 'yönetici') && 'Yönetici' || 'Üye'", + "roleMappingModeFixedRoles": "Sabit Roller", + "roleMappingModeMappingBuilder": "Harita Oluşturucu", + "roleMappingModeRawExpression": "Ham İfade", + "roleMappingFixedRolesPlaceholderSelect": "Bir veya daha fazla rol seçin", + "roleMappingFixedRolesPlaceholderFreeform": "Rol isimlerini yazın (organizasyon başına tam eşleşme)", + "roleMappingFixedRolesDescriptionSameForAll": "Her otomatik tedarik edilmiş kullanıcıya aynı rol setini atayın.", + "roleMappingFixedRolesDescriptionDefaultPolicy": "Varsayılan politikalar için, kullanıcıların sağlandığı her organizasyonda mevcut olan rol isimlerini yazın. İsimler tam olarak eşleşmelidir.", + "roleMappingClaimPath": "Hak Talep Yolu", + "roleMappingClaimPathPlaceholder": "gruplar", + "roleMappingClaimPathDescription": "Kaynak değerleri içeren belirteç yükündeki yol (örneğin, gruplar).", + "roleMappingMatchValue": "Eşleme Değeri", + "roleMappingAssignRoles": "Rolleri Ata", + "roleMappingAddMappingRule": "Eşleme Kuralı Ekle", + "roleMappingRawExpressionResultDescription": "İfade bir string veya string dizisine değerlendirilmelidir.", + "roleMappingRawExpressionResultDescriptionSingleRole": "İfade bir string (tek rol ismi) olarak değerlendirilmelidir.", + "roleMappingMatchValuePlaceholder": "Eşleme değeri (örneğin: admin)", + "roleMappingAssignRolesPlaceholderFreeform": "Rol isimlerini yazın (organizasyon başına tam eşleşme)", + "roleMappingBuilderFreeformRowHint": "Rol isimleri her hedef organizasyondaki bir rol ile eşleşmelidir.", + "roleMappingRemoveRule": "Kaldır", "idpGoogleConfiguration": "Google Yapılandırması", "idpGoogleConfigurationDescription": "Google OAuth2 kimlik bilgilerinizi yapılandırın", "idpGoogleClientIdDescription": "Google OAuth2 İstemci Kimliğiniz", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "Kuruluşun kimlik doğrulama sayfası için bir alan seçin", "domainPickerProvidedDomain": "Sağlanan Alan Adı", "domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı", + "domainPickerFreeDomainsPaidFeature": "Sağlanan alan adları ücretli bir özelliktir. Planınıza dahil bir alan adı almak için abone olun - kendi alan adınızı getirmenize gerek yok.", "domainPickerVerified": "Doğrulandı", "domainPickerUnverified": "Doğrulanmadı", + "domainPickerManual": "Manuel", "domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.", "domainPickerError": "Hata", "domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi", @@ -2235,7 +2354,7 @@ "description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek." } }, - "personalUseOnly": "Yalnızca kişisel kullanım (ücretsiz lisans — ödeme yapılmaz)", + "personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)", "buttons": { "continueToCheckout": "Ödemeye Devam Et" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle", "logRetentionActionLabel": "Eylem Günlüğü Saklama", "logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle", + "logRetentionConnectionLabel": "Bağlantı kayıtlarını ne kadar süre saklayacağınız", + "logRetentionConnectionDescription": "Bağlantı kayıtlarını ne kadar süre saklayacağınız", "logRetentionDisabled": "Devre Dışı", "logRetention3Days": "3 gün", "logRetention7Days": "7 gün", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu", "actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin", "accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin", + "connectionLogs": "Bağlantı Kayıtları", + "connectionLogsDescription": "Bu organizasyondaki tüneller için bağlantı geçmişine bakın", + "sidebarLogsConnection": "Bağlantı Kayıtları", + "sidebarLogsStreaming": "Akış", + "sourceAddress": "Kaynak Adresi", + "destinationAddress": "Hedef Adresi", + "duration": "Süre", "licenseRequiredToUse": "Bu özelliği kullanmak için bir Enterprise Edition lisansı veya Pangolin Cloud gereklidir. Tanıtım veya POC denemesi ayarlayın.", "ossEnterpriseEditionRequired": "Bu özelliği kullanmak için Enterprise Edition gereklidir. Bu özellik ayrıca Pangolin Cloud’da da mevcuttur. Tanıtım veya POC denemesi ayarlayın.", "certResolver": "Sertifika Çözücü", @@ -2487,6 +2615,9 @@ "machineClients": "Makine İstemcileri", "install": "Yükle", "run": "Çalıştır", + "envFile": "Ortam Dosyası", + "serviceFile": "Servis Dosyası", + "enableAndStart": "Etkinleştir ve Başlat", "clientNameDescription": "Daha sonra değiştirilebilecek istemcinin görünen adı.", "clientAddress": "İstemci Adresi (Gelişmiş)", "setupFailedToFetchSubnet": "Varsayılan alt ağ alınamadı", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "Bir rolü düzenleyin ve 'Cihaz Onaylarını Gerektir' seçeneğini etkinleştirin. Bu role sahip kullanıcıların yeni cihazlar için yönetici onayına ihtiyacı olacaktır.", "approvalsEmptyStatePreviewDescription": "Önizleme: Etkinleştirildiğinde, bekleyen cihaz talepleri incelenmek üzere burada görünecektir.", "approvalsEmptyStateButtonText": "Rolleri Yönet", - "domainErrorTitle": "Alan adınızı doğrulamada sorun yaşıyoruz" + "domainErrorTitle": "Alan adınızı doğrulamada sorun yaşıyoruz", + "idpAdminAutoProvisionPoliciesTabHint": "Rol eşleme ve organizasyon politikalarını Otomatik Tedarik Ayarları sekmesinde yapılandırın.", + "streamingTitle": "Olay Akışı", + "streamingDescription": "Olayları organizasyonunuzdan dış hedeflere gerçek zamanlı olarak iletin.", + "streamingUnnamedDestination": "Adsız hedef", + "streamingNoUrlConfigured": "URL yapılandırılmadı", + "streamingAddDestination": "Hedef Ekle", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Esnek kimlik doğrulama ve şablon oluşturmayla her HTTP uç noktasına olaylar gönderin.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Olayları S3 uyumlu bir nesne depolama kovasına iletin. Yakında gelicek.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.", + "streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.", + "streamingFailedToLoad": "Hedefler yüklenemedi", + "streamingUnexpectedError": "Beklenmeyen bir hata oluştu.", + "streamingFailedToUpdate": "Hedef güncellenemedi", + "streamingDeletedSuccess": "Hedef başarıyla silindi", + "streamingFailedToDelete": "Hedef silinemedi", + "streamingDeleteTitle": "Hedefi Sil", + "streamingDeleteButtonText": "Hedefi Sil", + "streamingDeleteDialogAreYouSure": "Silmek istediğinizden emin misiniz", + "streamingDeleteDialogThisDestination": "bu hedefi", + "streamingDeleteDialogPermanentlyRemoved": "? Tüm yapılandırma kalıcı olarak kaldırılacak.", + "httpDestEditTitle": "Hedefi Düzenle", + "httpDestAddTitle": "HTTP Hedefi Ekle", + "httpDestEditDescription": "Bu HTTP olay akışı hedefine yapılandırmayı güncelleyin.", + "httpDestAddDescription": "Organizasyonunuzun olaylarını almak için yeni bir HTTP uç noktası yapılandırın.", + "httpDestTabSettings": "Ayarlar", + "httpDestTabHeaders": "Başlıklar", + "httpDestTabBody": "Gövde", + "httpDestTabLogs": "Kayıtlar", + "httpDestNamePlaceholder": "Benim HTTP hedefim", + "httpDestUrlLabel": "Hedef URL", + "httpDestUrlErrorHttpRequired": "URL http veya https kullanmalıdır", + "httpDestUrlErrorHttpsRequired": "Bulut dağıtımlarında HTTPS gereklidir", + "httpDestUrlErrorInvalid": "Geçerli bir URL girin (örn. https://example.com/webhook)", + "httpDestAuthTitle": "Kimlik Doğrulama", + "httpDestAuthDescription": "Uç noktanıza yapılan isteklerin nasıl kimlik doğrulandığını seçin.", + "httpDestAuthNoneTitle": "Kimlik Doğrulama Yok", + "httpDestAuthNoneDescription": "Yetkilendirme başlığı olmadan istekler gönderir.", + "httpDestAuthBearerTitle": "Taşıyıcı Jetonu", + "httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı '' üst bilgisi ekler.", + "httpDestAuthBearerPlaceholder": "API anahtarınız veya jetonunuz", + "httpDestAuthBasicTitle": "Temel Kimlik Doğrulama", + "httpDestAuthBasicDescription": "Bir Yetkilendirme: Temel '' üst bilgisi ekler. Kimlik bilgilerini kullanıcı adı:şifre olarak sağlayın.", + "httpDestAuthBasicPlaceholder": "kullanıcı adı:şifre", + "httpDestAuthCustomTitle": "Özel Başlık", + "httpDestAuthCustomDescription": "Kimlik doğrulama için özel bir HTTP başlık adı ve değer belirtin (örn. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Başlık adı (örn. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Başlık değeri", + "httpDestCustomHeadersTitle": "Özel HTTP Başlıkları", + "httpDestCustomHeadersDescription": "Her giden isteğe özel başlıklar ekleyin. Statik jetonlar veya özel bir İçerik Türü için kullanışlıdır. Varsayılan olarak İçerik Türü: application/json gönderilir.", + "httpDestNoHeadersConfigured": "Özel başlık yapılandırılmamış. Bir tane eklemek için \"Başlık Ekle\"ye tıklayın.", + "httpDestHeaderNamePlaceholder": "Başlık adı", + "httpDestHeaderValuePlaceholder": "Değer", + "httpDestAddHeader": "Başlık Ekle", + "httpDestBodyTemplateTitle": "Özel Gövde Şablonu", + "httpDestBodyTemplateDescription": "Uç noktanıza gönderilen JSON yük yapısını kontrol edin. Devre dışı bırakılırsa, her olay için varsayılan bir JSON nesnesi gönderilir.", + "httpDestEnableBodyTemplate": "Özel gövde şablonunu etkinleştir", + "httpDestBodyTemplateLabel": "Gövde Şablonu (JSON)", + "httpDestBodyTemplateHint": "Yükünüzdeki olay alanlarına atıfta bulunmak için şablon değişkenlerini kullanın.", + "httpDestPayloadFormatTitle": "Yük Formatı", + "httpDestPayloadFormatDescription": "Her bir istek gövdesine olayların nasıl serileştirildiği.", + "httpDestFormatJsonArrayTitle": "JSON Dizisi", + "httpDestFormatJsonArrayDescription": "Her bir toplu işte bir istek, gövde bir JSON dizisidir. Çoğu genel webhook ve Datadog ile uyumludur.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "Her bir toplu işte bir istek, gövde satırlarla ayrılmış JSON'dur - her satıra bir nesne, dış dizi yoktur. Splunk HEC, Elastic / OpenSearch ve Grafana Loki tarafından gereklidir.", + "httpDestFormatSingleTitle": "Her İstek Başına Bir Olay", + "httpDestFormatSingleDescription": "Her olay için ayrı bir HTTP POST gönderir. Toplu işlere yetkemeyen uç noktalar için kullanın.", + "httpDestLogTypesTitle": "Kayıt Türleri", + "httpDestLogTypesDescription": "Bu hedefe hangi kayıt türlerinin iletileceğini seçin. Yalnızca etkin kayıt türleri yayınlanacaktır.", + "httpDestAccessLogsTitle": "Erişim Kayıtları", + "httpDestAccessLogsDescription": "Kimlik doğrulanmış ve reddedilen talepler dahil kaynak erişim denemeleri.", + "httpDestActionLogsTitle": "Eylem Kayıtları", + "httpDestActionLogsDescription": "Kullanıcılar tarafından organizasyon içerisinde yapılan yönetici eylemleri.", + "httpDestConnectionLogsTitle": "Bağlantı Kayıtları", + "httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.", + "httpDestRequestLogsTitle": "İstek Kayıtları", + "httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.", + "httpDestSaveChanges": "Değişiklikleri Kaydet", + "httpDestCreateDestination": "Hedef Oluştur", + "httpDestUpdatedSuccess": "Hedef başarıyla güncellendi", + "httpDestCreatedSuccess": "Hedef başarıyla oluşturuldu", + "httpDestUpdateFailed": "Hedef güncellenemedi", + "httpDestCreateFailed": "Hedef oluşturulamadı" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index f297d1ea9..4d5d96d7e 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -148,6 +148,11 @@ "createLink": "创建链接", "resourcesNotFound": "找不到资源", "resourceSearch": "搜索资源", + "machineSearch": "搜索机", + "machinesSearch": "搜索机器客户端...", + "machineNotFound": "未找到任何机", + "userDeviceSearch": "搜索用户设备", + "userDevicesSearch": "搜索用户设备...", "openMenu": "打开菜单", "resource": "资源", "title": "标题", @@ -323,6 +328,54 @@ "apiKeysDelete": "删除 API 密钥", "apiKeysManage": "管理 API 密钥", "apiKeysDescription": "API 密钥用于认证集成 API", + "provisioningKeysTitle": "置备密钥", + "provisioningKeysManage": "管理置备键", + "provisioningKeysDescription": "置备密钥用于验证您组织的自动站点配置。", + "provisioningManage": "置备中", + "provisioningDescription": "管理预配键和审查等待批准的站点。", + "pendingSites": "待定站点", + "siteApproveSuccess": "站点批准成功", + "siteApproveError": "批准站点出错", + "provisioningKeys": "置备键", + "searchProvisioningKeys": "搜索配备密钥...", + "provisioningKeysAdd": "生成置备键", + "provisioningKeysErrorDelete": "删除预配键时出错", + "provisioningKeysErrorDeleteMessage": "删除预配键时出错", + "provisioningKeysQuestionRemove": "您确定要从组织中删除此预配键吗?", + "provisioningKeysMessageRemove": "一旦移除,密钥不能再用于站点预配。", + "provisioningKeysDeleteConfirm": "确认删除置备键", + "provisioningKeysDelete": "删除置备键", + "provisioningKeysCreate": "生成置备键", + "provisioningKeysCreateDescription": "为组织生成一个新的预置密钥", + "provisioningKeysSeeAll": "查看所有预配键", + "provisioningKeysSave": "保存预配键", + "provisioningKeysSaveDescription": "您只能看到一次。复制它到一个安全的地方。", + "provisioningKeysErrorCreate": "创建预配键时出错", + "provisioningKeysList": "新建预配键", + "provisioningKeysMaxBatchSize": "最大批量大小", + "provisioningKeysUnlimitedBatchSize": "无限批量大小(无限制)", + "provisioningKeysMaxBatchUnlimited": "无限制", + "provisioningKeysMaxBatchSizeInvalid": "输入一个有效的最大批处理大小(1-1,000,000)。", + "provisioningKeysValidUntil": "有效期至", + "provisioningKeysValidUntilHint": "留空为无过期。", + "provisioningKeysValidUntilInvalid": "输入一个有效的日期和时间。", + "provisioningKeysNumUsed": "使用的时间", + "provisioningKeysLastUsed": "上次使用", + "provisioningKeysNoExpiry": "没有过期", + "provisioningKeysNeverUsed": "永不过期", + "provisioningKeysEdit": "编辑置备键", + "provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。", + "provisioningKeysApproveNewSites": "批准新站点", + "provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的站点。", + "provisioningKeysUpdateError": "更新预配键时出错", + "provisioningKeysUpdated": "置备密钥已更新", + "provisioningKeysUpdatedDescription": "您的更改已保存。", + "provisioningKeysBannerTitle": "站点置备密钥", + "provisioningKeysBannerDescription": "生成一个供应密钥,并将其与 Newt 连接器一起使用,以在首次启动时自动创建站点 - 无需为每个站点设置单独的凭据。", + "provisioningKeysBannerButtonText": "了解更多", + "pendingSitesBannerTitle": "待定站点", + "pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。", + "pendingSitesBannerButtonText": "了解更多", "apiKeysSettings": "{apiKeyName} 设置", "userTitle": "管理所有用户", "userDescription": "查看和管理系统中的所有用户", @@ -352,6 +405,10 @@ "licenseErrorKeyActivate": "激活许可证密钥失败", "licenseErrorKeyActivateDescription": "激活许可证密钥时出错。", "licenseAbout": "关于许可协议", + "licenseBannerTitle": "启用您的企业许可证", + "licenseBannerDescription": "为您自行托管的Pangolin实例解锁企业功能。购买许可证密钥以激活高级功能,然后在下方添加。", + "licenseBannerGetLicense": "获取许可证", + "licenseBannerViewDocs": "查看文档", "communityEdition": "社区版", "licenseAboutDescription": "这是针对商业环境中使用Pangolin的商业和企业用户。 如果您正在使用 Pangolin 供个人使用,您可以忽略此部分。", "licenseKeyActivated": "授权密钥已激活", @@ -509,9 +566,12 @@ "userSaved": "用户已保存", "userSavedDescription": "用户已更新。", "autoProvisioned": "自动设置", + "autoProvisionSettings": "自动提供设置", "autoProvisionedDescription": "允许此用户由身份提供商自动管理", "accessControlsDescription": "管理此用户在组织中可以访问和做什么", "accessControlsSubmit": "保存访问控制", + "singleRolePerUserPlanNotice": "您的计划仅支持每个用户一个角色。", + "singleRolePerUserEditionNotice": "此版本仅支持每个用户一个角色。", "roles": "角色", "accessUsersRoles": "管理用户和角色", "accessUsersRolesDescription": "邀请用户加入角色来管理访问组织", @@ -568,6 +628,8 @@ "targetErrorInvalidPortDescription": "请输入有效的端口号", "targetErrorNoSite": "没有选择站点", "targetErrorNoSiteDescription": "请选择目标站点", + "targetTargetsCleared": "目标已清除", + "targetTargetsClearedDescription": "所有目标已从此资源中移除", "targetCreated": "目标已创建", "targetCreatedDescription": "目标已成功创建", "targetErrorCreate": "创建目标失败", @@ -1119,6 +1181,7 @@ "setupTokenDescription": "从服务器控制台输入设置令牌。", "setupTokenRequired": "需要设置令牌", "actionUpdateSite": "更新站点", + "actionResetSiteBandwidth": "重置组织带宽", "actionListSiteRoles": "允许站点角色列表", "actionCreateResource": "创建资源", "actionDeleteResource": "删除资源", @@ -1148,7 +1211,7 @@ "actionRemoveUser": "删除用户", "actionListUsers": "列出用户", "actionAddUserRole": "添加用户角色", - "actionSetUserOrgRoles": "Set User Roles", + "actionSetUserOrgRoles": "设置用户角色", "actionGenerateAccessToken": "生成访问令牌", "actionDeleteAccessToken": "删除访问令牌", "actionListAccessTokens": "访问令牌", @@ -1265,6 +1328,7 @@ "sidebarRoles": "角色", "sidebarShareableLinks": "链接", "sidebarApiKeys": "API密钥", + "sidebarProvisioning": "置备中", "sidebarSettings": "设置", "sidebarAllUsers": "所有用户", "sidebarIdentityProviders": "身份提供商", @@ -1890,6 +1954,40 @@ "exitNode": "出口节点", "country": "国家", "rulesMatchCountry": "当前基于源 IP", + "region": "地区", + "selectRegion": "选择区域", + "searchRegions": "搜索区域...", + "noRegionFound": "未找到区域。", + "rulesMatchRegion": "选择一个区域国家组", + "rulesErrorInvalidRegion": "无效区域", + "rulesErrorInvalidRegionDescription": "请选择一个有效的区域。", + "regionAfrica": "非洲", + "regionNorthernAfrica": "B. 北非地区", + "regionEasternAfrica": "东部非洲", + "regionMiddleAfrica": "中东", + "regionSouthernAfrica": "D. 南 非", + "regionWesternAfrica": "D. 西部非洲", + "regionAmericas": "Americas", + "regionCaribbean": "加勒比", + "regionCentralAmerica": "中美洲:", + "regionSouthAmerica": "南 非", + "regionNorthernAmerica": "北美洲:", + "regionAsia": "亚洲", + "regionCentralAsia": "B. 亚 洲", + "regionEasternAsia": "东亚", + "regionSouthEasternAsia": "D. 东南亚区域", + "regionSouthernAsia": "D. 亚 洲", + "regionWesternAsia": "西亚", + "regionEurope": "欧洲", + "regionEasternEurope": "D. 欧 洲", + "regionNorthernEurope": "北欧洲", + "regionSouthernEurope": "南欧洲", + "regionWesternEurope": "西欧洲", + "regionOceania": "Oceania", + "regionAustraliaAndNewZealand": "澳大利亚和新西兰", + "regionMelanesia": "Melanesia", + "regionMicronesia": "Micronesia", + "regionPolynesia": "Polynesia", "managedSelfHosted": { "title": "托管自托管", "description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器", @@ -1938,6 +2036,25 @@ "invalidValue": "无效的值", "idpTypeLabel": "身份提供者类型", "roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'", + "roleMappingModeFixedRoles": "固定角色", + "roleMappingModeMappingBuilder": "映射构建器", + "roleMappingModeRawExpression": "原始表达式", + "roleMappingFixedRolesPlaceholderSelect": "选择一个或多个角色", + "roleMappingFixedRolesPlaceholderFreeform": "输入角色名称 (每个组织确切匹配)", + "roleMappingFixedRolesDescriptionSameForAll": "将相同的角色分配给每个自动配备的用户。", + "roleMappingFixedRolesDescriptionDefaultPolicy": "对于缺省策略,每个提供用户的组织中存在的角色名称类型。名称必须完全匹配。", + "roleMappingClaimPath": "认领路径", + "roleMappingClaimPathPlaceholder": "组", + "roleMappingClaimPathDescription": "包含源值的 token 有效负载路径 (例如组)。", + "roleMappingMatchValue": "匹配值", + "roleMappingAssignRoles": "分配角色", + "roleMappingAddMappingRule": "添加映射规则", + "roleMappingRawExpressionResultDescription": "表达式必须值为字符串或字符串。", + "roleMappingRawExpressionResultDescriptionSingleRole": "表达式必须计算到字符串(单个角色名称)。", + "roleMappingMatchValuePlaceholder": "匹配值(例如: 管理员)", + "roleMappingAssignRolesPlaceholderFreeform": "输入角色名称 (每个组织确切)", + "roleMappingBuilderFreeformRowHint": "角色名称必须匹配每个目标组织的角色。", + "roleMappingRemoveRule": "删除", "idpGoogleConfiguration": "Google 配置", "idpGoogleConfigurationDescription": "配置 Google OAuth2 凭据", "idpGoogleClientIdDescription": "Google OAuth2 Client ID", @@ -2001,8 +2118,10 @@ "selectDomainForOrgAuthPage": "选择组织认证页面的域", "domainPickerProvidedDomain": "提供的域", "domainPickerFreeProvidedDomain": "免费提供的域", + "domainPickerFreeDomainsPaidFeature": "提供的域名是付费功能。订阅即可将域名包含在您的计划中—无需自带域名。", "domainPickerVerified": "已验证", "domainPickerUnverified": "未验证", + "domainPickerManual": "手动", "domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。", "domainPickerError": "错误", "domainPickerErrorLoadDomains": "加载组织域名失败", @@ -2235,7 +2354,7 @@ "description": "企业特征、50个用户、50个站点和优先支持。" } }, - "personalUseOnly": "仅供个人使用 (免费许可证-无签出)", + "personalUseOnly": "仅限个人使用(免费许可 - 无需结账)", "buttons": { "continueToCheckout": "继续签出" }, @@ -2334,6 +2453,8 @@ "logRetentionAccessDescription": "保留访问日志的时间", "logRetentionActionLabel": "动作日志保留", "logRetentionActionDescription": "保留操作日志的时间", + "logRetentionConnectionLabel": "连接日志保留", + "logRetentionConnectionDescription": "保留连接日志的时间", "logRetentionDisabled": "已禁用", "logRetention3Days": "3 天", "logRetention7Days": "7 天", @@ -2344,6 +2465,13 @@ "logRetentionEndOfFollowingYear": "下一年结束", "actionLogsDescription": "查看此机构执行的操作历史", "accessLogsDescription": "查看此机构资源的访问认证请求", + "connectionLogs": "连接日志", + "connectionLogsDescription": "查看此机构隧道的连接日志", + "sidebarLogsConnection": "连接日志", + "sidebarLogsStreaming": "流流", + "sourceAddress": "源地址", + "destinationAddress": "目的地址", + "duration": "期限", "licenseRequiredToUse": "使用此功能需要企业版许可证或Pangolin Cloud预约演示或POC试用。", "ossEnterpriseEditionRequired": "需要 Enterprise Edition 才能使用此功能。 此功能也可在 Pangolin Cloud上获取。 预订演示或POC 试用。", "certResolver": "证书解决器", @@ -2487,6 +2615,9 @@ "machineClients": "机器客户端", "install": "安装", "run": "运行", + "envFile": "环境文件", + "serviceFile": "服务文件", + "enableAndStart": "启用并启动", "clientNameDescription": "可以稍后更改的客户端的显示名称。", "clientAddress": "客户端地址 (高级)", "setupFailedToFetchSubnet": "获取默认子网失败", @@ -2683,5 +2814,90 @@ "approvalsEmptyStateStep2Description": "编辑角色并启用“需要设备审批”选项。具有此角色的用户需要管理员批准新设备。", "approvalsEmptyStatePreviewDescription": "预览:如果启用,待处理设备请求将出现在这里供审核", "approvalsEmptyStateButtonText": "管理角色", - "domainErrorTitle": "我们在验证您的域名时遇到了问题" + "domainErrorTitle": "我们在验证您的域名时遇到了问题", + "idpAdminAutoProvisionPoliciesTabHint": "在 自动供应设置 选项卡上配置角色映射和组织策略。", + "streamingTitle": "事件流", + "streamingDescription": "实时将事件从您的组织流到外部目的地。", + "streamingUnnamedDestination": "未命名目标", + "streamingNoUrlConfigured": "未配置URL", + "streamingAddDestination": "添加目标", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "将事件发送到任意HTTP端点并灵活验证和模板。", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "将事件串流到 S3 兼容的对象存储桶。即将推出。", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。", + "streamingTypePickerDescription": "选择要开始的目标类型。", + "streamingFailedToLoad": "加载目的地失败", + "streamingUnexpectedError": "发生意外错误.", + "streamingFailedToUpdate": "更新目标失败", + "streamingDeletedSuccess": "目标删除成功", + "streamingFailedToDelete": "删除目标失败", + "streamingDeleteTitle": "删除目标", + "streamingDeleteButtonText": "删除目标", + "streamingDeleteDialogAreYouSure": "您确定要删除吗?", + "streamingDeleteDialogThisDestination": "这个目标", + "streamingDeleteDialogPermanentlyRemoved": "? 所有配置将被永久删除。", + "httpDestEditTitle": "编辑目标", + "httpDestAddTitle": "添加 HTTP 目标", + "httpDestEditDescription": "更新此 HTTP 事件流媒体目的地的配置。", + "httpDestAddDescription": "配置新的 HTTP 端点来接收您的组织事件。", + "httpDestTabSettings": "设置", + "httpDestTabHeaders": "信头", + "httpDestTabBody": "正文内容", + "httpDestTabLogs": "日志", + "httpDestNamePlaceholder": "我的 HTTP 目标", + "httpDestUrlLabel": "目标网址", + "httpDestUrlErrorHttpRequired": "URL 必须使用 http 或 https", + "httpDestUrlErrorHttpsRequired": "云端部署需要HTTPS", + "httpDestUrlErrorInvalid": "输入一个有效的 URL (例如,https://example.com/webhook)", + "httpDestAuthTitle": "认证", + "httpDestAuthDescription": "选择如何验证您的端点的请求。", + "httpDestAuthNoneTitle": "无身份验证", + "httpDestAuthNoneDescription": "在没有授权头的情况下发送请求。", + "httpDestAuthBearerTitle": "持有者令牌", + "httpDestAuthBearerDescription": "在每个请求中添加授权:Bearer “” 头。", + "httpDestAuthBearerPlaceholder": "您的 API 密钥或令牌", + "httpDestAuthBasicTitle": "基本认证", + "httpDestAuthBasicDescription": "添加一个Authorization: Basic \"<凭据>\" 标头。 以用户名:密码形式提供凭据。", + "httpDestAuthBasicPlaceholder": "用户名:密码", + "httpDestAuthCustomTitle": "自定义标题", + "httpDestAuthCustomDescription": "指定自定义 HTTP 头名称和身份验证值 (例如,X-API 键)。", + "httpDestAuthCustomHeaderNamePlaceholder": "标题名称(例如X-API-键)", + "httpDestAuthCustomHeaderValuePlaceholder": "页眉值", + "httpDestCustomHeadersTitle": "自定义 HTTP 头", + "httpDestCustomHeadersDescription": "向每个输出请求添加自定义标题。用于静态令牌或自定义内容类型。默认情况下,内容类型:应用程序/json已发送。", + "httpDestNoHeadersConfigured": "未配置自定义头。单击\"添加头\"以添加一个。", + "httpDestHeaderNamePlaceholder": "标题名称", + "httpDestHeaderValuePlaceholder": "值", + "httpDestAddHeader": "添加标题", + "httpDestBodyTemplateTitle": "自定义实体模板", + "httpDestBodyTemplateDescription": "控制发送到您的端点的 JSON 有效载荷结构。如果禁用,将为每个事件发送一个 JSON 默认对象。", + "httpDestEnableBodyTemplate": "启用自定义实体模板", + "httpDestBodyTemplateLabel": "身体模板 (JSON)", + "httpDestBodyTemplateHint": "将模板变量用于您有效载荷中的参考事件字段。", + "httpDestPayloadFormatTitle": "有效载荷格式", + "httpDestPayloadFormatDescription": "事件如何序列化为每个请求实体。", + "httpDestFormatJsonArrayTitle": "JSON 数组", + "httpDestFormatJsonArrayDescription": "每批一个请求,实体是一个 JSON 数组。与大多数通用的 Web 钩子和数据兼容。", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "每批有一个请求,物体是换行符限制的 JSON ——每行一个对象,不是外部数组。 Sluk HEC、Elastic / OpenSearch和Grafana Loki所需。", + "httpDestFormatSingleTitle": "每个请求一个事件", + "httpDestFormatSingleDescription": "为每个事件单独发送一个 HTTP POST。仅用于无法处理批量的端点。", + "httpDestLogTypesTitle": "日志类型", + "httpDestLogTypesDescription": "选择转发到此目的地的日志类型。只有启用的日志类型才会被连续使用。", + "httpDestAccessLogsTitle": "访问日志", + "httpDestAccessLogsDescription": "资源访问尝试,包括已验证和拒绝的请求。", + "httpDestActionLogsTitle": "操作日志", + "httpDestActionLogsDescription": "组织内部用户采取的行政行动。", + "httpDestConnectionLogsTitle": "连接日志", + "httpDestConnectionLogsDescription": "站点和隧道连接事件,包括连接和断开连接。", + "httpDestRequestLogsTitle": "请求日志", + "httpDestRequestLogsDescription": "HTTP 请求代理资源日志,包括方法、路径和响应代码。", + "httpDestSaveChanges": "保存更改", + "httpDestCreateDestination": "创建目标", + "httpDestUpdatedSuccess": "目标已成功更新", + "httpDestCreatedSuccess": "目标创建成功", + "httpDestUpdateFailed": "更新目标失败", + "httpDestCreateFailed": "创建目标失败" } diff --git a/messages/zh-TW.json b/messages/zh-TW.json index 8b9d05f53..cf7c25ced 100644 --- a/messages/zh-TW.json +++ b/messages/zh-TW.json @@ -1,2399 +1,2403 @@ { - "setupCreate": "創建您的第一個組織、網站和資源", - "headerAuthCompatibilityInfo": "啟用此選項以在缺少驗證令牌時強制回傳 401 未授權回應。這對於不會在沒有伺服器挑戰的情況下發送憑證的瀏覽器或特定 HTTP 函式庫是必需的。", - "headerAuthCompatibility": "擴展相容性", - "setupNewOrg": "新建組織", - "setupCreateOrg": "創建組織", - "setupCreateResources": "創建資源", - "setupOrgName": "組織名稱", - "orgDisplayName": "這是您組織的顯示名稱。", - "orgId": "組織ID", - "setupIdentifierMessage": "這是您組織的唯一標識符。這是與顯示名稱分開的。", - "setupErrorIdentifier": "組織ID 已被使用。請另選一個。", - "componentsErrorNoMemberCreate": "您目前不是任何組織的成員。創建組織以開始操作。", - "componentsErrorNoMember": "您目前不是任何組織的成員。", - "welcome": "歡迎使用 Pangolin", - "welcomeTo": "歡迎來到", - "componentsCreateOrg": "創建組織", - "componentsMember": "您屬於 {count, plural, =0 {沒有組織} one {一個組織} other {# 個組織}}。", - "componentsInvalidKey": "檢測到無效或過期的許可證金鑰。按照許可證條款操作以繼續使用所有功能。", - "dismiss": "忽略", - "componentsLicenseViolation": "許可證超限:該伺服器使用了 {usedSites} 個站點,已超過授權的 {maxSites} 個。請遵守許可證條款以繼續使用全部功能。", - "componentsSupporterMessage": "感謝您的支持!您現在是 Pangolin 的 {tier} 用戶。", - "inviteErrorNotValid": "很抱歉,但看起來你試圖訪問的邀請尚未被接受或不再有效。", - "inviteErrorUser": "很抱歉,但看起來你想要訪問的邀請不是這個用戶。", - "inviteLoginUser": "請確保您以正確的用戶登錄。", - "inviteErrorNoUser": "很抱歉,但看起來你想訪問的邀請不是一個存在的用戶。", - "inviteCreateUser": "請先創建一個帳戶。", - "goHome": "返回首頁", - "inviteLogInOtherUser": "以不同的用戶登錄", - "createAnAccount": "創建帳戶", - "inviteNotAccepted": "邀請未接受", - "authCreateAccount": "創建一個帳戶以開始", - "authNoAccount": "沒有帳戶?", - "email": "電子郵件地址", - "password": "密碼", - "confirmPassword": "確認密碼", - "createAccount": "創建帳戶", - "viewSettings": "查看設置", - "delete": "刪除", - "name": "名稱", - "online": "在線", - "offline": "離線的", - "site": "站點", - "dataIn": "數據輸入", - "dataOut": "數據輸出", - "connectionType": "連接類型", - "tunnelType": "隧道類型", - "local": "本地的", - "edit": "編輯", - "siteConfirmDelete": "確認刪除站點", - "siteDelete": "刪除站點", - "siteMessageRemove": "一旦移除,站點將無法訪問。與站點相關的所有目標也將被移除。", - "siteQuestionRemove": "您確定要從組織中刪除該站點嗎?", - "siteManageSites": "管理站點", - "siteDescription": "允許通過安全隧道連接到您的網路", - "sitesBannerTitle": "連接任何網路", - "sitesBannerDescription": "站點是與遠端網路的連接,使 Pangolin 能夠為任何地方的使用者提供對公共或私有資源的存取。在任何可以執行二進位檔案或容器的地方安裝站點網路連接器 (Newt) 以建立連接。", - "sitesBannerButtonText": "安裝站點", - "siteCreate": "創建站點", - "siteCreateDescription2": "按照下面的步驟創建和連接一個新站點", - "siteCreateDescription": "創建一個新站點開始連接您的資源", - "close": "關閉", - "siteErrorCreate": "創建站點出錯", - "siteErrorCreateKeyPair": "找不到金鑰對或站點預設值", - "siteErrorCreateDefaults": "未找到站點預設值", - "method": "方法", - "siteMethodDescription": "這是您將如何顯示連接。", - "siteLearnNewt": "學習如何在您的系統上安裝 Newt", - "siteSeeConfigOnce": "您只能看到一次配置。", - "siteLoadWGConfig": "正在載入 WireGuard 配置...", - "siteDocker": "擴展 Docker 部署詳細資訊", - "toggle": "切換", - "dockerCompose": "Docker Compose", - "dockerRun": "Docker Run", - "siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多", - "siteConfirmCopy": "我已經複製了配置資訊", - "searchSitesProgress": "搜索站點...", - "siteAdd": "添加站點", - "siteInstallNewt": "安裝 Newt", - "siteInstallNewtDescription": "在您的系統中運行 Newt", - "WgConfiguration": "WireGuard 配置", - "WgConfigurationDescription": "使用以下配置連接到您的網路", - "operatingSystem": "操作系統", - "commands": "命令", - "recommended": "推薦", - "siteNewtDescription": "為獲得最佳用戶體驗,請使用 Newt。其底層採用 WireGuard 技術,可直接通過 Pangolin 控制台,使用區域網路地址訪問您私有網路中的資源。", - "siteRunsInDocker": "在 Docker 中運行", - "siteRunsInShell": "在 macOS 、 Linux 和 Windows 的 Shell 中運行", - "siteErrorDelete": "刪除站點出錯", - "siteErrorUpdate": "更新站點失敗", - "siteErrorUpdateDescription": "更新站點時出錯。", - "siteUpdated": "站點已更新", - "siteUpdatedDescription": "網站已更新。", - "siteGeneralDescription": "配置此站點的常規設置", - "siteSettingDescription": "配置您網站上的設置", - "siteSetting": "{siteName} 設置", - "siteNewtTunnel": "Newt 隧道 (推薦)", - "siteNewtTunnelDescription": "最簡單的方式來連接到您的網路。不需要任何額外設置。", - "siteWg": "基本 WireGuard", - "siteWgDescription": "使用任何 WireGuard 用戶端來建立隧道。需要手動配置 NAT。", - "siteWgDescriptionSaas": "使用任何 WireGuard 用戶端建立隧道。需要手動配置 NAT。僅適用於自託管節點。", - "siteLocalDescription": "僅限本地資源。不需要隧道。", - "siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。", - "siteSeeAll": "查看所有站點", - "siteTunnelDescription": "確定如何連接到您的網站", - "siteNewtCredentials": "Newt 憑證", - "siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑證", - "remoteNodeCredentialsDescription": "這是遠端節點與伺服器進行驗證的方式", - "siteCredentialsSave": "保存您的憑證", - "siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。", - "siteInfo": "站點資訊", - "status": "狀態", - "shareTitle": "管理共享連結", - "shareDescription": "創建可共享的連結,允許暫時或永久訪問您的資源", - "shareSearch": "搜索共享連結...", - "shareCreate": "創建共享連結", - "shareErrorDelete": "刪除連結失敗", - "shareErrorDeleteMessage": "刪除連結時出錯", - "shareDeleted": "連結已刪除", - "shareDeletedDescription": "連結已刪除", - "shareTokenDescription": "您的訪問令牌可以透過兩種方式傳遞:作為查詢參數或請求頭。 每次驗證訪問請求都必須從用戶端傳遞。", - "accessToken": "訪問令牌", - "usageExamples": "用法範例", - "tokenId": "令牌 ID", - "requestHeades": "請求頭", - "queryParameter": "查詢參數", - "importantNote": "重要提示", - "shareImportantDescription": "出於安全考慮,建議盡可能在使用請求頭傳遞參數,因為查詢參數可能會被瀏覽器歷史記錄或伺服器日誌記錄。", - "token": "令牌", - "shareTokenSecurety": "請妥善保管您的訪問令牌,不要將其暴露在公開訪問的區域或用戶端代碼中。", - "shareErrorFetchResource": "獲取資源失敗", - "shareErrorFetchResourceDescription": "獲取資源時出錯", - "shareErrorCreate": "無法創建共享連結", - "shareErrorCreateDescription": "創建共享連結時出錯", - "shareCreateDescription": "任何具有此連結的人都可以訪問資源", - "shareTitleOptional": "標題 (可選)", - "expireIn": "過期時間", - "neverExpire": "永不過期", - "shareExpireDescription": "過期時間是連結可以使用並提供對資源的訪問時間。 此時間後,連結將不再工作,使用此連結的用戶將失去對資源的訪問。", - "shareSeeOnce": "您只能看到一次此連結。請確保複製它。", - "shareAccessHint": "任何具有此連結的人都可以訪問該資源。小心地分享它。", - "shareTokenUsage": "查看訪問令牌使用情況", - "createLink": "創建連結", - "resourcesNotFound": "找不到資源", - "resourceSearch": "搜索資源", - "openMenu": "打開菜單", - "resource": "資源", - "title": "標題", - "created": "已創建", - "expires": "過期時間", - "never": "永不過期", - "shareErrorSelectResource": "請選擇一個資源", - "proxyResourceTitle": "管理公開資源", - "proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源", - "proxyResourcesBannerTitle": "基於網頁的公開存取", - "proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。", - "clientResourceTitle": "管理私有資源", - "clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源", - "privateResourcesBannerTitle": "零信任私有存取", - "privateResourcesBannerDescription": "私有資源使用零信任安全性,確保使用者和機器只能存取您明確授權的資源。連接使用者裝置或機器客戶端以透過安全的虛擬私人網路存取這些資源。", - "resourcesSearch": "搜索資源...", - "resourceAdd": "添加資源", - "resourceErrorDelte": "刪除資源時出錯", - "authentication": "認證", - "protected": "受到保護", - "notProtected": "未受到保護", - "resourceMessageRemove": "一旦刪除,資源將不再可訪問。與該資源相關的所有目標也將被刪除。", - "resourceQuestionRemove": "您確定要從組織中刪除資源嗎?", - "resourceHTTP": "HTTPS 資源", - "resourceHTTPDescription": "使用子域或根域名通過 HTTPS 向您的應用程式提出代理請求。", - "resourceRaw": "TCP/UDP 資源", - "resourceRawDescription": "使用 TCP/UDP 使用埠號向您的應用提出代理請求。", - "resourceCreate": "創建資源", - "resourceCreateDescription": "按照下面的步驟創建新資源", - "resourceSeeAll": "查看所有資源", - "resourceInfo": "資源資訊", - "resourceNameDescription": "這是資源的顯示名稱。", - "siteSelect": "選擇站點", - "siteSearch": "搜索站點", - "siteNotFound": "未找到站點。", - "selectCountry": "選擇國家", - "searchCountries": "搜索國家...", - "noCountryFound": "找不到國家。", - "siteSelectionDescription": "此站點將為目標提供連接。", - "resourceType": "資源類型", - "resourceTypeDescription": "確定如何訪問您的資源", - "resourceHTTPSSettings": "HTTPS 設置", - "resourceHTTPSSettingsDescription": "配置如何通過 HTTPS 訪問您的資源", - "domainType": "域類型", - "subdomain": "子域名", - "baseDomain": "根域名", - "subdomnainDescription": "您的資源可以訪問的子域名。", - "resourceRawSettings": "TCP/UDP 設置", - "resourceRawSettingsDescription": "設定如何透過 TCP/UDP 存取資源", - "protocol": "協議", - "protocolSelect": "選擇協議", - "resourcePortNumber": "埠號", - "resourcePortNumberDescription": "代理請求的外部埠號。", - "cancel": "取消", - "resourceConfig": "配置片段", - "resourceConfigDescription": "複製並黏貼這些配置片段以設置您的 TCP/UDP 資源", - "resourceAddEntrypoints": "Traefik: 添加入口點", - "resourceExposePorts": "Gerbil:在 Docker Compose 中顯示埠", - "resourceLearnRaw": "學習如何配置 TCP/UDP 資源", - "resourceBack": "返回資源", - "resourceGoTo": "轉到資源", - "resourceDelete": "刪除資源", - "resourceDeleteConfirm": "確認刪除資源", - "visibility": "可見性", - "enabled": "已啟用", - "disabled": "已禁用", - "general": "概覽", - "generalSettings": "常規設置", - "proxy": "代理伺服器", - "internal": "內部設置", - "rules": "規則", - "resourceSettingDescription": "配置您資源上的設置", - "resourceSetting": "{resourceName} 設置", - "alwaysAllow": "一律允許", - "alwaysDeny": "一律拒絕", - "passToAuth": "傳遞至認證", - "orgSettingsDescription": "配置您組織的一般設定", - "orgGeneralSettings": "組織設置", - "orgGeneralSettingsDescription": "管理您的機構詳細資訊和配置", - "saveGeneralSettings": "保存常規設置", - "saveSettings": "保存設置", - "orgDangerZone": "危險區域", - "orgDangerZoneDescription": "一旦刪除該組織,將無法恢復,請務必確認。", - "orgDelete": "刪除組織", - "orgDeleteConfirm": "確認刪除組織", - "orgMessageRemove": "此操作不可逆,這將刪除所有相關數據。", - "orgMessageConfirm": "要確認,請在下面輸入組織名稱。", - "orgQuestionRemove": "您確定要刪除組織嗎?", - "orgUpdated": "組織已更新", - "orgUpdatedDescription": "組織已更新。", - "orgErrorUpdate": "更新組織失敗", - "orgErrorUpdateMessage": "更新組織時出錯。", - "orgErrorFetch": "獲取組織失敗", - "orgErrorFetchMessage": "列出您的組織時出錯", - "orgErrorDelete": "刪除組織失敗", - "orgErrorDeleteMessage": "刪除組織時出錯。", - "orgDeleted": "組織已刪除", - "orgDeletedMessage": "組織及其數據已被刪除。", - "orgMissing": "缺少組織 ID", - "orgMissingMessage": "沒有組織ID,無法重新生成邀請。", - "accessUsersManage": "管理用戶", - "accessUsersDescription": "邀請用戶並位他們添加角色以管理訪問您的組織", - "accessUsersSearch": "搜索用戶...", - "accessUserCreate": "創建用戶", - "accessUserRemove": "刪除用戶", - "username": "使用者名稱", - "identityProvider": "身份提供商", - "role": "角色", - "nameRequired": "名稱是必填項", - "accessRolesManage": "管理角色", - "accessRolesDescription": "配置角色來管理訪問您的組織", - "accessRolesSearch": "搜索角色...", - "accessRolesAdd": "添加角色", - "accessRoleDelete": "刪除角色", - "description": "描述", - "inviteTitle": "打開邀請", - "inviteDescription": "管理您給其他用戶的邀請", - "inviteSearch": "搜索邀請...", - "minutes": "分鐘", - "hours": "小時", - "days": "天", - "weeks": "周", - "months": "月", - "years": "年", - "day": "{count, plural, other {# 天}}", - "apiKeysTitle": "API 金鑰", - "apiKeysConfirmCopy2": "您必須確認您已複製 API 金鑰。", - "apiKeysErrorCreate": "創建 API 金鑰出錯", - "apiKeysErrorSetPermission": "設置權限出錯", - "apiKeysCreate": "生成 API 金鑰", - "apiKeysCreateDescription": "為您的組織生成一個新的 API 金鑰", - "apiKeysGeneralSettings": "權限", - "apiKeysGeneralSettingsDescription": "確定此 API 金鑰可以做什麼", - "apiKeysList": "您的 API 金鑰", - "apiKeysSave": "保存您的 API 金鑰", - "apiKeysSaveDescription": "該資訊僅會顯示一次,請確保將其複製到安全的位置。", - "apiKeysInfo": "您的 API 金鑰是:", - "apiKeysConfirmCopy": "我已複製 API 金鑰", - "generate": "生成", - "done": "完成", - "apiKeysSeeAll": "查看所有 API 金鑰", - "apiKeysPermissionsErrorLoadingActions": "載入 API 金鑰操作時出錯", - "apiKeysPermissionsErrorUpdate": "設置權限出錯", - "apiKeysPermissionsUpdated": "權限已更新", - "apiKeysPermissionsUpdatedDescription": "權限已更新。", - "apiKeysPermissionsGeneralSettings": "權限", - "apiKeysPermissionsGeneralSettingsDescription": "確定此 API 金鑰可以做什麼", - "apiKeysPermissionsSave": "保存權限", - "apiKeysPermissionsTitle": "權限", - "apiKeys": "API 金鑰", - "searchApiKeys": "搜索 API 金鑰...", - "apiKeysAdd": "生成 API 金鑰", - "apiKeysErrorDelete": "刪除 API 金鑰出錯", - "apiKeysErrorDeleteMessage": "刪除 API 金鑰出錯", - "apiKeysQuestionRemove": "您確定要從組織中刪除 API 金鑰嗎?", - "apiKeysMessageRemove": "一旦刪除,此API金鑰將無法被使用。", - "apiKeysDeleteConfirm": "確認刪除 API 金鑰", - "apiKeysDelete": "刪除 API 金鑰", - "apiKeysManage": "管理 API 金鑰", - "apiKeysDescription": "API 金鑰用於認證集成 API", - "apiKeysSettings": "{apiKeyName} 設置", - "userTitle": "管理所有用戶", - "userDescription": "查看和管理系統中的所有用戶", - "userAbount": "關於用戶管理", - "userAbountDescription": "此表格顯示系統中所有根用戶對象。每個用戶可能屬於多個組織。 從組織中刪除用戶不會刪除其根用戶對象 - 他們將保留在系統中。 要從系統中完全刪除用戶,您必須使用此表格中的刪除操作刪除其根用戶對象。", - "userServer": "伺服器用戶", - "userSearch": "搜索伺服器用戶...", - "userErrorDelete": "刪除用戶時出錯", - "userDeleteConfirm": "確認刪除用戶", - "userDeleteServer": "從伺服器刪除用戶", - "userMessageRemove": "該用戶將被從所有組織中刪除並完全從伺服器中刪除。", - "userQuestionRemove": "您確定要從伺服器永久刪除用戶嗎?", - "licenseKey": "許可證金鑰", - "valid": "有效", - "numberOfSites": "站點數量", - "licenseKeySearch": "搜索許可證金鑰...", - "licenseKeyAdd": "添加許可證金鑰", - "type": "類型", - "licenseKeyRequired": "需要許可證金鑰", - "licenseTermsAgree": "您必須同意許可條款", - "licenseErrorKeyLoad": "載入許可證金鑰失敗", - "licenseErrorKeyLoadDescription": "載入許可證金鑰時出錯。", - "licenseErrorKeyDelete": "刪除許可證金鑰失敗", - "licenseErrorKeyDeleteDescription": "刪除許可證金鑰時出錯。", - "licenseKeyDeleted": "許可證金鑰已刪除", - "licenseKeyDeletedDescription": "許可證金鑰已被刪除。", - "licenseErrorKeyActivate": "啟用許可證金鑰失敗", - "licenseErrorKeyActivateDescription": "啟用許可證金鑰時出錯。", - "licenseAbout": "關於許可協議", - "communityEdition": "社區版", - "licenseAboutDescription": "這是針對商業環境中使用Pangolin的商業和企業用戶。 如果您正在使用 Pangolin 供個人使用,您可以忽略此部分。", - "licenseKeyActivated": "授權金鑰已啟用", - "licenseKeyActivatedDescription": "已成功啟用許可證金鑰。", - "licenseErrorKeyRecheck": "重新檢查許可證金鑰失敗", - "licenseErrorKeyRecheckDescription": "重新檢查許可證金鑰時出錯。", - "licenseErrorKeyRechecked": "重新檢查許可證金鑰", - "licenseErrorKeyRecheckedDescription": "已重新檢查所有許可證金鑰", - "licenseActivateKey": "啟用許可證金鑰", - "licenseActivateKeyDescription": "輸入一個許可金鑰來啟用它。", - "licenseActivate": "啟用許可證", - "licenseAgreement": "通過檢查此框,您確認您已經閱讀並同意與您的許可證金鑰相關的許可條款。", - "fossorialLicense": "查看Fossorial Commercial License和訂閱條款", - "licenseMessageRemove": "這將刪除許可證金鑰和它授予的所有相關權限。", - "licenseMessageConfirm": "要確認,請在下面輸入許可證金鑰。", - "licenseQuestionRemove": "您確定要刪除許可證金鑰?", - "licenseKeyDelete": "刪除許可證金鑰", - "licenseKeyDeleteConfirm": "確認刪除許可證金鑰", - "licenseTitle": "管理許可證狀態", - "licenseTitleDescription": "查看和管理系統中的許可證金鑰", - "licenseHost": "主機許可證", - "licenseHostDescription": "管理主機的主許可證金鑰。", - "licensedNot": "未授權", - "hostId": "主機 ID", - "licenseReckeckAll": "重新檢查所有金鑰", - "licenseSiteUsage": "站點使用情況", - "licenseSiteUsageDecsription": "查看使用此許可的站點數量。", - "licenseNoSiteLimit": "使用未經許可主機的站點數量沒有限制。", - "licensePurchase": "購買許可證", - "licensePurchaseSites": "購買更多站點", - "licenseSitesUsedMax": "使用了 {usedSites}/{maxSites} 個站點", - "licenseSitesUsed": "{count, plural, =0 {# 站點} one {# 站點} other {# 站點}}", - "licensePurchaseDescription": "請選擇您希望 {selectedMode, select, license {直接購買許可證,您可以隨時增加更多站點。} other {為現有許可證購買更多站點}}", - "licenseFee": "許可證費用", - "licensePriceSite": "每個站點的價格", - "total": "總計", - "licenseContinuePayment": "繼續付款", - "pricingPage": "定價頁面", - "pricingPortal": "前往付款頁面", - "licensePricingPage": "關於最新的價格和折扣,請訪問 ", - "invite": "邀請", - "inviteRegenerate": "重新生成邀請", - "inviteRegenerateDescription": "撤銷以前的邀請並創建一個新的邀請", - "inviteRemove": "移除邀請", - "inviteRemoveError": "刪除邀請失敗", - "inviteRemoveErrorDescription": "刪除邀請時出錯。", - "inviteRemoved": "邀請已刪除", - "inviteRemovedDescription": "為 {email} 創建的邀請已刪除", - "inviteQuestionRemove": "您確定要刪除邀請嗎?", - "inviteMessageRemove": "一旦刪除,這個邀請將不再有效。", - "inviteMessageConfirm": "要確認,請在下面輸入邀請的電子郵件地址。", - "inviteQuestionRegenerate": "您確定要重新邀請 {email} 嗎?這將會撤銷掉之前的邀請", - "inviteRemoveConfirm": "確認刪除邀請", - "inviteRegenerated": "重新生成邀請", - "inviteSent": "邀請郵件已成功發送至 {email}。", - "inviteSentEmail": "發送電子郵件通知給用戶", - "inviteGenerate": "已為 {email} 創建新的邀請。", - "inviteDuplicateError": "重複的邀請", - "inviteDuplicateErrorDescription": "此用戶的邀請已存在。", - "inviteRateLimitError": "超出速率限制", - "inviteRateLimitErrorDescription": "您超過了每小時3次再生的限制。請稍後再試。", - "inviteRegenerateError": "重新生成邀請失敗", - "inviteRegenerateErrorDescription": "重新生成邀請時出錯。", - "inviteValidityPeriod": "有效期", - "inviteValidityPeriodSelect": "選擇有效期", - "inviteRegenerateMessage": "邀請已重新生成。用戶必須訪問下面的連結才能接受邀請。", - "inviteRegenerateButton": "重新生成", - "expiresAt": "到期於", - "accessRoleUnknown": "未知角色", - "placeholder": "占位符", - "userErrorOrgRemove": "刪除用戶失敗", - "userErrorOrgRemoveDescription": "刪除用戶時出錯。", - "userOrgRemoved": "用戶已刪除", - "userOrgRemovedDescription": "已將 {email} 從組織中移除。", - "userQuestionOrgRemove": "您確定要從組織中刪除此用戶嗎?", - "userMessageOrgRemove": "一旦刪除,這個用戶將不再能夠訪問組織。 你總是可以稍後重新邀請他們,但他們需要再次接受邀請。", - "userRemoveOrgConfirm": "確認刪除用戶", - "userRemoveOrg": "從組織中刪除用戶", - "users": "用戶", - "accessRoleMember": "成員", - "accessRoleOwner": "所有者", - "userConfirmed": "已確認", - "idpNameInternal": "內部設置", - "emailInvalid": "無效的電子郵件地址", - "inviteValidityDuration": "請選擇持續時間", - "accessRoleSelectPlease": "請選擇一個角色", - "usernameRequired": "必須輸入使用者名稱", - "idpSelectPlease": "請選擇身份提供商", - "idpGenericOidc": "通用的 OAuth2/OIDC 提供商。", - "accessRoleErrorFetch": "獲取角色失敗", - "accessRoleErrorFetchDescription": "獲取角色時出錯", - "idpErrorFetch": "獲取身份提供者失敗", - "idpErrorFetchDescription": "獲取身份提供者時出錯", - "userErrorExists": "用戶已存在", - "userErrorExistsDescription": "此用戶已經是組織成員。", - "inviteError": "邀請用戶失敗", - "inviteErrorDescription": "邀請用戶時出錯", - "userInvited": "用戶邀請", - "userInvitedDescription": "用戶已被成功邀請。", - "userErrorCreate": "創建用戶失敗", - "userErrorCreateDescription": "創建用戶時出錯", - "userCreated": "用戶已創建", - "userCreatedDescription": "用戶已成功創建。", - "userTypeInternal": "內部用戶", - "userTypeInternalDescription": "邀請用戶直接加入您的組織。", - "userTypeExternal": "外部用戶", - "userTypeExternalDescription": "創建一個具有外部身份提供商的用戶。", - "accessUserCreateDescription": "按照下面的步驟創建一個新用戶", - "userSeeAll": "查看所有用戶", - "userTypeTitle": "用戶類型", - "userTypeDescription": "確定如何創建用戶", - "userSettings": "用戶資訊", - "userSettingsDescription": "輸入新用戶的詳細資訊", - "inviteEmailSent": "發送邀請郵件給用戶", - "inviteValid": "有效", - "selectDuration": "選擇持續時間", - "selectResource": "選擇資源", - "filterByResource": "依資源篩選", - "resetFilters": "重設篩選條件", - "totalBlocked": "被 Pangolin 阻擋的請求", - "totalRequests": "總請求數", - "requestsByCountry": "依國家/地區的請求", - "requestsByDay": "依日期的請求", - "blocked": "已阻擋", - "allowed": "已允許", - "topCountries": "熱門國家/地區", - "accessRoleSelect": "選擇角色", - "inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。", - "inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。", - "inviteExpiresIn": "邀請將在{days, plural, other {# 天}}後過期。", - "idpTitle": "身份提供商", - "idpSelect": "為外部用戶選擇身份提供商", - "idpNotConfigured": "沒有配置身份提供者。請在創建外部用戶之前配置身份提供者。", - "usernameUniq": "這必須匹配所選身份提供者中存在的唯一使用者名稱。", - "emailOptional": "電子郵件(可選)", - "nameOptional": "名稱(可選)", - "accessControls": "訪問控制", - "userDescription2": "管理此用戶的設置", - "accessRoleErrorAdd": "添加用戶到角色失敗", - "accessRoleErrorAddDescription": "添加用戶到角色時出錯。", - "userSaved": "用戶已保存", - "userSavedDescription": "用戶已更新。", - "autoProvisioned": "自動設置", - "autoProvisionedDescription": "允許此用戶由身份提供商自動管理", - "accessControlsDescription": "管理此用戶在組織中可以訪問和做什麼", - "accessControlsSubmit": "保存訪問控制", - "roles": "角色", - "accessUsersRoles": "管理用戶和角色", - "accessUsersRolesDescription": "邀請用戶並將他們添加到角色以管理訪問您的組織", - "key": "關鍵字", - "createdAt": "創建於", - "proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式,或將空保存為取消自訂 Header。", - "proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。", - "proxyEnableSSL": "啟用 SSL", - "proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。", - "target": "目標", - "configureTarget": "配置目標", - "targetErrorFetch": "獲取目標失敗", - "targetErrorFetchDescription": "獲取目標時出錯", - "siteErrorFetch": "獲取資源失敗", - "siteErrorFetchDescription": "獲取資源時出錯", - "targetErrorDuplicate": "重複的目標", - "targetErrorDuplicateDescription": "具有這些設置的目標已存在", - "targetWireGuardErrorInvalidIp": "無效的目標IP", - "targetWireGuardErrorInvalidIpDescription": "目標IP必須在站點子網內", - "targetsUpdated": "目標已更新", - "targetsUpdatedDescription": "目標和設置更新成功", - "targetsErrorUpdate": "更新目標失敗", - "targetsErrorUpdateDescription": "更新目標時出錯", - "targetTlsUpdate": "TLS 設置已更新", - "targetTlsUpdateDescription": "您的 TLS 設置已成功更新", - "targetErrorTlsUpdate": "更新 TLS 設置失敗", - "targetErrorTlsUpdateDescription": "更新 TLS 設置時出錯", - "proxyUpdated": "代理設置已更新", - "proxyUpdatedDescription": "您的代理設置已成功更新", - "proxyErrorUpdate": "更新代理設置失敗", - "proxyErrorUpdateDescription": "更新代理設置時出錯", - "targetAddr": "IP / 域名", - "targetPort": "埠", - "targetProtocol": "協議", - "targetTlsSettings": "安全連接配置", - "targetTlsSettingsDescription": "配置資源的 SSL/TLS 設置", - "targetTlsSettingsAdvanced": "高級TLS設置", - "targetTlsSni": "TLS 伺服器名稱", - "targetTlsSniDescription": "SNI使用的 TLS 伺服器名稱。留空使用預設值。", - "targetTlsSubmit": "保存設置", - "targets": "目標配置", - "targetsDescription": "設置目標來路由流量到您的後端服務", - "targetStickySessions": "啟用置頂會話", - "targetStickySessionsDescription": "將連接保持在同一個後端目標的整個會話中。", - "methodSelect": "選擇方法", - "targetSubmit": "添加目標", - "targetNoOne": "此資源沒有任何目標。添加目標來配置向您後端發送請求的位置。", - "targetNoOneDescription": "在上面添加多個目標將啟用負載平衡。", - "targetsSubmit": "保存目標", - "addTarget": "添加目標", - "targetErrorInvalidIp": "無效的 IP 地址", - "targetErrorInvalidIpDescription": "請輸入有效的IP位址或主機名", - "targetErrorInvalidPort": "無效的埠", - "targetErrorInvalidPortDescription": "請輸入有效的埠號", - "targetErrorNoSite": "沒有選擇站點", - "targetErrorNoSiteDescription": "請選擇目標站點", - "targetCreated": "目標已創建", - "targetCreatedDescription": "目標已成功創建", - "targetErrorCreate": "創建目標失敗", - "targetErrorCreateDescription": "創建目標時出錯", - "tlsServerName": "TLS 伺服器名稱", - "tlsServerNameDescription": "用於 SNI 的 TLS 伺服器名稱", - "save": "保存", - "proxyAdditional": "附加代理設置", - "proxyAdditionalDescription": "配置你的資源如何處理代理設置", - "proxyCustomHeader": "自訂主機 Header", - "proxyCustomHeaderDescription": "代理請求時設置的 Header。留空則使用預設值。", - "proxyAdditionalSubmit": "保存代理設置", - "subnetMaskErrorInvalid": "子網掩碼無效。必須在 0 和 32 之間。", - "ipAddressErrorInvalidFormat": "無效的 IP 地址格式", - "ipAddressErrorInvalidOctet": "無效的 IP 地址", - "path": "路徑", - "matchPath": "匹配路徑", - "ipAddressRange": "IP 範圍", - "rulesErrorFetch": "獲取規則失敗", - "rulesErrorFetchDescription": "獲取規則時出錯", - "rulesErrorDuplicate": "複製規則", - "rulesErrorDuplicateDescription": "帶有這些設置的規則已存在", - "rulesErrorInvalidIpAddressRange": "無效的 CIDR", - "rulesErrorInvalidIpAddressRangeDescription": "請輸入一個有效的 CIDR 值", - "rulesErrorInvalidUrl": "無效的 URL 路徑", - "rulesErrorInvalidUrlDescription": "請輸入一個有效的 URL 路徑值", - "rulesErrorInvalidIpAddress": "無效的 IP", - "rulesErrorInvalidIpAddressDescription": "請輸入一個有效的IP位址", - "rulesErrorUpdate": "更新規則失敗", - "rulesErrorUpdateDescription": "更新規則時出錯", - "rulesUpdated": "啟用規則", - "rulesUpdatedDescription": "規則已更新", - "rulesMatchIpAddressRangeDescription": "以 CIDR 格式輸入地址(如:103.21.244.0/22)", - "rulesMatchIpAddress": "輸入IP位址(例如,103.21.244.12)", - "rulesMatchUrl": "輸入一個 URL 路徑或模式(例如/api/v1/todos 或 /api/v1/*)", - "rulesErrorInvalidPriority": "無效的優先度", - "rulesErrorInvalidPriorityDescription": "請輸入一個有效的優先度", - "rulesErrorDuplicatePriority": "重複的優先度", - "rulesErrorDuplicatePriorityDescription": "請輸入唯一的優先度", - "ruleUpdated": "規則已更新", - "ruleUpdatedDescription": "規則更新成功", - "ruleErrorUpdate": "操作失敗", - "ruleErrorUpdateDescription": "保存過程中發生錯誤", - "rulesPriority": "優先權", - "rulesAction": "行為", - "rulesMatchType": "匹配類型", - "value": "值", - "rulesAbout": "關於規則", - "rulesAboutDescription": "規則使您能夠依據特定條件控制資源訪問權限。您可以創建基於 IP 地址或 URL 路徑的規則,以允許或拒絕訪問。", - "rulesActions": "行動", - "rulesActionAlwaysAllow": "總是允許:繞過所有身份驗證方法", - "rulesActionAlwaysDeny": "總是拒絕:阻止所有請求;無法嘗試驗證", - "rulesActionPassToAuth": "傳遞至認證:允許嘗試身份驗證方法", - "rulesMatchCriteria": "匹配條件", - "rulesMatchCriteriaIpAddress": "匹配一個指定的 IP 地址", - "rulesMatchCriteriaIpAddressRange": "在 CIDR 符號中匹配一系列IP位址", - "rulesMatchCriteriaUrl": "匹配一個 URL 路徑或模式", - "rulesEnable": "啟用規則", - "rulesEnableDescription": "啟用或禁用此資源的規則評估", - "rulesResource": "資源規則配置", - "rulesResourceDescription": "配置規則來控制對您資源的訪問", - "ruleSubmit": "添加規則", - "rulesNoOne": "沒有規則。使用表單添加規則。", - "rulesOrder": "規則按優先順序評定。", - "rulesSubmit": "保存規則", - "resourceErrorCreate": "創建資源時出錯", - "resourceErrorCreateDescription": "創建資源時出錯", - "resourceErrorCreateMessage": "創建資源時發生錯誤:", - "resourceErrorCreateMessageDescription": "發生意外錯誤", - "sitesErrorFetch": "獲取站點出錯", - "sitesErrorFetchDescription": "獲取站點時出錯", - "domainsErrorFetch": "獲取域名出錯", - "domainsErrorFetchDescription": "獲取域時出錯", - "none": "無", - "unknown": "未知", - "resources": "資源", - "resourcesDescription": "資源是您私有網路中運行的應用程式的代理。您可以為私有網路中的任何 HTTP/HTTPS 或 TCP/UDP 服務創建資源。每個資源都必須連接到一個站點,以通過加密的 WireGuard 隧道實現私密且安全的連接。", - "resourcesWireGuardConnect": "採用 WireGuard 提供的加密安全連接", - "resourcesMultipleAuthenticationMethods": "配置多個身份驗證方法", - "resourcesUsersRolesAccess": "基於用戶和角色的訪問控制", - "resourcesErrorUpdate": "切換資源失敗", - "resourcesErrorUpdateDescription": "更新資源時出錯", - "access": "訪問權限", - "shareLink": "{resource} 的分享連結", - "resourceSelect": "選擇資源", - "shareLinks": "分享連結", - "share": "分享連結", - "shareDescription2": "創建資源共享連結。連結提供對資源的臨時或無限制訪問。 當您創建連結時,您可以配置連結的到期時間。", - "shareEasyCreate": "輕鬆創建和分享", - "shareConfigurableExpirationDuration": "可配置的過期時間", - "shareSecureAndRevocable": "安全和可撤銷的", - "nameMin": "名稱長度必須大於 {len} 字元。", - "nameMax": "名稱長度必須小於 {len} 字元。", - "sitesConfirmCopy": "請確認您已經複製了配置。", - "unknownCommand": "未知命令", - "newtErrorFetchReleases": "無法獲取版本資訊: {err}", - "newtErrorFetchLatest": "無法獲取最新版資訊: {err}", - "newtEndpoint": "Newt 端點", - "newtId": "Newt ID", - "newtSecretKey": "Newt 私鑰", - "architecture": "架構", - "sites": "站點", - "siteWgAnyClients": "使用任何 WireGuard 用戶端連接。您必須使用對等IP解決您的內部資源。", - "siteWgCompatibleAllClients": "與所有 WireGuard 用戶端相容", - "siteWgManualConfigurationRequired": "需要手動配置", - "userErrorNotAdminOrOwner": "用戶不是管理員或所有者", - "pangolinSettings": "設置 - Pangolin", - "accessRoleYour": "您的角色:", - "accessRoleSelect2": "選擇角色", - "accessUserSelect": "選擇一個用戶", - "otpEmailEnter": "輸入電子郵件", - "otpEmailEnterDescription": "在輸入欄位輸入後按 Enter 鍵添加電子郵件。", - "otpEmailErrorInvalid": "無效的信箱地址。通配符(*)必須占據整個開頭部分。", - "otpEmailSmtpRequired": "需要先配置 SMTP", - "otpEmailSmtpRequiredDescription": "必須在伺服器上啟用 SMTP 才能使用一次性密碼驗證。", - "otpEmailTitle": "一次性密碼", - "otpEmailTitleDescription": "資源訪問需要基於電子郵件的身份驗證", - "otpEmailWhitelist": "電子郵件白名單", - "otpEmailWhitelistList": "白名單郵件", - "otpEmailWhitelistListDescription": "只有擁有這些電子郵件地址的用戶才能訪問此資源。 他們將被提示輸入一次性密碼發送到他們的電子郵件。 通配符 (*@example.com) 可以用來允許來自一個域名的任何電子郵件地址。", - "otpEmailWhitelistSave": "保存白名單", - "passwordAdd": "添加密碼", - "passwordRemove": "刪除密碼", - "pincodeAdd": "添加 PIN 碼", - "pincodeRemove": "移除 PIN 碼", - "resourceAuthMethods": "身份驗證方法", - "resourceAuthMethodsDescriptions": "允許透過額外的認證方法訪問資源", - "resourceAuthSettingsSave": "保存成功", - "resourceAuthSettingsSaveDescription": "已保存身份驗證設置", - "resourceErrorAuthFetch": "獲取數據失敗", - "resourceErrorAuthFetchDescription": "獲取數據時出錯", - "resourceErrorPasswordRemove": "刪除資源密碼出錯", - "resourceErrorPasswordRemoveDescription": "刪除資源密碼時出錯", - "resourceErrorPasswordSetup": "設置資源密碼出錯", - "resourceErrorPasswordSetupDescription": "設置資源密碼時出錯", - "resourceErrorPincodeRemove": "刪除資源固定碼時出錯", - "resourceErrorPincodeRemoveDescription": "刪除資源PIN碼時出錯", - "resourceErrorPincodeSetup": "設置資源 PIN 碼時出錯", - "resourceErrorPincodeSetupDescription": "設置資源 PIN 碼時發生錯誤", - "resourceErrorUsersRolesSave": "設置角色失敗", - "resourceErrorUsersRolesSaveDescription": "設置角色時出錯", - "resourceErrorWhitelistSave": "保存白名單失敗", - "resourceErrorWhitelistSaveDescription": "保存白名單時出錯", - "resourcePasswordSubmit": "啟用密碼保護", - "resourcePasswordProtection": "密碼保護 {status}", - "resourcePasswordRemove": "已刪除資源密碼", - "resourcePasswordRemoveDescription": "已成功刪除資源密碼", - "resourcePasswordSetup": "設置資源密碼", - "resourcePasswordSetupDescription": "已成功設置資源密碼", - "resourcePasswordSetupTitle": "設置密碼", - "resourcePasswordSetupTitleDescription": "設置密碼來保護此資源", - "resourcePincode": "PIN 碼", - "resourcePincodeSubmit": "啟用 PIN 碼保護", - "resourcePincodeProtection": "PIN 碼保護 {status}", - "resourcePincodeRemove": "資源 PIN 碼已刪除", - "resourcePincodeRemoveDescription": "已成功刪除資源 PIN 碼", - "resourcePincodeSetup": "資源 PIN 碼已設置", - "resourcePincodeSetupDescription": "資源 PIN 碼已成功設置", - "resourcePincodeSetupTitle": "設置 PIN 碼", - "resourcePincodeSetupTitleDescription": "設置 PIN 碼來保護此資源", - "resourceRoleDescription": "管理員總是可以訪問此資源。", - "resourceUsersRoles": "用戶和角色", - "resourceUsersRolesDescription": "配置用戶和角色可以訪問此資源", - "resourceUsersRolesSubmit": "保存用戶和角色", - "resourceWhitelistSave": "保存成功", - "resourceWhitelistSaveDescription": "白名單設置已保存", - "ssoUse": "使用平台 SSO", - "ssoUseDescription": "對於所有啟用此功能的資源,現有用戶只需登錄一次。", - "proxyErrorInvalidPort": "無效的埠號", - "subdomainErrorInvalid": "無效的子域", - "domainErrorFetch": "獲取域名失敗", - "domainErrorFetchDescription": "獲取域名時出錯", - "resourceErrorUpdate": "更新資源失敗", - "resourceErrorUpdateDescription": "更新資源時出錯", - "resourceUpdated": "資源已更新", - "resourceUpdatedDescription": "資源已成功更新", - "resourceErrorTransfer": "轉移資源失敗", - "resourceErrorTransferDescription": "轉移資源時出錯", - "resourceTransferred": "資源已傳輸", - "resourceTransferredDescription": "資源已成功傳輸", - "resourceErrorToggle": "切換資源失敗", - "resourceErrorToggleDescription": "更新資源時出錯", - "resourceVisibilityTitle": "可見性", - "resourceVisibilityTitleDescription": "完全啟用或禁用資源可見性", - "resourceGeneral": "常規設置", - "resourceGeneralDescription": "配置此資源的常規設置", - "resourceEnable": "啟用資源", - "resourceTransfer": "轉移資源", - "resourceTransferDescription": "將此資源轉移到另一個站點", - "resourceTransferSubmit": "轉移資源", - "siteDestination": "目標站點", - "searchSites": "搜索站點", - "countries": "國家/地區", - "accessRoleCreate": "創建角色", - "accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。", - "accessRoleCreateSubmit": "創建角色", - "accessRoleCreated": "角色已創建", - "accessRoleCreatedDescription": "角色已成功創建。", - "accessRoleErrorCreate": "創建角色失敗", - "accessRoleErrorCreateDescription": "創建角色時出錯。", - "accessRoleErrorNewRequired": "需要新角色", - "accessRoleErrorRemove": "刪除角色失敗", - "accessRoleErrorRemoveDescription": "刪除角色時出錯。", - "accessRoleName": "角色名稱", - "accessRoleQuestionRemove": "您即將刪除 {name} 角色。 此操作無法撤銷。", - "accessRoleRemove": "刪除角色", - "accessRoleRemoveDescription": "從組織中刪除角色", - "accessRoleRemoveSubmit": "刪除角色", - "accessRoleRemoved": "角色已刪除", - "accessRoleRemovedDescription": "角色已成功刪除。", - "accessRoleRequiredRemove": "刪除此角色之前,請選擇一個新角色來轉移現有成員。", - "manage": "管理", - "sitesNotFound": "未找到站點。", - "pangolinServerAdmin": "伺服器管理員 - Pangolin", - "licenseTierProfessional": "專業許可證", - "licenseTierEnterprise": "企業許可證", - "licenseTierPersonal": "個人許可證", - "licensed": "已授權", - "yes": "是", - "no": "否", - "sitesAdditional": "其他站點", - "licenseKeys": "許可證金鑰", - "sitestCountDecrease": "減少站點數量", - "sitestCountIncrease": "增加站點數量", - "idpManage": "管理身份提供商", - "idpManageDescription": "查看和管理系統中的身份提供商", - "idpDeletedDescription": "身份提供商刪除成功", - "idpOidc": "OAuth2/OIDC", - "idpQuestionRemove": "您確定要永久刪除身份提供者嗎?", - "idpMessageRemove": "這將刪除身份提供者和所有相關的配置。透過此提供者進行身份驗證的用戶將無法登錄。", - "idpMessageConfirm": "要確認,請在下面輸入身份提供者的名稱。", - "idpConfirmDelete": "確認刪除身份提供商", - "idpDelete": "刪除身份提供商", - "idp": "身份提供商", - "idpSearch": "搜索身份提供者...", - "idpAdd": "添加身份提供商", - "idpClientIdRequired": "用戶端 ID 是必需的。", - "idpClientSecretRequired": "用戶端金鑰是必需的。", - "idpErrorAuthUrlInvalid": "身份驗證 URL 必須是有效的 URL。", - "idpErrorTokenUrlInvalid": "令牌 URL 必須是有效的 URL。", - "idpPathRequired": "標識符路徑是必需的。", - "idpScopeRequired": "授權範圍是必需的。", - "idpOidcDescription": "配置 OpenID 連接身份提供商", - "idpCreatedDescription": "身份提供商創建成功", - "idpCreate": "創建身份提供商", - "idpCreateDescription": "配置用戶身份驗證的新身份提供商", - "idpSeeAll": "查看所有身份提供商", - "idpSettingsDescription": "配置身份提供者的基本資訊", - "idpDisplayName": "此身份提供商的顯示名稱", - "idpAutoProvisionUsers": "自動提供用戶", - "idpAutoProvisionUsersDescription": "如果啟用,用戶將在首次登錄時自動在系統中創建,並且能夠映射用戶到角色和組織。", - "licenseBadge": "EE", - "idpType": "提供者類型", - "idpTypeDescription": "選擇您想要配置的身份提供者類型", - "idpOidcConfigure": "OAuth2/OIDC 配置", - "idpOidcConfigureDescription": "配置 OAuth2/OIDC 供應商端點和憑據", - "idpClientId": "用戶端ID", - "idpClientIdDescription": "來自您身份提供商的 OAuth2 用戶端 ID", - "idpClientSecret": "用戶端金鑰", - "idpClientSecretDescription": "來自身份提供商的 OAuth2 用戶端金鑰", - "idpAuthUrl": "授權 URL", - "idpAuthUrlDescription": "OAuth2 授權端點的 URL", - "idpTokenUrl": "令牌 URL", - "idpTokenUrlDescription": "OAuth2 令牌端點的 URL", - "idpOidcConfigureAlert": "重要提示", - "idpOidcConfigureAlertDescription": "創建身份提供方後,您需要在其設置中配置回調 URL。回調 URL 會在創建成功後提供。", - "idpToken": "令牌配置", - "idpTokenDescription": "配置如何從 ID 令牌中提取用戶資訊", - "idpJmespathAbout": "關於 JMESPath", - "idpJmespathAboutDescription": "以下路徑使用 JMESPath 語法從 ID 令牌中提取值。", - "idpJmespathAboutDescriptionLink": "了解更多 JMESPath 資訊", - "idpJmespathLabel": "標識符路徑", - "idpJmespathLabelDescription": "ID 令牌中用戶標識符的路徑", - "idpJmespathEmailPathOptional": "信箱路徑(可選)", - "idpJmespathEmailPathOptionalDescription": "ID 令牌中用戶信箱的路徑", - "idpJmespathNamePathOptional": "使用者名稱路徑(可選)", - "idpJmespathNamePathOptionalDescription": "ID 令牌中使用者名稱的路徑", - "idpOidcConfigureScopes": "作用域(Scopes)", - "idpOidcConfigureScopesDescription": "以空格分隔的 OAuth2 請求作用域列表", - "idpSubmit": "創建身份提供商", - "orgPolicies": "組織策略", - "idpSettings": "{idpName} 設置", - "idpCreateSettingsDescription": "配置身份提供商的設置", - "roleMapping": "角色映射", - "orgMapping": "組織映射", - "orgPoliciesSearch": "搜索組織策略...", - "orgPoliciesAdd": "添加組織策略", - "orgRequired": "組織是必填項", - "error": "錯誤", - "success": "成功", - "orgPolicyAddedDescription": "策略添加成功", - "orgPolicyUpdatedDescription": "策略更新成功", - "orgPolicyDeletedDescription": "已成功刪除策略", - "defaultMappingsUpdatedDescription": "默認映射更新成功", - "orgPoliciesAbout": "關於組織政策", - "orgPoliciesAboutDescription": "組織策略用於根據用戶的 ID 令牌來控制對組織的訪問。 您可以指定 JMESPath 表達式來提取角色和組織資訊從 ID 令牌中提取資訊。", - "orgPoliciesAboutDescriptionLink": "欲了解更多資訊,請參閱文件。", - "defaultMappingsOptional": "默認映射(可選)", - "defaultMappingsOptionalDescription": "當沒有為某個組織定義組織的政策時,使用默認映射。 您可以指定默認角色和組織映射回到這裡。", - "defaultMappingsRole": "默認角色映射", - "defaultMappingsRoleDescription": "此表達式的結果必須返回組織中定義的角色名稱作為字串。", - "defaultMappingsOrg": "默認組織映射", - "defaultMappingsOrgDescription": "此表達式必須返回 組織ID 或 true 才能允許用戶訪問組織。", - "defaultMappingsSubmit": "保存默認映射", - "orgPoliciesEdit": "編輯組織策略", - "org": "組織", - "orgSelect": "選擇組織", - "orgSearch": "搜索", - "orgNotFound": "找不到組織。", - "roleMappingPathOptional": "角色映射路徑(可選)", - "orgMappingPathOptional": "組織映射路徑(可選)", - "orgPolicyUpdate": "更新策略", - "orgPolicyAdd": "添加策略", - "orgPolicyConfig": "配置組織訪問權限", - "idpUpdatedDescription": "身份提供商更新成功", - "redirectUrl": "重定向網址", - "orgIdpRedirectUrls": "重新導向網址", - "redirectUrlAbout": "關於重定向網址", - "redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。", - "pangolinAuth": "認證 - Pangolin", - "verificationCodeLengthRequirements": "您的驗證碼必須是 8 個字元。", - "errorOccurred": "發生錯誤", - "emailErrorVerify": "驗證電子郵件失敗:", - "emailVerified": "電子郵件驗證成功!重定向您...", - "verificationCodeErrorResend": "無法重新發送驗證碼:", - "verificationCodeResend": "驗證碼已重新發送", - "verificationCodeResendDescription": "我們已將驗證碼重新發送到您的電子郵件地址。請檢查您的收件箱。", - "emailVerify": "驗證電子郵件", - "emailVerifyDescription": "輸入驗證碼發送到您的電子郵件地址。", - "verificationCode": "驗證碼", - "verificationCodeEmailSent": "我們向您的電子郵件地址發送了驗證碼。", - "submit": "提交", - "emailVerifyResendProgress": "正在重新發送...", - "emailVerifyResend": "沒有收到代碼?點擊此處重新發送", - "passwordNotMatch": "密碼不匹配", - "signupError": "註冊時出錯", - "pangolinLogoAlt": "Pangolin 標誌", - "inviteAlready": "看起來您已被邀請!", - "inviteAlreadyDescription": "要接受邀請,您必須登錄或創建一個帳戶。", - "signupQuestion": "已經有一個帳戶?", - "login": "登錄", - "resourceNotFound": "找不到資源", - "resourceNotFoundDescription": "您要訪問的資源不存在。", - "pincodeRequirementsLength": "PIN碼必須是 6 位數字", - "pincodeRequirementsChars": "PIN 必須只包含數字", - "passwordRequirementsLength": "密碼必須至少 1 個字元長", - "passwordRequirementsTitle": "密碼要求:", - "passwordRequirementLength": "至少 8 個字元長", - "passwordRequirementUppercase": "至少一個大寫字母", - "passwordRequirementLowercase": "至少一個小寫字母", - "passwordRequirementNumber": "至少一個數字", - "passwordRequirementSpecial": "至少一個特殊字元", - "passwordRequirementsMet": "✓ 密碼滿足所有要求", - "passwordStrength": "密碼強度", - "passwordStrengthWeak": "弱", - "passwordStrengthMedium": "中", - "passwordStrengthStrong": "強", - "passwordRequirements": "要求:", - "passwordRequirementLengthText": "8+ 個字元", - "passwordRequirementUppercaseText": "大寫字母 (A-Z)", - "passwordRequirementLowercaseText": "小寫字母 (a-z)", - "passwordRequirementNumberText": "數字 (0-9)", - "passwordRequirementSpecialText": "特殊字元 (!@#$%...)", - "passwordsDoNotMatch": "密碼不匹配", - "otpEmailRequirementsLength": "OTP 必須至少 1 個字元長", - "otpEmailSent": "OTP 已發送", - "otpEmailSentDescription": "OTP 已經發送到您的電子郵件", - "otpEmailErrorAuthenticate": "通過電子郵件身份驗證失敗", - "pincodeErrorAuthenticate": "Pincode 驗證失敗", - "passwordErrorAuthenticate": "密碼驗證失敗", - "poweredBy": "支持者:", - "authenticationRequired": "需要身份驗證", - "authenticationMethodChoose": "請選擇您偏好的方式來訪問 {name}", - "authenticationRequest": "您必須通過身份驗證才能訪問 {name}", - "user": "用戶", - "pincodeInput": "6 位數字 PIN 碼", - "pincodeSubmit": "使用 PIN 登錄", - "passwordSubmit": "使用密碼登錄", - "otpEmailDescription": "一次性代碼將發送到此電子郵件。", - "otpEmailSend": "發送一次性代碼", - "otpEmail": "一次性密碼 (OTP)", - "otpEmailSubmit": "提交 OTP", - "backToEmail": "回到電子郵件", - "noSupportKey": "伺服器當前未使用支持者金鑰,歡迎支持本項目!", - "accessDenied": "訪問被拒絕", - "accessDeniedDescription": "當前帳戶無權訪問此資源。如認為這是錯誤,請與管理員聯繫。", - "accessTokenError": "檢查訪問令牌時出錯", - "accessGranted": "已授予訪問", - "accessUrlInvalid": "訪問 URL 無效", - "accessGrantedDescription": "您已獲准訪問此資源,正在為您跳轉...", - "accessUrlInvalidDescription": "此共享訪問URL無效。請聯絡資源所有者獲取新URL。", - "tokenInvalid": "無效的令牌", - "pincodeInvalid": "無效的代碼", - "passwordErrorRequestReset": "請求重設失敗:", - "passwordErrorReset": "重設密碼失敗:", - "passwordResetSuccess": "密碼重設成功!返回登錄...", - "passwordReset": "重設密碼", - "passwordResetDescription": "按照步驟重設您的密碼", - "passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。", - "passwordResetCode": "驗證碼", - "passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。", - "generatePasswordResetCode": "產生密碼重設代碼", - "passwordResetCodeGenerated": "密碼重設代碼已產生", - "passwordResetCodeGeneratedDescription": "請將此代碼分享給使用者。他們可以用它來重設密碼。", - "passwordResetUrl": "重設網址", - "passwordNew": "新密碼", - "passwordNewConfirm": "確認新密碼", - "changePassword": "更改密碼", - "changePasswordDescription": "更新您的帳戶密碼", - "oldPassword": "當前密碼", - "newPassword": "新密碼", - "confirmNewPassword": "確認新密碼", - "changePasswordError": "更改密碼失敗", - "changePasswordErrorDescription": "更改您的密碼時出錯", - "changePasswordSuccess": "密碼修改成功", - "changePasswordSuccessDescription": "您的密碼已成功更新", - "passwordExpiryRequired": "需要密碼過期", - "passwordExpiryDescription": "該機構要求您每 {maxDays} 天更改一次密碼。", - "changePasswordNow": "現在更改密碼", - "pincodeAuth": "驗證器代碼", - "pincodeSubmit2": "提交代碼", - "passwordResetSubmit": "請求重設", - "passwordResetAlreadyHaveCode": "輸入代碼", - "passwordResetSmtpRequired": "請聯絡您的管理員", - "passwordResetSmtpRequiredDescription": "需要密碼重設代碼才能重設您的密碼。請聯絡您的管理員尋求協助。", - "passwordBack": "回到密碼", - "loginBack": "返回登錄", - "signup": "註冊", - "loginStart": "登錄以開始", - "idpOidcTokenValidating": "正在驗證 OIDC 令牌", - "idpOidcTokenResponse": "驗證 OIDC 令牌響應", - "idpErrorOidcTokenValidating": "驗證 OIDC 令牌出錯", - "idpConnectingTo": "連接到{name}", - "idpConnectingToDescription": "正在驗證您的身份", - "idpConnectingToProcess": "正在連接...", - "idpConnectingToFinished": "已連接", - "idpErrorConnectingTo": "無法連接到 {name},請聯絡管理員協助處理。", - "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} TB", - "gigabytes": "{count} GB", - "megabytes": "{count} MB", - "tagsEntered": "已輸入的標籤", - "tagsEnteredDescription": "這些是您輸入的標籤。", - "tagsWarnCannotBeLessThanZero": "最大標籤和最小標籤不能小於 0", - "tagsWarnNotAllowedAutocompleteOptions": "標記不允許為每個自動完成選項", - "tagsWarnInvalid": "無效的標籤,每個有效標籤", - "tagWarnTooShort": "標籤 {tagText} 太短", - "tagWarnTooLong": "標籤 {tagText} 太長", - "tagsWarnReachedMaxNumber": "已達到允許標籤的最大數量", - "tagWarnDuplicate": "未添加重複標籤 {tagText}", - "supportKeyInvalid": "無效金鑰", - "supportKeyInvalidDescription": "您的支持者金鑰無效。", - "supportKeyValid": "有效的金鑰", - "supportKeyValidDescription": "您的支持者金鑰已被驗證。感謝您的支持!", - "supportKeyErrorValidationDescription": "驗證支持者金鑰失敗。", - "supportKey": "支持開發和通過一個 Pangolin !", - "supportKeyDescription": "購買支持者鑰匙,幫助我們繼續為社區發展 Pangolin 。 您的貢獻使我們能夠投入更多的時間來維護和添加所有人的新功能。 我們永遠不會用這個來支付牆上的功能。這與任何商業版是分開的。", - "supportKeyPet": "您還可以領養並見到屬於自己的 Pangolin!", - "supportKeyPurchase": "付款通過 GitHub 進行處理,之後您可以在以下位置獲取您的金鑰:", - "supportKeyPurchaseLink": "我們的網站", - "supportKeyPurchase2": "並在這裡兌換。", - "supportKeyLearnMore": "了解更多。", - "supportKeyOptions": "請選擇最適合您的選項。", - "supportKetOptionFull": "完全支持者", - "forWholeServer": "適用於整個伺服器", - "lifetimePurchase": "終身購買", - "supporterStatus": "支持者狀態", - "buy": "購買", - "supportKeyOptionLimited": "有限支持者", - "forFiveUsers": "適用於 5 或更少用戶", - "supportKeyRedeem": "兌換支持者金鑰", - "supportKeyHideSevenDays": "隱藏 7 天", - "supportKeyEnter": "輸入支持者金鑰", - "supportKeyEnterDescription": "見到你自己的 Pangolin!", - "githubUsername": "GitHub 使用者名稱", - "supportKeyInput": "支持者金鑰", - "supportKeyBuy": "購買支持者金鑰", - "logoutError": "註銷錯誤", - "signingAs": "登錄為", - "serverAdmin": "伺服器管理員", - "managedSelfhosted": "託管自託管", - "otpEnable": "啟用雙因子認證", - "otpDisable": "禁用雙因子認證", - "logout": "登出", - "licenseTierProfessionalRequired": "需要專業版", - "licenseTierProfessionalRequiredDescription": "此功能僅在專業版可用。", - "actionGetOrg": "獲取組織", - "updateOrgUser": "更新組織用戶", - "createOrgUser": "創建組織用戶", - "actionUpdateOrg": "更新組織", - "actionRemoveInvitation": "移除邀請", - "actionUpdateUser": "更新用戶", - "actionGetUser": "獲取用戶", - "actionGetOrgUser": "獲取組織用戶", - "actionListOrgDomains": "列出組織域", - "actionCreateSite": "創建站點", - "actionDeleteSite": "刪除站點", - "actionGetSite": "獲取站點", - "actionListSites": "站點列表", - "actionApplyBlueprint": "應用藍圖", - "actionListBlueprints": "藍圖列表", - "actionGetBlueprint": "獲取藍圖", - "setupToken": "設置令牌", - "setupTokenDescription": "從伺服器控制台輸入設定令牌。", - "setupTokenRequired": "需要設置令牌", - "actionUpdateSite": "更新站點", - "actionListSiteRoles": "允許站點角色列表", - "actionCreateResource": "創建資源", - "actionDeleteResource": "刪除資源", - "actionGetResource": "獲取資源", - "actionListResource": "列出資源", - "actionUpdateResource": "更新資源", - "actionListResourceUsers": "列出資源用戶", - "actionSetResourceUsers": "設置資源用戶", - "actionSetAllowedResourceRoles": "設置允許的資源角色", - "actionListAllowedResourceRoles": "列出允許的資源角色", - "actionSetResourcePassword": "設置資源密碼", - "actionSetResourcePincode": "設置資源粉碼", - "actionSetResourceEmailWhitelist": "設置資源電子郵件白名單", - "actionGetResourceEmailWhitelist": "獲取資源電子郵件白名單", - "actionCreateTarget": "創建目標", - "actionDeleteTarget": "刪除目標", - "actionGetTarget": "獲取目標", - "actionListTargets": "列表目標", - "actionUpdateTarget": "更新目標", - "actionCreateRole": "創建角色", - "actionDeleteRole": "刪除角色", - "actionGetRole": "獲取角色", - "actionListRole": "角色列表", - "actionUpdateRole": "更新角色", - "actionListAllowedRoleResources": "列表允許的角色資源", - "actionInviteUser": "邀請用戶", - "actionRemoveUser": "刪除用戶", - "actionListUsers": "列出用戶", - "actionAddUserRole": "添加用戶角色", - "actionSetUserOrgRoles": "Set User Roles", - "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", - "actionGetIdp": "獲取 IDP", - "actionCreateIdpOrg": "創建 IDP 組織策略", - "actionDeleteIdpOrg": "刪除 IDP 組織策略", - "actionListIdpOrgs": "列出 IDP 組織", - "actionUpdateIdpOrg": "更新 IDP 組織", - "actionCreateClient": "創建用戶端", - "actionDeleteClient": "刪除用戶端", - "actionUpdateClient": "更新用戶端", - "actionListClients": "列出用戶端", - "actionGetClient": "獲取用戶端", - "actionCreateSiteResource": "創建站點資源", - "actionDeleteSiteResource": "刪除站點資源", - "actionGetSiteResource": "獲取站點資源", - "actionListSiteResources": "列出站點資源", - "actionUpdateSiteResource": "更新站點資源", - "actionListInvitations": "邀請列表", - "actionExportLogs": "匯出日誌", - "actionViewLogs": "查看日誌", - "noneSelected": "未選擇", - "orgNotFound2": "未找到組織。", - "searchProgress": "搜索中...", - "create": "創建", - "orgs": "組織", - "loginError": "登錄時出錯", - "loginRequiredForDevice": "需要登入以驗證您的裝置。", - "passwordForgot": "忘記密碼?", - "otpAuth": "兩步驗證", - "otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。", - "otpAuthSubmit": "提交代碼", - "idpContinue": "或者繼續", - "otpAuthBack": "返回登錄", - "navbar": "導航菜單", - "navbarDescription": "應用程式的主導航菜單", - "navbarDocsLink": "文件", - "otpErrorEnable": "無法啟用 2FA", - "otpErrorEnableDescription": "啟用 2FA 時出錯", - "otpSetupCheckCode": "請輸入您的 6 位數字代碼", - "otpSetupCheckCodeRetry": "無效的代碼。請重試。", - "otpSetup": "啟用兩步驗證", - "otpSetupDescription": "用額外的保護層來保護您的帳戶", - "otpSetupScanQr": "用您的身份驗證程序掃描此二維碼或手動輸入金鑰:", - "otpSetupSecretCode": "驗證器代碼", - "otpSetupSuccess": "啟用兩步驗證", - "otpSetupSuccessStoreBackupCodes": "您的帳戶現在更加安全。不要忘記保存您的備份代碼。", - "otpErrorDisable": "無法禁用 2FA", - "otpErrorDisableDescription": "禁用 2FA 時出錯", - "otpRemove": "禁用兩步驗證", - "otpRemoveDescription": "為您的帳戶禁用兩步驗證", - "otpRemoveSuccess": "雙重身份驗證已禁用", - "otpRemoveSuccessMessage": "您的帳戶已禁用雙重身份驗證。您可以隨時再次啟用它。", - "otpRemoveSubmit": "禁用兩步驗證", - "paginator": "第 {current} 頁,共 {last} 頁", - "paginatorToFirst": "轉到第一頁", - "paginatorToPrevious": "轉到上一頁", - "paginatorToNext": "轉到下一頁", - "paginatorToLast": "轉到最後一頁", - "copyText": "複製文本", - "copyTextFailed": "複製文本失敗: ", - "copyTextClipboard": "複製到剪貼簿", - "inviteErrorInvalidConfirmation": "無效確認", - "passwordRequired": "必須填寫密碼", - "allowAll": "允許所有", - "permissionsAllowAll": "允許所有權限", - "githubUsernameRequired": "必須填寫 GitHub 使用者名稱", - "supportKeyRequired": "必須填寫支持者金鑰", - "passwordRequirementsChars": "密碼至少需要 8 個字元", - "language": "語言", - "verificationCodeRequired": "必須輸入代碼", - "userErrorNoUpdate": "沒有要更新的用戶", - "siteErrorNoUpdate": "沒有要更新的站點", - "resourceErrorNoUpdate": "沒有可更新的資源", - "authErrorNoUpdate": "沒有要更新的身份驗證資訊", - "orgErrorNoUpdate": "沒有要更新的組織", - "orgErrorNoProvided": "未提供組織", - "apiKeysErrorNoUpdate": "沒有要更新的 API 金鑰", - "sidebarOverview": "概覽", - "sidebarHome": "首頁", - "sidebarSites": "站點", - "sidebarResources": "資源", - "sidebarProxyResources": "公開", - "sidebarClientResources": "私有", - "sidebarAccessControl": "訪問控制", - "sidebarLogsAndAnalytics": "日誌與分析", - "sidebarUsers": "用戶", - "sidebarAdmin": "管理員", - "sidebarInvitations": "邀請", - "sidebarRoles": "角色", - "sidebarShareableLinks": "分享連結", - "sidebarApiKeys": "API 金鑰", - "sidebarSettings": "設置", - "sidebarAllUsers": "所有用戶", - "sidebarIdentityProviders": "身份提供商", - "sidebarLicense": "證書", - "sidebarClients": "用戶端", - "sidebarUserDevices": "使用者", - "sidebarMachineClients": "機器", - "sidebarDomains": "域", - "sidebarGeneral": "管理", - "sidebarLogAndAnalytics": "日誌與分析", - "sidebarBluePrints": "藍圖", - "sidebarOrganization": "組織", - "sidebarLogsAnalytics": "分析", - "blueprints": "藍圖", - "blueprintsDescription": "應用聲明配置並查看先前運行的", - "blueprintAdd": "添加藍圖", - "blueprintGoBack": "查看所有藍圖", - "blueprintCreate": "創建藍圖", - "blueprintCreateDescription2": "按照下面的步驟創建和應用新的藍圖", - "blueprintDetails": "藍圖詳細資訊", - "blueprintDetailsDescription": "查看應用藍圖的結果和發生的任何錯誤", - "blueprintInfo": "藍圖資訊", - "message": "留言", - "blueprintContentsDescription": "定義描述您基礎設施的 YAML 內容", - "blueprintErrorCreateDescription": "應用藍圖時出錯", - "blueprintErrorCreate": "創建藍圖時出錯", - "searchBlueprintProgress": "搜索藍圖...", - "appliedAt": "應用於", - "source": "來源", - "contents": "目錄", - "parsedContents": "解析內容 (只讀)", - "enableDockerSocket": "啟用 Docker 藍圖", - "enableDockerSocketDescription": "啟用 Docker Socket 標籤擦除藍圖標籤。套接字路徑必須提供給新的。", - "enableDockerSocketLink": "了解更多", - "viewDockerContainers": "查看停靠容器", - "containersIn": "{siteName} 中的容器", - "selectContainerDescription": "選擇任何容器作為目標的主機名。點擊埠使用埠。", - "containerName": "名稱", - "containerImage": "圖片", - "containerState": "狀態", - "containerNetworks": "網路", - "containerHostnameIp": "主機名/IP", - "containerLabels": "標籤", - "containerLabelsCount": "{count, plural, other {# 標籤}}", - "containerLabelsTitle": "容器標籤", - "containerLabelEmpty": "<為空>", - "containerPorts": "埠", - "containerPortsMore": "+{count} 更多", - "containerActions": "行動", - "select": "選擇", - "noContainersMatchingFilters": "沒有找到匹配當前過濾器的容器。", - "showContainersWithoutPorts": "顯示沒有埠的容器", - "showStoppedContainers": "顯示已停止的容器", - "noContainersFound": "未找到容器。請確保 Docker 容器正在運行。", - "searchContainersPlaceholder": "在 {count} 個容器中搜索...", - "searchResultsCount": "{count, plural, other {# 個結果}}", - "filters": "篩選器", - "filterOptions": "過濾器選項", - "filterPorts": "埠", - "filterStopped": "已停止", - "clearAllFilters": "清除所有過濾器", - "columns": "列", - "toggleColumns": "切換列", - "refreshContainersList": "刷新容器列表", - "searching": "搜索中...", - "noContainersFoundMatching": "未找到與 \"{filter}\" 匹配的容器。", - "light": "淺色", - "dark": "深色", - "system": "系統", - "theme": "主題", - "subnetRequired": "子網是必填項", - "initialSetupTitle": "初始伺服器設置", - "initialSetupDescription": "創建初始伺服器管理員帳戶。 只能存在一個伺服器管理員。 您可以隨時更改這些憑據。", - "createAdminAccount": "創建管理員帳戶", - "setupErrorCreateAdmin": "創建伺服器管理員帳戶時發生錯誤。", - "certificateStatus": "證書狀態", - "loading": "載入中", - "restart": "重啟", - "domains": "域", - "domainsDescription": "管理您的組織域", - "domainsSearch": "搜索域...", - "domainAdd": "添加域", - "domainAddDescription": "在您的組織中註冊新域", - "domainCreate": "創建域", - "domainCreatedDescription": "域創建成功", - "domainDeletedDescription": "成功刪除域", - "domainQuestionRemove": "您確定要從您的帳戶中刪除域名嗎?", - "domainMessageRemove": "移除後,該域將不再與您的帳戶關聯。", - "domainConfirmDelete": "確認刪除域", - "domainDelete": "刪除域", - "domain": "域", - "selectDomainTypeNsName": "域委派(NS)", - "selectDomainTypeNsDescription": "此域及其所有子域。當您希望控制整個域區域時使用此選項。", - "selectDomainTypeCnameName": "單個域(CNAME)", - "selectDomainTypeCnameDescription": "僅此特定域。用於單個子域或特定域條目。", - "selectDomainTypeWildcardName": "通配符域", - "selectDomainTypeWildcardDescription": "此域名及其子域名。", - "domainDelegation": "單個域", - "selectType": "選擇一個類型", - "actions": "操作", - "refresh": "刷新", - "refreshError": "刷新數據失敗", - "verified": "已驗證", - "pending": "待定", - "sidebarBilling": "計費", - "billing": "計費", - "orgBillingDescription": "管理您的帳單資訊和訂閱", - "github": "GitHub", - "pangolinHosted": "Pangolin 託管", - "fossorial": "Fossorial", - "completeAccountSetup": "完成帳戶設定", - "completeAccountSetupDescription": "設置您的密碼以開始", - "accountSetupSent": "我們將發送帳號設定代碼到該電子郵件地址。", - "accountSetupCode": "設置代碼", - "accountSetupCodeDescription": "請檢查您的信箱以獲取設置代碼。", - "passwordCreate": "創建密碼", - "passwordCreateConfirm": "確認密碼", - "accountSetupSubmit": "發送設置代碼", - "completeSetup": "完成設置", - "accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!", - "documentation": "文件", - "saveAllSettings": "保存所有設置", - "saveResourceTargets": "儲存目標", - "saveResourceHttp": "儲存代理設定", - "saveProxyProtocol": "儲存代理協定設定", - "settingsUpdated": "設置已更新", - "settingsUpdatedDescription": "所有設置已成功更新", - "settingsErrorUpdate": "設置更新失敗", - "settingsErrorUpdateDescription": "更新設置時發生錯誤", - "sidebarCollapse": "摺疊", - "sidebarExpand": "展開", - "productUpdateMoreInfo": "還有 {noOfUpdates} 項更新", - "productUpdateInfo": "{noOfUpdates} 項更新", - "productUpdateWhatsNew": "新功能", - "productUpdateTitle": "產品更新", - "productUpdateEmpty": "沒有更新", - "dismissAll": "全部關閉", - "pangolinUpdateAvailable": "有可用更新", - "pangolinUpdateAvailableInfo": "版本 {version} 已準備好安裝", - "pangolinUpdateAvailableReleaseNotes": "查看發行說明", - "newtUpdateAvailable": "更新可用", - "newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。", - "domainPickerEnterDomain": "域名", - "domainPickerPlaceholder": "example.com", - "domainPickerDescription": "輸入資源的完整域名以查看可用選項。", - "domainPickerDescriptionSaas": "輸入完整域名、子域或名稱以查看可用選項。", - "domainPickerTabAll": "所有", - "domainPickerTabOrganization": "組織", - "domainPickerTabProvided": "提供的", - "domainPickerSortAsc": "A-Z", - "domainPickerSortDesc": "Z-A", - "domainPickerCheckingAvailability": "檢查可用性...", - "domainPickerNoMatchingDomains": "未找到匹配的域名。嘗試不同的域名或檢查您組織的域名設置。", - "domainPickerOrganizationDomains": "組織域", - "domainPickerProvidedDomains": "提供的域", - "domainPickerSubdomain": "子域:{subdomain}", - "domainPickerNamespace": "命名空間:{namespace}", - "domainPickerShowMore": "顯示更多", - "regionSelectorTitle": "選擇區域", - "regionSelectorInfo": "選擇區域以幫助提升您所在地的性能。您不必與伺服器在相同的區域。", - "regionSelectorPlaceholder": "選擇一個區域", - "regionSelectorComingSoon": "即將推出", - "billingLoadingSubscription": "正在載入訂閱...", - "billingFreeTier": "免費層", - "billingWarningOverLimit": "警告:您已超出一個或多個使用限制。在您修改訂閱或調整使用情況之前,您的站點將無法連接。", - "billingUsageLimitsOverview": "使用限制概覽", - "billingMonitorUsage": "監控您的使用情況以對比已配置的限制。如需提高限制請聯絡我們 support@pangolin.net。", - "billingDataUsage": "數據使用情況", - "billingOnlineTime": "站點在線時間", - "billingUsers": "活躍用戶", - "billingDomains": "活躍域", - "billingRemoteExitNodes": "活躍自託管節點", - "billingNoLimitConfigured": "未配置限制", - "billingEstimatedPeriod": "估計結算週期", - "billingIncludedUsage": "包含的使用量", - "billingIncludedUsageDescription": "您當前訂閱計劃中包含的使用量", - "billingFreeTierIncludedUsage": "免費層使用額度", - "billingIncluded": "包含", - "billingEstimatedTotal": "預計總額:", - "billingNotes": "備註", - "billingEstimateNote": "這是根據您當前使用情況的估算。", - "billingActualChargesMayVary": "實際費用可能會有變化。", - "billingBilledAtEnd": "您將在結算週期結束時被計費。", - "billingModifySubscription": "修改訂閱", - "billingStartSubscription": "開始訂閱", - "billingRecurringCharge": "週期性收費", - "billingManageSubscriptionSettings": "管理您的訂閱設置和偏好", - "billingNoActiveSubscription": "您沒有活躍的訂閱。開始訂閱以增加使用限制。", - "billingFailedToLoadSubscription": "無法載入訂閱", - "billingFailedToLoadUsage": "無法載入使用情況", - "billingFailedToGetCheckoutUrl": "無法獲取結帳網址", - "billingPleaseTryAgainLater": "請稍後再試。", - "billingCheckoutError": "結帳錯誤", - "billingFailedToGetPortalUrl": "無法獲取門戶網址", - "billingPortalError": "門戶錯誤", - "billingDataUsageInfo": "當連接到雲端時,您將為透過安全隧道傳輸的所有數據收取費用。 這包括您所有站點的進出流量。 當您達到上限時,您的站點將斷開連接,直到您升級計劃或減少使用。使用節點時不收取數據。", - "billingOnlineTimeInfo": "您要根據您的網站連接到雲端的時間長短收取費用。 例如,44,640 分鐘等於一個 24/7 全月運行的網站。 當您達到上限時,您的站點將斷開連接,直到您升級計劃或減少使用。使用節點時不收取費用。", - "billingUsersInfo": "根據您組織中的活躍用戶數量收費。按日計算帳單。", - "billingDomainInfo": "根據組織中活躍域的數量收費。按日計算帳單。", - "billingRemoteExitNodesInfo": "根據您組織中已管理節點的數量收費。按日計算帳單。", - "domainNotFound": "域未找到", - "domainNotFoundDescription": "此資源已禁用,因為該域不再在我們的系統中存在。請為此資源設置一個新域。", - "failed": "失敗", - "createNewOrgDescription": "創建一個新組織", - "organization": "組織", - "port": "埠", - "securityKeyManage": "管理安全金鑰", - "securityKeyDescription": "添加或刪除用於無密碼認證的安全金鑰", - "securityKeyRegister": "註冊新的安全金鑰", - "securityKeyList": "您的安全金鑰", - "securityKeyNone": "尚未註冊安全金鑰", - "securityKeyNameRequired": "名稱為必填項", - "securityKeyRemove": "刪除", - "securityKeyLastUsed": "上次使用:{date}", - "securityKeyNameLabel": "名稱", - "securityKeyRegisterSuccess": "安全金鑰註冊成功", - "securityKeyRegisterError": "註冊安全金鑰失敗", - "securityKeyRemoveSuccess": "安全金鑰刪除成功", - "securityKeyRemoveError": "刪除安全金鑰失敗", - "securityKeyLoadError": "載入安全金鑰失敗", - "securityKeyLogin": "使用安全金鑰繼續", - "securityKeyAuthError": "使用安全金鑰認證失敗", - "securityKeyRecommendation": "考慮在其他設備上註冊另一個安全金鑰,以確保不會被鎖定在您的帳戶之外。", - "registering": "註冊中...", - "securityKeyPrompt": "請使用您的安全金鑰驗證身份。確保您的安全金鑰已連接並準備好。", - "securityKeyBrowserNotSupported": "您的瀏覽器不支持安全金鑰。請使用像 Chrome、Firefox 或 Safari 這樣的現代瀏覽器。", - "securityKeyPermissionDenied": "請允許訪問您的安全金鑰以繼續登錄。", - "securityKeyRemovedTooQuickly": "請保持您的安全金鑰連接,直到登錄過程完成。", - "securityKeyNotSupported": "您的安全金鑰可能不相容。請嘗試不同的安全金鑰。", - "securityKeyUnknownError": "使用安全金鑰時出現問題。請再試一次。", - "twoFactorRequired": "註冊安全金鑰需要兩步驗證。", - "twoFactor": "兩步驗證", - "twoFactorAuthentication": "兩步驗證", - "twoFactorDescription": "這個組織需要雙重身份驗證。", - "enableTwoFactor": "啟用兩步驗證", - "organizationSecurityPolicy": "組織安全政策", - "organizationSecurityPolicyDescription": "此機構擁有安全要求,您必須先滿足才能訪問", - "securityRequirements": "安全要求", - "allRequirementsMet": "已滿足所有要求", - "completeRequirementsToContinue": "完成下面的要求以繼續訪問此組織", - "youCanNowAccessOrganization": "您現在可以訪問此組織", - "reauthenticationRequired": "會話長度", - "reauthenticationDescription": "該機構要求您每 {maxDays} 天登錄一次。", - "reauthenticationDescriptionHours": "該機構要求您每 {maxHours} 小時登錄一次。", - "reauthenticateNow": "再次登錄", - "adminEnabled2FaOnYourAccount": "管理員已為 {email} 啟用兩步驗證。請完成設置以繼續。", - "securityKeyAdd": "添加安全金鑰", - "securityKeyRegisterTitle": "註冊新安全金鑰", - "securityKeyRegisterDescription": "連接您的安全金鑰並輸入名稱以便識別", - "securityKeyTwoFactorRequired": "要求兩步驗證", - "securityKeyTwoFactorDescription": "請輸入你的兩步驗證代碼以註冊安全金鑰", - "securityKeyTwoFactorRemoveDescription": "請輸入你的兩步驗證代碼以移除安全金鑰", - "securityKeyTwoFactorCode": "雙因素代碼", - "securityKeyRemoveTitle": "移除安全金鑰", - "securityKeyRemoveDescription": "輸入您的密碼以移除安全金鑰 \"{name}\"", - "securityKeyNoKeysRegistered": "沒有註冊安全金鑰", - "securityKeyNoKeysDescription": "添加安全金鑰以加強您的帳戶安全", - "createDomainRequired": "必須輸入域", - "createDomainAddDnsRecords": "添加 DNS 記錄", - "createDomainAddDnsRecordsDescription": "將以下 DNS 記錄添加到您的域名提供商以完成設置。", - "createDomainNsRecords": "NS 記錄", - "createDomainRecord": "記錄", - "createDomainType": "類型:", - "createDomainName": "名稱:", - "createDomainValue": "值:", - "createDomainCnameRecords": "CNAME 記錄", - "createDomainARecords": "A記錄", - "createDomainRecordNumber": "記錄 {number}", - "createDomainTxtRecords": "TXT 記錄", - "createDomainSaveTheseRecords": "保存這些記錄", - "createDomainSaveTheseRecordsDescription": "務必保存這些 DNS 記錄,因為您將無法再次查看它們。", - "createDomainDnsPropagation": "DNS 傳播", - "createDomainDnsPropagationDescription": "DNS 更改可能需要一些時間才能在網路上傳播。這可能需要從幾分鐘到 48 小時,具體取決於您的 DNS 提供商和 TTL 設置。", - "resourcePortRequired": "非 HTTP 資源必須輸入埠號", - "resourcePortNotAllowed": "HTTP 資源不應設置埠號", - "billingPricingCalculatorLink": "價格計算機", - "signUpTerms": { - "IAgreeToThe": "我同意", - "termsOfService": "服務條款", - "and": "和", - "privacyPolicy": "隱私政策" - }, - "signUpMarketing": { - "keepMeInTheLoop": "透過電子郵件接收新聞、更新和新功能通知。" - }, - "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 私鑰", - "clientCredentialsSave": "保存您的憑據", - "clientCredentialsSaveDescription": "該資訊僅會顯示一次,請確保將其複製到安全位置。", - "generalSettingsDescription": "配置此用戶端的常規設置", - "clientUpdated": "用戶端已更新", - "clientUpdatedDescription": "用戶端已更新。", - "clientUpdateFailed": "更新用戶端失敗", - "clientUpdateError": "更新用戶端時出錯。", - "sitesFetchFailed": "獲取站點失敗", - "sitesFetchError": "獲取站點時出錯。", - "olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。", - "olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。", - "enterCidrRange": "輸入 CIDR 範圍", - "resourceEnableProxy": "啟用公共代理", - "resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。", - "externalProxyEnabled": "外部代理已啟用", - "addNewTarget": "添加新目標", - "targetsList": "目標列表", - "advancedMode": "高級模式", - "advancedSettings": "進階設定", - "targetErrorDuplicateTargetFound": "找到重複的目標", - "healthCheckHealthy": "正常", - "healthCheckUnhealthy": "不正常", - "healthCheckUnknown": "未知", - "healthCheck": "健康檢查", - "configureHealthCheck": "配置健康檢查", - "configureHealthCheckDescription": "為 {target} 設置健康監控", - "enableHealthChecks": "啟用健康檢查", - "enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。", - "healthScheme": "方法", - "healthSelectScheme": "選擇方法", - "healthCheckPortInvalid": "健康檢查連接埠必須介於 1 到 65535 之間", - "healthCheckPath": "路徑", - "healthHostname": "IP / 主機", - "healthPort": "埠", - "healthCheckPathDescription": "用於檢查健康狀態的路徑。", - "healthyIntervalSeconds": "正常間隔", - "unhealthyIntervalSeconds": "不正常間隔", - "IntervalSeconds": "正常間隔", - "timeoutSeconds": "超時", - "timeIsInSeconds": "時間以秒為單位", - "retryAttempts": "重試次數", - "expectedResponseCodes": "期望響應代碼", - "expectedResponseCodesDescription": "HTTP 狀態碼表示健康狀態。如留空,200-300 被視為健康。", - "customHeaders": "自訂 Headers", - "customHeadersDescription": "Header 斷行分隔:Header 名稱:值", - "headersValidationError": "Header 必須是格式:Header 名稱:值。", - "saveHealthCheck": "保存健康檢查", - "healthCheckSaved": "健康檢查已保存", - "healthCheckSavedDescription": "健康檢查配置已成功保存。", - "healthCheckError": "健康檢查錯誤", - "healthCheckErrorDescription": "保存健康檢查配置時出錯", - "healthCheckPathRequired": "健康檢查路徑為必填項", - "healthCheckMethodRequired": "HTTP 方法為必填項", - "healthCheckIntervalMin": "檢查間隔必須至少為 5 秒", - "healthCheckTimeoutMin": "超時必須至少為 1 秒", - "healthCheckRetryMin": "重試次數必須至少為 1 次", - "httpMethod": "HTTP 方法", - "selectHttpMethod": "選擇 HTTP 方法", - "domainPickerSubdomainLabel": "子域名", - "domainPickerBaseDomainLabel": "根域名", - "domainPickerSearchDomains": "搜索域名...", - "domainPickerNoDomainsFound": "未找到域名", - "domainPickerLoadingDomains": "載入域名...", - "domainPickerSelectBaseDomain": "選擇根域名...", - "domainPickerNotAvailableForCname": "不適用於 CNAME 域", - "domainPickerEnterSubdomainOrLeaveBlank": "輸入子域名或留空以使用根域名。", - "domainPickerEnterSubdomainToSearch": "輸入一個子域名以搜索並從可用免費域名中選擇。", - "domainPickerFreeDomains": "免費域名", - "domainPickerSearchForAvailableDomains": "搜索可用域名", - "domainPickerNotWorkSelfHosted": "注意:自託管實例當前不提供免費的域名。", - "resourceDomain": "域名", - "resourceEditDomain": "編輯域名", - "siteName": "站點名稱", - "proxyPort": "埠", - "resourcesTableProxyResources": "代理資源", - "resourcesTableClientResources": "用戶端資源", - "resourcesTableNoProxyResourcesFound": "未找到代理資源。", - "resourcesTableNoInternalResourcesFound": "未找到內部資源。", - "resourcesTableDestination": "目標", - "resourcesTableAlias": "別名", - "resourcesTableClients": "用戶端", - "resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。", - "resourcesTableNoTargets": "無目標", - "resourcesTableHealthy": "健康", - "resourcesTableDegraded": "降級", - "resourcesTableOffline": "離線", - "resourcesTableUnknown": "未知", - "resourcesTableNotMonitored": "未監控", - "editInternalResourceDialogEditClientResource": "編輯用戶端資源", - "editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。", - "editInternalResourceDialogResourceProperties": "資源屬性", - "editInternalResourceDialogName": "名稱", - "editInternalResourceDialogProtocol": "協議", - "editInternalResourceDialogSitePort": "站點埠", - "editInternalResourceDialogTargetConfiguration": "目標配置", - "editInternalResourceDialogCancel": "取消", - "editInternalResourceDialogSaveResource": "保存資源", - "editInternalResourceDialogSuccess": "成功", - "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "內部資源更新成功", - "editInternalResourceDialogError": "錯誤", - "editInternalResourceDialogFailedToUpdateInternalResource": "更新內部資源失敗", - "editInternalResourceDialogNameRequired": "名稱為必填項", - "editInternalResourceDialogNameMaxLength": "名稱長度必須小於 255 個字元", - "editInternalResourceDialogProxyPortMin": "代理埠必須至少為 1", - "editInternalResourceDialogProxyPortMax": "代理埠必須小於 65536", - "editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", - "editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", - "editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", - "editInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠", - "editInternalResourceDialogMode": "模式", - "editInternalResourceDialogModePort": "連接埠", - "editInternalResourceDialogModeHost": "主機", - "editInternalResourceDialogModeCidr": "CIDR", - "editInternalResourceDialogDestination": "目的地", - "editInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。", - "editInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名稱位址。", - "editInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。", - "editInternalResourceDialogAlias": "別名", - "editInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。", - "createInternalResourceDialogNoSitesAvailable": "暫無可用站點", - "createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。", - "createInternalResourceDialogClose": "關閉", - "createInternalResourceDialogCreateClientResource": "創建用戶端資源", - "createInternalResourceDialogCreateClientResourceDescription": "創建一個新資源,該資源將可供連接到所選站點的用戶端訪問。", - "createInternalResourceDialogResourceProperties": "資源屬性", - "createInternalResourceDialogName": "名稱", - "createInternalResourceDialogSite": "站點", - "selectSite": "選擇站點...", - "noSitesFound": "找不到站點。", - "createInternalResourceDialogProtocol": "協議", - "createInternalResourceDialogTcp": "TCP", - "createInternalResourceDialogUdp": "UDP", - "createInternalResourceDialogSitePort": "站點埠", - "createInternalResourceDialogSitePortDescription": "使用此埠在連接到用戶端時訪問站點上的資源。", - "createInternalResourceDialogTargetConfiguration": "目標配置", - "createInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名地址。", - "createInternalResourceDialogDestinationPortDescription": "資源在目標 IP 上可訪問的埠。", - "createInternalResourceDialogCancel": "取消", - "createInternalResourceDialogCreateResource": "創建資源", - "createInternalResourceDialogSuccess": "成功", - "createInternalResourceDialogInternalResourceCreatedSuccessfully": "內部資源創建成功", - "createInternalResourceDialogError": "錯誤", - "createInternalResourceDialogFailedToCreateInternalResource": "創建內部資源失敗", - "createInternalResourceDialogNameRequired": "名稱為必填項", - "createInternalResourceDialogNameMaxLength": "名稱長度必須小於 255 個字元", - "createInternalResourceDialogPleaseSelectSite": "請選擇一個站點", - "createInternalResourceDialogProxyPortMin": "代理埠必須至少為 1", - "createInternalResourceDialogProxyPortMax": "代理埠必須小於 65536", - "createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", - "createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", - "createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", - "createInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠", - "createInternalResourceDialogMode": "模式", - "createInternalResourceDialogModePort": "連接埠", - "createInternalResourceDialogModeHost": "主機", - "createInternalResourceDialogModeCidr": "CIDR", - "createInternalResourceDialogDestination": "目的地", - "createInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。", - "createInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。", - "createInternalResourceDialogAlias": "別名", - "createInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。", - "siteConfiguration": "配置", - "siteAcceptClientConnections": "接受用戶端連接", - "siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。", - "siteAddress": "站點地址", - "siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。", - "siteNameDescription": "站點的顯示名稱,可以稍後更改。", - "autoLoginExternalIdp": "自動使用外部 IDP 登錄", - "autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。", - "selectIdp": "選擇 IDP", - "selectIdpPlaceholder": "選擇一個 IDP...", - "selectIdpRequired": "在啟用自動登錄時,請選擇一個 IDP。", - "autoLoginTitle": "重定向中", - "autoLoginDescription": "正在將您重定向到外部身份提供商進行身份驗證。", - "autoLoginProcessing": "準備身份驗證...", - "autoLoginRedirecting": "重定向到登錄...", - "autoLoginError": "自動登錄錯誤", - "autoLoginErrorNoRedirectUrl": "未從身份提供商收到重定向 URL。", - "autoLoginErrorGeneratingUrl": "生成身份驗證 URL 失敗。", - "remoteExitNodeManageRemoteExitNodes": "遠程節點", - "remoteExitNodeDescription": "自我主機一個或多個遠程節點來擴展您的網路連接並減少對雲的依賴性", - "remoteExitNodes": "節點", - "searchRemoteExitNodes": "搜索節點...", - "remoteExitNodeAdd": "添加節點", - "remoteExitNodeErrorDelete": "刪除節點時出錯", - "remoteExitNodeQuestionRemove": "您確定要從組織中刪除該節點嗎?", - "remoteExitNodeMessageRemove": "一旦刪除,該節點將不再能夠訪問。", - "remoteExitNodeConfirmDelete": "確認刪除節點", - "remoteExitNodeDelete": "刪除節點", - "sidebarRemoteExitNodes": "遠程節點", - "remoteExitNodeId": "ID", - "remoteExitNodeSecretKey": "密鑰", - "remoteExitNodeCreate": { - "title": "創建節點", - "description": "創建一個新節點來擴展您的網路連接", - "viewAllButton": "查看所有節點", - "strategy": { - "title": "創建策略", - "description": "選擇此選項以手動配置您的節點或生成新憑據。", - "adopt": { - "title": "採納節點", - "description": "如果您已經擁有該節點的憑據,請選擇此項。" - }, - "generate": { - "title": "生成金鑰", - "description": "如果您想為節點生成新金鑰,請選擇此選項" - } + "setupCreate": "創建您的第一個組織、網站和資源", + "headerAuthCompatibilityInfo": "啟用此選項以在缺少驗證令牌時強制回傳 401 未授權回應。這對於不會在沒有伺服器挑戰的情況下發送憑證的瀏覽器或特定 HTTP 函式庫是必需的。", + "headerAuthCompatibility": "擴展相容性", + "setupNewOrg": "新建組織", + "setupCreateOrg": "創建組織", + "setupCreateResources": "創建資源", + "setupOrgName": "組織名稱", + "orgDisplayName": "這是您組織的顯示名稱。", + "orgId": "組織ID", + "setupIdentifierMessage": "這是您組織的唯一標識符。這是與顯示名稱分開的。", + "setupErrorIdentifier": "組織ID 已被使用。請另選一個。", + "componentsErrorNoMemberCreate": "您目前不是任何組織的成員。創建組織以開始操作。", + "componentsErrorNoMember": "您目前不是任何組織的成員。", + "welcome": "歡迎使用 Pangolin", + "welcomeTo": "歡迎來到", + "componentsCreateOrg": "創建組織", + "componentsMember": "您屬於 {count, plural, =0 {沒有組織} one {一個組織} other {# 個組織}}。", + "componentsInvalidKey": "檢測到無效或過期的許可證金鑰。按照許可證條款操作以繼續使用所有功能。", + "dismiss": "忽略", + "componentsLicenseViolation": "許可證超限:該伺服器使用了 {usedSites} 個站點,已超過授權的 {maxSites} 個。請遵守許可證條款以繼續使用全部功能。", + "componentsSupporterMessage": "感謝您的支持!您現在是 Pangolin 的 {tier} 用戶。", + "inviteErrorNotValid": "很抱歉,但看起來你試圖訪問的邀請尚未被接受或不再有效。", + "inviteErrorUser": "很抱歉,但看起來你想要訪問的邀請不是這個用戶。", + "inviteLoginUser": "請確保您以正確的用戶登錄。", + "inviteErrorNoUser": "很抱歉,但看起來你想訪問的邀請不是一個存在的用戶。", + "inviteCreateUser": "請先創建一個帳戶。", + "goHome": "返回首頁", + "inviteLogInOtherUser": "以不同的用戶登錄", + "createAnAccount": "創建帳戶", + "inviteNotAccepted": "邀請未接受", + "authCreateAccount": "創建一個帳戶以開始", + "authNoAccount": "沒有帳戶?", + "email": "電子郵件地址", + "password": "密碼", + "confirmPassword": "確認密碼", + "createAccount": "創建帳戶", + "viewSettings": "查看設置", + "delete": "刪除", + "name": "名稱", + "online": "在線", + "offline": "離線的", + "site": "站點", + "dataIn": "數據輸入", + "dataOut": "數據輸出", + "connectionType": "連接類型", + "tunnelType": "隧道類型", + "local": "本地的", + "edit": "編輯", + "siteConfirmDelete": "確認刪除站點", + "siteDelete": "刪除站點", + "siteMessageRemove": "一旦移除,站點將無法訪問。與站點相關的所有目標也將被移除。", + "siteQuestionRemove": "您確定要從組織中刪除該站點嗎?", + "siteManageSites": "管理站點", + "siteDescription": "允許通過安全隧道連接到您的網路", + "sitesBannerTitle": "連接任何網路", + "sitesBannerDescription": "站點是與遠端網路的連接,使 Pangolin 能夠為任何地方的使用者提供對公共或私有資源的存取。在任何可以執行二進位檔案或容器的地方安裝站點網路連接器 (Newt) 以建立連接。", + "sitesBannerButtonText": "安裝站點", + "siteCreate": "創建站點", + "siteCreateDescription2": "按照下面的步驟創建和連接一個新站點", + "siteCreateDescription": "創建一個新站點開始連接您的資源", + "close": "關閉", + "siteErrorCreate": "創建站點出錯", + "siteErrorCreateKeyPair": "找不到金鑰對或站點預設值", + "siteErrorCreateDefaults": "未找到站點預設值", + "method": "方法", + "siteMethodDescription": "這是您將如何顯示連接。", + "siteLearnNewt": "學習如何在您的系統上安裝 Newt", + "siteSeeConfigOnce": "您只能看到一次配置。", + "siteLoadWGConfig": "正在載入 WireGuard 配置...", + "siteDocker": "擴展 Docker 部署詳細資訊", + "toggle": "切換", + "dockerCompose": "Docker Compose", + "dockerRun": "Docker Run", + "siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多", + "siteConfirmCopy": "我已經複製了配置資訊", + "searchSitesProgress": "搜索站點...", + "siteAdd": "添加站點", + "siteInstallNewt": "安裝 Newt", + "siteInstallNewtDescription": "在您的系統中運行 Newt", + "WgConfiguration": "WireGuard 配置", + "WgConfigurationDescription": "使用以下配置連接到您的網路", + "operatingSystem": "操作系統", + "commands": "命令", + "recommended": "推薦", + "siteNewtDescription": "為獲得最佳用戶體驗,請使用 Newt。其底層採用 WireGuard 技術,可直接通過 Pangolin 控制台,使用區域網路地址訪問您私有網路中的資源。", + "siteRunsInDocker": "在 Docker 中運行", + "siteRunsInShell": "在 macOS 、 Linux 和 Windows 的 Shell 中運行", + "siteErrorDelete": "刪除站點出錯", + "siteErrorUpdate": "更新站點失敗", + "siteErrorUpdateDescription": "更新站點時出錯。", + "siteUpdated": "站點已更新", + "siteUpdatedDescription": "網站已更新。", + "siteGeneralDescription": "配置此站點的常規設置", + "siteSettingDescription": "配置您網站上的設置", + "siteSetting": "{siteName} 設置", + "siteNewtTunnel": "Newt 隧道 (推薦)", + "siteNewtTunnelDescription": "最簡單的方式來連接到您的網路。不需要任何額外設置。", + "siteWg": "基本 WireGuard", + "siteWgDescription": "使用任何 WireGuard 用戶端來建立隧道。需要手動配置 NAT。", + "siteWgDescriptionSaas": "使用任何 WireGuard 用戶端建立隧道。需要手動配置 NAT。僅適用於自託管節點。", + "siteLocalDescription": "僅限本地資源。不需要隧道。", + "siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。", + "siteSeeAll": "查看所有站點", + "siteTunnelDescription": "確定如何連接到您的網站", + "siteNewtCredentials": "Newt 憑證", + "siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑證", + "remoteNodeCredentialsDescription": "這是遠端節點與伺服器進行驗證的方式", + "siteCredentialsSave": "保存您的憑證", + "siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。", + "siteInfo": "站點資訊", + "status": "狀態", + "shareTitle": "管理共享連結", + "shareDescription": "創建可共享的連結,允許暫時或永久訪問您的資源", + "shareSearch": "搜索共享連結...", + "shareCreate": "創建共享連結", + "shareErrorDelete": "刪除連結失敗", + "shareErrorDeleteMessage": "刪除連結時出錯", + "shareDeleted": "連結已刪除", + "shareDeletedDescription": "連結已刪除", + "shareTokenDescription": "您的訪問令牌可以透過兩種方式傳遞:作為查詢參數或請求頭。 每次驗證訪問請求都必須從用戶端傳遞。", + "accessToken": "訪問令牌", + "usageExamples": "用法範例", + "tokenId": "令牌 ID", + "requestHeades": "請求頭", + "queryParameter": "查詢參數", + "importantNote": "重要提示", + "shareImportantDescription": "出於安全考慮,建議盡可能在使用請求頭傳遞參數,因為查詢參數可能會被瀏覽器歷史記錄或伺服器日誌記錄。", + "token": "令牌", + "shareTokenSecurety": "請妥善保管您的訪問令牌,不要將其暴露在公開訪問的區域或用戶端代碼中。", + "shareErrorFetchResource": "獲取資源失敗", + "shareErrorFetchResourceDescription": "獲取資源時出錯", + "shareErrorCreate": "無法創建共享連結", + "shareErrorCreateDescription": "創建共享連結時出錯", + "shareCreateDescription": "任何具有此連結的人都可以訪問資源", + "shareTitleOptional": "標題 (可選)", + "expireIn": "過期時間", + "neverExpire": "永不過期", + "shareExpireDescription": "過期時間是連結可以使用並提供對資源的訪問時間。 此時間後,連結將不再工作,使用此連結的用戶將失去對資源的訪問。", + "shareSeeOnce": "您只能看到一次此連結。請確保複製它。", + "shareAccessHint": "任何具有此連結的人都可以訪問該資源。小心地分享它。", + "shareTokenUsage": "查看訪問令牌使用情況", + "createLink": "創建連結", + "resourcesNotFound": "找不到資源", + "resourceSearch": "搜索資源", + "openMenu": "打開菜單", + "resource": "資源", + "title": "標題", + "created": "已創建", + "expires": "過期時間", + "never": "永不過期", + "shareErrorSelectResource": "請選擇一個資源", + "proxyResourceTitle": "管理公開資源", + "proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源", + "proxyResourcesBannerTitle": "基於網頁的公開存取", + "proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。", + "clientResourceTitle": "管理私有資源", + "clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源", + "privateResourcesBannerTitle": "零信任私有存取", + "privateResourcesBannerDescription": "私有資源使用零信任安全性,確保使用者和機器只能存取您明確授權的資源。連接使用者裝置或機器客戶端以透過安全的虛擬私人網路存取這些資源。", + "resourcesSearch": "搜索資源...", + "resourceAdd": "添加資源", + "resourceErrorDelte": "刪除資源時出錯", + "authentication": "認證", + "protected": "受到保護", + "notProtected": "未受到保護", + "resourceMessageRemove": "一旦刪除,資源將不再可訪問。與該資源相關的所有目標也將被刪除。", + "resourceQuestionRemove": "您確定要從組織中刪除資源嗎?", + "resourceHTTP": "HTTPS 資源", + "resourceHTTPDescription": "使用子域或根域名通過 HTTPS 向您的應用程式提出代理請求。", + "resourceRaw": "TCP/UDP 資源", + "resourceRawDescription": "使用 TCP/UDP 使用埠號向您的應用提出代理請求。", + "resourceCreate": "創建資源", + "resourceCreateDescription": "按照下面的步驟創建新資源", + "resourceSeeAll": "查看所有資源", + "resourceInfo": "資源資訊", + "resourceNameDescription": "這是資源的顯示名稱。", + "siteSelect": "選擇站點", + "siteSearch": "搜索站點", + "siteNotFound": "未找到站點。", + "selectCountry": "選擇國家", + "searchCountries": "搜索國家...", + "noCountryFound": "找不到國家。", + "siteSelectionDescription": "此站點將為目標提供連接。", + "resourceType": "資源類型", + "resourceTypeDescription": "確定如何訪問您的資源", + "resourceHTTPSSettings": "HTTPS 設置", + "resourceHTTPSSettingsDescription": "配置如何通過 HTTPS 訪問您的資源", + "domainType": "域類型", + "subdomain": "子域名", + "baseDomain": "根域名", + "subdomnainDescription": "您的資源可以訪問的子域名。", + "resourceRawSettings": "TCP/UDP 設置", + "resourceRawSettingsDescription": "設定如何透過 TCP/UDP 存取資源", + "protocol": "協議", + "protocolSelect": "選擇協議", + "resourcePortNumber": "埠號", + "resourcePortNumberDescription": "代理請求的外部埠號。", + "cancel": "取消", + "resourceConfig": "配置片段", + "resourceConfigDescription": "複製並黏貼這些配置片段以設置您的 TCP/UDP 資源", + "resourceAddEntrypoints": "Traefik: 添加入口點", + "resourceExposePorts": "Gerbil:在 Docker Compose 中顯示埠", + "resourceLearnRaw": "學習如何配置 TCP/UDP 資源", + "resourceBack": "返回資源", + "resourceGoTo": "轉到資源", + "resourceDelete": "刪除資源", + "resourceDeleteConfirm": "確認刪除資源", + "visibility": "可見性", + "enabled": "已啟用", + "disabled": "已禁用", + "general": "概覽", + "generalSettings": "常規設置", + "proxy": "代理伺服器", + "internal": "內部設置", + "rules": "規則", + "resourceSettingDescription": "配置您資源上的設置", + "resourceSetting": "{resourceName} 設置", + "alwaysAllow": "一律允許", + "alwaysDeny": "一律拒絕", + "passToAuth": "傳遞至認證", + "orgSettingsDescription": "配置您組織的一般設定", + "orgGeneralSettings": "組織設置", + "orgGeneralSettingsDescription": "管理您的機構詳細資訊和配置", + "saveGeneralSettings": "保存常規設置", + "saveSettings": "保存設置", + "orgDangerZone": "危險區域", + "orgDangerZoneDescription": "一旦刪除該組織,將無法恢復,請務必確認。", + "orgDelete": "刪除組織", + "orgDeleteConfirm": "確認刪除組織", + "orgMessageRemove": "此操作不可逆,這將刪除所有相關數據。", + "orgMessageConfirm": "要確認,請在下面輸入組織名稱。", + "orgQuestionRemove": "您確定要刪除組織嗎?", + "orgUpdated": "組織已更新", + "orgUpdatedDescription": "組織已更新。", + "orgErrorUpdate": "更新組織失敗", + "orgErrorUpdateMessage": "更新組織時出錯。", + "orgErrorFetch": "獲取組織失敗", + "orgErrorFetchMessage": "列出您的組織時出錯", + "orgErrorDelete": "刪除組織失敗", + "orgErrorDeleteMessage": "刪除組織時出錯。", + "orgDeleted": "組織已刪除", + "orgDeletedMessage": "組織及其數據已被刪除。", + "orgMissing": "缺少組織 ID", + "orgMissingMessage": "沒有組織ID,無法重新生成邀請。", + "accessUsersManage": "管理用戶", + "accessUsersDescription": "邀請用戶並位他們添加角色以管理訪問您的組織", + "accessUsersSearch": "搜索用戶...", + "accessUserCreate": "創建用戶", + "accessUserRemove": "刪除用戶", + "username": "使用者名稱", + "identityProvider": "身份提供商", + "role": "角色", + "nameRequired": "名稱是必填項", + "accessRolesManage": "管理角色", + "accessRolesDescription": "配置角色來管理訪問您的組織", + "accessRolesSearch": "搜索角色...", + "accessRolesAdd": "添加角色", + "accessRoleDelete": "刪除角色", + "description": "描述", + "inviteTitle": "打開邀請", + "inviteDescription": "管理您給其他用戶的邀請", + "inviteSearch": "搜索邀請...", + "minutes": "分鐘", + "hours": "小時", + "days": "天", + "weeks": "周", + "months": "月", + "years": "年", + "day": "{count, plural, other {# 天}}", + "apiKeysTitle": "API 金鑰", + "apiKeysConfirmCopy2": "您必須確認您已複製 API 金鑰。", + "apiKeysErrorCreate": "創建 API 金鑰出錯", + "apiKeysErrorSetPermission": "設置權限出錯", + "apiKeysCreate": "生成 API 金鑰", + "apiKeysCreateDescription": "為您的組織生成一個新的 API 金鑰", + "apiKeysGeneralSettings": "權限", + "apiKeysGeneralSettingsDescription": "確定此 API 金鑰可以做什麼", + "apiKeysList": "您的 API 金鑰", + "apiKeysSave": "保存您的 API 金鑰", + "apiKeysSaveDescription": "該資訊僅會顯示一次,請確保將其複製到安全的位置。", + "apiKeysInfo": "您的 API 金鑰是:", + "apiKeysConfirmCopy": "我已複製 API 金鑰", + "generate": "生成", + "done": "完成", + "apiKeysSeeAll": "查看所有 API 金鑰", + "apiKeysPermissionsErrorLoadingActions": "載入 API 金鑰操作時出錯", + "apiKeysPermissionsErrorUpdate": "設置權限出錯", + "apiKeysPermissionsUpdated": "權限已更新", + "apiKeysPermissionsUpdatedDescription": "權限已更新。", + "apiKeysPermissionsGeneralSettings": "權限", + "apiKeysPermissionsGeneralSettingsDescription": "確定此 API 金鑰可以做什麼", + "apiKeysPermissionsSave": "保存權限", + "apiKeysPermissionsTitle": "權限", + "apiKeys": "API 金鑰", + "searchApiKeys": "搜索 API 金鑰...", + "apiKeysAdd": "生成 API 金鑰", + "apiKeysErrorDelete": "刪除 API 金鑰出錯", + "apiKeysErrorDeleteMessage": "刪除 API 金鑰出錯", + "apiKeysQuestionRemove": "您確定要從組織中刪除 API 金鑰嗎?", + "apiKeysMessageRemove": "一旦刪除,此API金鑰將無法被使用。", + "apiKeysDeleteConfirm": "確認刪除 API 金鑰", + "apiKeysDelete": "刪除 API 金鑰", + "apiKeysManage": "管理 API 金鑰", + "apiKeysDescription": "API 金鑰用於認證集成 API", + "apiKeysSettings": "{apiKeyName} 設置", + "userTitle": "管理所有用戶", + "userDescription": "查看和管理系統中的所有用戶", + "userAbount": "關於用戶管理", + "userAbountDescription": "此表格顯示系統中所有根用戶對象。每個用戶可能屬於多個組織。 從組織中刪除用戶不會刪除其根用戶對象 - 他們將保留在系統中。 要從系統中完全刪除用戶,您必須使用此表格中的刪除操作刪除其根用戶對象。", + "userServer": "伺服器用戶", + "userSearch": "搜索伺服器用戶...", + "userErrorDelete": "刪除用戶時出錯", + "userDeleteConfirm": "確認刪除用戶", + "userDeleteServer": "從伺服器刪除用戶", + "userMessageRemove": "該用戶將被從所有組織中刪除並完全從伺服器中刪除。", + "userQuestionRemove": "您確定要從伺服器永久刪除用戶嗎?", + "licenseKey": "許可證金鑰", + "valid": "有效", + "numberOfSites": "站點數量", + "licenseKeySearch": "搜索許可證金鑰...", + "licenseKeyAdd": "添加許可證金鑰", + "type": "類型", + "licenseKeyRequired": "需要許可證金鑰", + "licenseTermsAgree": "您必須同意許可條款", + "licenseErrorKeyLoad": "載入許可證金鑰失敗", + "licenseErrorKeyLoadDescription": "載入許可證金鑰時出錯。", + "licenseErrorKeyDelete": "刪除許可證金鑰失敗", + "licenseErrorKeyDeleteDescription": "刪除許可證金鑰時出錯。", + "licenseKeyDeleted": "許可證金鑰已刪除", + "licenseKeyDeletedDescription": "許可證金鑰已被刪除。", + "licenseErrorKeyActivate": "啟用許可證金鑰失敗", + "licenseErrorKeyActivateDescription": "啟用許可證金鑰時出錯。", + "licenseAbout": "關於許可協議", + "licenseBannerTitle": "Enable Your Enterprise License", + "licenseBannerDescription": "Unlock enterprise features for your self-hosted Pangolin instance. Purchase a license key to activate premium capabilities, then add it below.", + "licenseBannerGetLicense": "Get a License", + "licenseBannerViewDocs": "View Documentation", + "communityEdition": "社區版", + "licenseAboutDescription": "這是針對商業環境中使用Pangolin的商業和企業用戶。 如果您正在使用 Pangolin 供個人使用,您可以忽略此部分。", + "licenseKeyActivated": "授權金鑰已啟用", + "licenseKeyActivatedDescription": "已成功啟用許可證金鑰。", + "licenseErrorKeyRecheck": "重新檢查許可證金鑰失敗", + "licenseErrorKeyRecheckDescription": "重新檢查許可證金鑰時出錯。", + "licenseErrorKeyRechecked": "重新檢查許可證金鑰", + "licenseErrorKeyRecheckedDescription": "已重新檢查所有許可證金鑰", + "licenseActivateKey": "啟用許可證金鑰", + "licenseActivateKeyDescription": "輸入一個許可金鑰來啟用它。", + "licenseActivate": "啟用許可證", + "licenseAgreement": "通過檢查此框,您確認您已經閱讀並同意與您的許可證金鑰相關的許可條款。", + "fossorialLicense": "查看Fossorial Commercial License和訂閱條款", + "licenseMessageRemove": "這將刪除許可證金鑰和它授予的所有相關權限。", + "licenseMessageConfirm": "要確認,請在下面輸入許可證金鑰。", + "licenseQuestionRemove": "您確定要刪除許可證金鑰?", + "licenseKeyDelete": "刪除許可證金鑰", + "licenseKeyDeleteConfirm": "確認刪除許可證金鑰", + "licenseTitle": "管理許可證狀態", + "licenseTitleDescription": "查看和管理系統中的許可證金鑰", + "licenseHost": "主機許可證", + "licenseHostDescription": "管理主機的主許可證金鑰。", + "licensedNot": "未授權", + "hostId": "主機 ID", + "licenseReckeckAll": "重新檢查所有金鑰", + "licenseSiteUsage": "站點使用情況", + "licenseSiteUsageDecsription": "查看使用此許可的站點數量。", + "licenseNoSiteLimit": "使用未經許可主機的站點數量沒有限制。", + "licensePurchase": "購買許可證", + "licensePurchaseSites": "購買更多站點", + "licenseSitesUsedMax": "使用了 {usedSites}/{maxSites} 個站點", + "licenseSitesUsed": "{count, plural, =0 {# 站點} one {# 站點} other {# 站點}}", + "licensePurchaseDescription": "請選擇您希望 {selectedMode, select, license {直接購買許可證,您可以隨時增加更多站點。} other {為現有許可證購買更多站點}}", + "licenseFee": "許可證費用", + "licensePriceSite": "每個站點的價格", + "total": "總計", + "licenseContinuePayment": "繼續付款", + "pricingPage": "定價頁面", + "pricingPortal": "前往付款頁面", + "licensePricingPage": "關於最新的價格和折扣,請訪問 ", + "invite": "邀請", + "inviteRegenerate": "重新生成邀請", + "inviteRegenerateDescription": "撤銷以前的邀請並創建一個新的邀請", + "inviteRemove": "移除邀請", + "inviteRemoveError": "刪除邀請失敗", + "inviteRemoveErrorDescription": "刪除邀請時出錯。", + "inviteRemoved": "邀請已刪除", + "inviteRemovedDescription": "為 {email} 創建的邀請已刪除", + "inviteQuestionRemove": "您確定要刪除邀請嗎?", + "inviteMessageRemove": "一旦刪除,這個邀請將不再有效。", + "inviteMessageConfirm": "要確認,請在下面輸入邀請的電子郵件地址。", + "inviteQuestionRegenerate": "您確定要重新邀請 {email} 嗎?這將會撤銷掉之前的邀請", + "inviteRemoveConfirm": "確認刪除邀請", + "inviteRegenerated": "重新生成邀請", + "inviteSent": "邀請郵件已成功發送至 {email}。", + "inviteSentEmail": "發送電子郵件通知給用戶", + "inviteGenerate": "已為 {email} 創建新的邀請。", + "inviteDuplicateError": "重複的邀請", + "inviteDuplicateErrorDescription": "此用戶的邀請已存在。", + "inviteRateLimitError": "超出速率限制", + "inviteRateLimitErrorDescription": "您超過了每小時3次再生的限制。請稍後再試。", + "inviteRegenerateError": "重新生成邀請失敗", + "inviteRegenerateErrorDescription": "重新生成邀請時出錯。", + "inviteValidityPeriod": "有效期", + "inviteValidityPeriodSelect": "選擇有效期", + "inviteRegenerateMessage": "邀請已重新生成。用戶必須訪問下面的連結才能接受邀請。", + "inviteRegenerateButton": "重新生成", + "expiresAt": "到期於", + "accessRoleUnknown": "未知角色", + "placeholder": "占位符", + "userErrorOrgRemove": "刪除用戶失敗", + "userErrorOrgRemoveDescription": "刪除用戶時出錯。", + "userOrgRemoved": "用戶已刪除", + "userOrgRemovedDescription": "已將 {email} 從組織中移除。", + "userQuestionOrgRemove": "您確定要從組織中刪除此用戶嗎?", + "userMessageOrgRemove": "一旦刪除,這個用戶將不再能夠訪問組織。 你總是可以稍後重新邀請他們,但他們需要再次接受邀請。", + "userRemoveOrgConfirm": "確認刪除用戶", + "userRemoveOrg": "從組織中刪除用戶", + "users": "用戶", + "accessRoleMember": "成員", + "accessRoleOwner": "所有者", + "userConfirmed": "已確認", + "idpNameInternal": "內部設置", + "emailInvalid": "無效的電子郵件地址", + "inviteValidityDuration": "請選擇持續時間", + "accessRoleSelectPlease": "請選擇一個角色", + "usernameRequired": "必須輸入使用者名稱", + "idpSelectPlease": "請選擇身份提供商", + "idpGenericOidc": "通用的 OAuth2/OIDC 提供商。", + "accessRoleErrorFetch": "獲取角色失敗", + "accessRoleErrorFetchDescription": "獲取角色時出錯", + "idpErrorFetch": "獲取身份提供者失敗", + "idpErrorFetchDescription": "獲取身份提供者時出錯", + "userErrorExists": "用戶已存在", + "userErrorExistsDescription": "此用戶已經是組織成員。", + "inviteError": "邀請用戶失敗", + "inviteErrorDescription": "邀請用戶時出錯", + "userInvited": "用戶邀請", + "userInvitedDescription": "用戶已被成功邀請。", + "userErrorCreate": "創建用戶失敗", + "userErrorCreateDescription": "創建用戶時出錯", + "userCreated": "用戶已創建", + "userCreatedDescription": "用戶已成功創建。", + "userTypeInternal": "內部用戶", + "userTypeInternalDescription": "邀請用戶直接加入您的組織。", + "userTypeExternal": "外部用戶", + "userTypeExternalDescription": "創建一個具有外部身份提供商的用戶。", + "accessUserCreateDescription": "按照下面的步驟創建一個新用戶", + "userSeeAll": "查看所有用戶", + "userTypeTitle": "用戶類型", + "userTypeDescription": "確定如何創建用戶", + "userSettings": "用戶資訊", + "userSettingsDescription": "輸入新用戶的詳細資訊", + "inviteEmailSent": "發送邀請郵件給用戶", + "inviteValid": "有效", + "selectDuration": "選擇持續時間", + "selectResource": "選擇資源", + "filterByResource": "依資源篩選", + "resetFilters": "重設篩選條件", + "totalBlocked": "被 Pangolin 阻擋的請求", + "totalRequests": "總請求數", + "requestsByCountry": "依國家/地區的請求", + "requestsByDay": "依日期的請求", + "blocked": "已阻擋", + "allowed": "已允許", + "topCountries": "熱門國家/地區", + "accessRoleSelect": "選擇角色", + "inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。", + "inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。", + "inviteExpiresIn": "邀請將在{days, plural, other {# 天}}後過期。", + "idpTitle": "身份提供商", + "idpSelect": "為外部用戶選擇身份提供商", + "idpNotConfigured": "沒有配置身份提供者。請在創建外部用戶之前配置身份提供者。", + "usernameUniq": "這必須匹配所選身份提供者中存在的唯一使用者名稱。", + "emailOptional": "電子郵件(可選)", + "nameOptional": "名稱(可選)", + "accessControls": "訪問控制", + "userDescription2": "管理此用戶的設置", + "accessRoleErrorAdd": "添加用戶到角色失敗", + "accessRoleErrorAddDescription": "添加用戶到角色時出錯。", + "userSaved": "用戶已保存", + "userSavedDescription": "用戶已更新。", + "autoProvisioned": "自動設置", + "autoProvisionedDescription": "允許此用戶由身份提供商自動管理", + "accessControlsDescription": "管理此用戶在組織中可以訪問和做什麼", + "accessControlsSubmit": "保存訪問控制", + "roles": "角色", + "accessUsersRoles": "管理用戶和角色", + "accessUsersRolesDescription": "邀請用戶並將他們添加到角色以管理訪問您的組織", + "key": "關鍵字", + "createdAt": "創建於", + "proxyErrorInvalidHeader": "無效的自訂主機 Header。使用域名格式,或將空保存為取消自訂 Header。", + "proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。", + "proxyEnableSSL": "啟用 SSL", + "proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。", + "target": "目標", + "configureTarget": "配置目標", + "targetErrorFetch": "獲取目標失敗", + "targetErrorFetchDescription": "獲取目標時出錯", + "siteErrorFetch": "獲取資源失敗", + "siteErrorFetchDescription": "獲取資源時出錯", + "targetErrorDuplicate": "重複的目標", + "targetErrorDuplicateDescription": "具有這些設置的目標已存在", + "targetWireGuardErrorInvalidIp": "無效的目標IP", + "targetWireGuardErrorInvalidIpDescription": "目標IP必須在站點子網內", + "targetsUpdated": "目標已更新", + "targetsUpdatedDescription": "目標和設置更新成功", + "targetsErrorUpdate": "更新目標失敗", + "targetsErrorUpdateDescription": "更新目標時出錯", + "targetTlsUpdate": "TLS 設置已更新", + "targetTlsUpdateDescription": "您的 TLS 設置已成功更新", + "targetErrorTlsUpdate": "更新 TLS 設置失敗", + "targetErrorTlsUpdateDescription": "更新 TLS 設置時出錯", + "proxyUpdated": "代理設置已更新", + "proxyUpdatedDescription": "您的代理設置已成功更新", + "proxyErrorUpdate": "更新代理設置失敗", + "proxyErrorUpdateDescription": "更新代理設置時出錯", + "targetAddr": "IP / 域名", + "targetPort": "埠", + "targetProtocol": "協議", + "targetTlsSettings": "安全連接配置", + "targetTlsSettingsDescription": "配置資源的 SSL/TLS 設置", + "targetTlsSettingsAdvanced": "高級TLS設置", + "targetTlsSni": "TLS 伺服器名稱", + "targetTlsSniDescription": "SNI使用的 TLS 伺服器名稱。留空使用預設值。", + "targetTlsSubmit": "保存設置", + "targets": "目標配置", + "targetsDescription": "設置目標來路由流量到您的後端服務", + "targetStickySessions": "啟用置頂會話", + "targetStickySessionsDescription": "將連接保持在同一個後端目標的整個會話中。", + "methodSelect": "選擇方法", + "targetSubmit": "添加目標", + "targetNoOne": "此資源沒有任何目標。添加目標來配置向您後端發送請求的位置。", + "targetNoOneDescription": "在上面添加多個目標將啟用負載平衡。", + "targetsSubmit": "保存目標", + "addTarget": "添加目標", + "targetErrorInvalidIp": "無效的 IP 地址", + "targetErrorInvalidIpDescription": "請輸入有效的IP位址或主機名", + "targetErrorInvalidPort": "無效的埠", + "targetErrorInvalidPortDescription": "請輸入有效的埠號", + "targetErrorNoSite": "沒有選擇站點", + "targetErrorNoSiteDescription": "請選擇目標站點", + "targetCreated": "目標已創建", + "targetCreatedDescription": "目標已成功創建", + "targetErrorCreate": "創建目標失敗", + "targetErrorCreateDescription": "創建目標時出錯", + "tlsServerName": "TLS 伺服器名稱", + "tlsServerNameDescription": "用於 SNI 的 TLS 伺服器名稱", + "save": "保存", + "proxyAdditional": "附加代理設置", + "proxyAdditionalDescription": "配置你的資源如何處理代理設置", + "proxyCustomHeader": "自訂主機 Header", + "proxyCustomHeaderDescription": "代理請求時設置的 Header。留空則使用預設值。", + "proxyAdditionalSubmit": "保存代理設置", + "subnetMaskErrorInvalid": "子網掩碼無效。必須在 0 和 32 之間。", + "ipAddressErrorInvalidFormat": "無效的 IP 地址格式", + "ipAddressErrorInvalidOctet": "無效的 IP 地址", + "path": "路徑", + "matchPath": "匹配路徑", + "ipAddressRange": "IP 範圍", + "rulesErrorFetch": "獲取規則失敗", + "rulesErrorFetchDescription": "獲取規則時出錯", + "rulesErrorDuplicate": "複製規則", + "rulesErrorDuplicateDescription": "帶有這些設置的規則已存在", + "rulesErrorInvalidIpAddressRange": "無效的 CIDR", + "rulesErrorInvalidIpAddressRangeDescription": "請輸入一個有效的 CIDR 值", + "rulesErrorInvalidUrl": "無效的 URL 路徑", + "rulesErrorInvalidUrlDescription": "請輸入一個有效的 URL 路徑值", + "rulesErrorInvalidIpAddress": "無效的 IP", + "rulesErrorInvalidIpAddressDescription": "請輸入一個有效的IP位址", + "rulesErrorUpdate": "更新規則失敗", + "rulesErrorUpdateDescription": "更新規則時出錯", + "rulesUpdated": "啟用規則", + "rulesUpdatedDescription": "規則已更新", + "rulesMatchIpAddressRangeDescription": "以 CIDR 格式輸入地址(如:103.21.244.0/22)", + "rulesMatchIpAddress": "輸入IP位址(例如,103.21.244.12)", + "rulesMatchUrl": "輸入一個 URL 路徑或模式(例如/api/v1/todos 或 /api/v1/*)", + "rulesErrorInvalidPriority": "無效的優先度", + "rulesErrorInvalidPriorityDescription": "請輸入一個有效的優先度", + "rulesErrorDuplicatePriority": "重複的優先度", + "rulesErrorDuplicatePriorityDescription": "請輸入唯一的優先度", + "ruleUpdated": "規則已更新", + "ruleUpdatedDescription": "規則更新成功", + "ruleErrorUpdate": "操作失敗", + "ruleErrorUpdateDescription": "保存過程中發生錯誤", + "rulesPriority": "優先權", + "rulesAction": "行為", + "rulesMatchType": "匹配類型", + "value": "值", + "rulesAbout": "關於規則", + "rulesAboutDescription": "規則使您能夠依據特定條件控制資源訪問權限。您可以創建基於 IP 地址或 URL 路徑的規則,以允許或拒絕訪問。", + "rulesActions": "行動", + "rulesActionAlwaysAllow": "總是允許:繞過所有身份驗證方法", + "rulesActionAlwaysDeny": "總是拒絕:阻止所有請求;無法嘗試驗證", + "rulesActionPassToAuth": "傳遞至認證:允許嘗試身份驗證方法", + "rulesMatchCriteria": "匹配條件", + "rulesMatchCriteriaIpAddress": "匹配一個指定的 IP 地址", + "rulesMatchCriteriaIpAddressRange": "在 CIDR 符號中匹配一系列IP位址", + "rulesMatchCriteriaUrl": "匹配一個 URL 路徑或模式", + "rulesEnable": "啟用規則", + "rulesEnableDescription": "啟用或禁用此資源的規則評估", + "rulesResource": "資源規則配置", + "rulesResourceDescription": "配置規則來控制對您資源的訪問", + "ruleSubmit": "添加規則", + "rulesNoOne": "沒有規則。使用表單添加規則。", + "rulesOrder": "規則按優先順序評定。", + "rulesSubmit": "保存規則", + "resourceErrorCreate": "創建資源時出錯", + "resourceErrorCreateDescription": "創建資源時出錯", + "resourceErrorCreateMessage": "創建資源時發生錯誤:", + "resourceErrorCreateMessageDescription": "發生意外錯誤", + "sitesErrorFetch": "獲取站點出錯", + "sitesErrorFetchDescription": "獲取站點時出錯", + "domainsErrorFetch": "獲取域名出錯", + "domainsErrorFetchDescription": "獲取域時出錯", + "none": "無", + "unknown": "未知", + "resources": "資源", + "resourcesDescription": "資源是您私有網路中運行的應用程式的代理。您可以為私有網路中的任何 HTTP/HTTPS 或 TCP/UDP 服務創建資源。每個資源都必須連接到一個站點,以通過加密的 WireGuard 隧道實現私密且安全的連接。", + "resourcesWireGuardConnect": "採用 WireGuard 提供的加密安全連接", + "resourcesMultipleAuthenticationMethods": "配置多個身份驗證方法", + "resourcesUsersRolesAccess": "基於用戶和角色的訪問控制", + "resourcesErrorUpdate": "切換資源失敗", + "resourcesErrorUpdateDescription": "更新資源時出錯", + "access": "訪問權限", + "shareLink": "{resource} 的分享連結", + "resourceSelect": "選擇資源", + "shareLinks": "分享連結", + "share": "分享連結", + "shareDescription2": "創建資源共享連結。連結提供對資源的臨時或無限制訪問。 當您創建連結時,您可以配置連結的到期時間。", + "shareEasyCreate": "輕鬆創建和分享", + "shareConfigurableExpirationDuration": "可配置的過期時間", + "shareSecureAndRevocable": "安全和可撤銷的", + "nameMin": "名稱長度必須大於 {len} 字元。", + "nameMax": "名稱長度必須小於 {len} 字元。", + "sitesConfirmCopy": "請確認您已經複製了配置。", + "unknownCommand": "未知命令", + "newtErrorFetchReleases": "無法獲取版本資訊: {err}", + "newtErrorFetchLatest": "無法獲取最新版資訊: {err}", + "newtEndpoint": "Newt 端點", + "newtId": "Newt ID", + "newtSecretKey": "Newt 私鑰", + "architecture": "架構", + "sites": "站點", + "siteWgAnyClients": "使用任何 WireGuard 用戶端連接。您必須使用對等IP解決您的內部資源。", + "siteWgCompatibleAllClients": "與所有 WireGuard 用戶端相容", + "siteWgManualConfigurationRequired": "需要手動配置", + "userErrorNotAdminOrOwner": "用戶不是管理員或所有者", + "pangolinSettings": "設置 - Pangolin", + "accessRoleYour": "您的角色:", + "accessRoleSelect2": "選擇角色", + "accessUserSelect": "選擇一個用戶", + "otpEmailEnter": "輸入電子郵件", + "otpEmailEnterDescription": "在輸入欄位輸入後按 Enter 鍵添加電子郵件。", + "otpEmailErrorInvalid": "無效的信箱地址。通配符(*)必須占據整個開頭部分。", + "otpEmailSmtpRequired": "需要先配置 SMTP", + "otpEmailSmtpRequiredDescription": "必須在伺服器上啟用 SMTP 才能使用一次性密碼驗證。", + "otpEmailTitle": "一次性密碼", + "otpEmailTitleDescription": "資源訪問需要基於電子郵件的身份驗證", + "otpEmailWhitelist": "電子郵件白名單", + "otpEmailWhitelistList": "白名單郵件", + "otpEmailWhitelistListDescription": "只有擁有這些電子郵件地址的用戶才能訪問此資源。 他們將被提示輸入一次性密碼發送到他們的電子郵件。 通配符 (*@example.com) 可以用來允許來自一個域名的任何電子郵件地址。", + "otpEmailWhitelistSave": "保存白名單", + "passwordAdd": "添加密碼", + "passwordRemove": "刪除密碼", + "pincodeAdd": "添加 PIN 碼", + "pincodeRemove": "移除 PIN 碼", + "resourceAuthMethods": "身份驗證方法", + "resourceAuthMethodsDescriptions": "允許透過額外的認證方法訪問資源", + "resourceAuthSettingsSave": "保存成功", + "resourceAuthSettingsSaveDescription": "已保存身份驗證設置", + "resourceErrorAuthFetch": "獲取數據失敗", + "resourceErrorAuthFetchDescription": "獲取數據時出錯", + "resourceErrorPasswordRemove": "刪除資源密碼出錯", + "resourceErrorPasswordRemoveDescription": "刪除資源密碼時出錯", + "resourceErrorPasswordSetup": "設置資源密碼出錯", + "resourceErrorPasswordSetupDescription": "設置資源密碼時出錯", + "resourceErrorPincodeRemove": "刪除資源固定碼時出錯", + "resourceErrorPincodeRemoveDescription": "刪除資源PIN碼時出錯", + "resourceErrorPincodeSetup": "設置資源 PIN 碼時出錯", + "resourceErrorPincodeSetupDescription": "設置資源 PIN 碼時發生錯誤", + "resourceErrorUsersRolesSave": "設置角色失敗", + "resourceErrorUsersRolesSaveDescription": "設置角色時出錯", + "resourceErrorWhitelistSave": "保存白名單失敗", + "resourceErrorWhitelistSaveDescription": "保存白名單時出錯", + "resourcePasswordSubmit": "啟用密碼保護", + "resourcePasswordProtection": "密碼保護 {status}", + "resourcePasswordRemove": "已刪除資源密碼", + "resourcePasswordRemoveDescription": "已成功刪除資源密碼", + "resourcePasswordSetup": "設置資源密碼", + "resourcePasswordSetupDescription": "已成功設置資源密碼", + "resourcePasswordSetupTitle": "設置密碼", + "resourcePasswordSetupTitleDescription": "設置密碼來保護此資源", + "resourcePincode": "PIN 碼", + "resourcePincodeSubmit": "啟用 PIN 碼保護", + "resourcePincodeProtection": "PIN 碼保護 {status}", + "resourcePincodeRemove": "資源 PIN 碼已刪除", + "resourcePincodeRemoveDescription": "已成功刪除資源 PIN 碼", + "resourcePincodeSetup": "資源 PIN 碼已設置", + "resourcePincodeSetupDescription": "資源 PIN 碼已成功設置", + "resourcePincodeSetupTitle": "設置 PIN 碼", + "resourcePincodeSetupTitleDescription": "設置 PIN 碼來保護此資源", + "resourceRoleDescription": "管理員總是可以訪問此資源。", + "resourceUsersRoles": "用戶和角色", + "resourceUsersRolesDescription": "配置用戶和角色可以訪問此資源", + "resourceUsersRolesSubmit": "保存用戶和角色", + "resourceWhitelistSave": "保存成功", + "resourceWhitelistSaveDescription": "白名單設置已保存", + "ssoUse": "使用平台 SSO", + "ssoUseDescription": "對於所有啟用此功能的資源,現有用戶只需登錄一次。", + "proxyErrorInvalidPort": "無效的埠號", + "subdomainErrorInvalid": "無效的子域", + "domainErrorFetch": "獲取域名失敗", + "domainErrorFetchDescription": "獲取域名時出錯", + "resourceErrorUpdate": "更新資源失敗", + "resourceErrorUpdateDescription": "更新資源時出錯", + "resourceUpdated": "資源已更新", + "resourceUpdatedDescription": "資源已成功更新", + "resourceErrorTransfer": "轉移資源失敗", + "resourceErrorTransferDescription": "轉移資源時出錯", + "resourceTransferred": "資源已傳輸", + "resourceTransferredDescription": "資源已成功傳輸", + "resourceErrorToggle": "切換資源失敗", + "resourceErrorToggleDescription": "更新資源時出錯", + "resourceVisibilityTitle": "可見性", + "resourceVisibilityTitleDescription": "完全啟用或禁用資源可見性", + "resourceGeneral": "常規設置", + "resourceGeneralDescription": "配置此資源的常規設置", + "resourceEnable": "啟用資源", + "resourceTransfer": "轉移資源", + "resourceTransferDescription": "將此資源轉移到另一個站點", + "resourceTransferSubmit": "轉移資源", + "siteDestination": "目標站點", + "searchSites": "搜索站點", + "countries": "國家/地區", + "accessRoleCreate": "創建角色", + "accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。", + "accessRoleCreateSubmit": "創建角色", + "accessRoleCreated": "角色已創建", + "accessRoleCreatedDescription": "角色已成功創建。", + "accessRoleErrorCreate": "創建角色失敗", + "accessRoleErrorCreateDescription": "創建角色時出錯。", + "accessRoleErrorNewRequired": "需要新角色", + "accessRoleErrorRemove": "刪除角色失敗", + "accessRoleErrorRemoveDescription": "刪除角色時出錯。", + "accessRoleName": "角色名稱", + "accessRoleQuestionRemove": "您即將刪除 {name} 角色。 此操作無法撤銷。", + "accessRoleRemove": "刪除角色", + "accessRoleRemoveDescription": "從組織中刪除角色", + "accessRoleRemoveSubmit": "刪除角色", + "accessRoleRemoved": "角色已刪除", + "accessRoleRemovedDescription": "角色已成功刪除。", + "accessRoleRequiredRemove": "刪除此角色之前,請選擇一個新角色來轉移現有成員。", + "manage": "管理", + "sitesNotFound": "未找到站點。", + "pangolinServerAdmin": "伺服器管理員 - Pangolin", + "licenseTierProfessional": "專業許可證", + "licenseTierEnterprise": "企業許可證", + "licenseTierPersonal": "個人許可證", + "licensed": "已授權", + "yes": "是", + "no": "否", + "sitesAdditional": "其他站點", + "licenseKeys": "許可證金鑰", + "sitestCountDecrease": "減少站點數量", + "sitestCountIncrease": "增加站點數量", + "idpManage": "管理身份提供商", + "idpManageDescription": "查看和管理系統中的身份提供商", + "idpDeletedDescription": "身份提供商刪除成功", + "idpOidc": "OAuth2/OIDC", + "idpQuestionRemove": "您確定要永久刪除身份提供者嗎?", + "idpMessageRemove": "這將刪除身份提供者和所有相關的配置。透過此提供者進行身份驗證的用戶將無法登錄。", + "idpMessageConfirm": "要確認,請在下面輸入身份提供者的名稱。", + "idpConfirmDelete": "確認刪除身份提供商", + "idpDelete": "刪除身份提供商", + "idp": "身份提供商", + "idpSearch": "搜索身份提供者...", + "idpAdd": "添加身份提供商", + "idpClientIdRequired": "用戶端 ID 是必需的。", + "idpClientSecretRequired": "用戶端金鑰是必需的。", + "idpErrorAuthUrlInvalid": "身份驗證 URL 必須是有效的 URL。", + "idpErrorTokenUrlInvalid": "令牌 URL 必須是有效的 URL。", + "idpPathRequired": "標識符路徑是必需的。", + "idpScopeRequired": "授權範圍是必需的。", + "idpOidcDescription": "配置 OpenID 連接身份提供商", + "idpCreatedDescription": "身份提供商創建成功", + "idpCreate": "創建身份提供商", + "idpCreateDescription": "配置用戶身份驗證的新身份提供商", + "idpSeeAll": "查看所有身份提供商", + "idpSettingsDescription": "配置身份提供者的基本資訊", + "idpDisplayName": "此身份提供商的顯示名稱", + "idpAutoProvisionUsers": "自動提供用戶", + "idpAutoProvisionUsersDescription": "如果啟用,用戶將在首次登錄時自動在系統中創建,並且能夠映射用戶到角色和組織。", + "licenseBadge": "EE", + "idpType": "提供者類型", + "idpTypeDescription": "選擇您想要配置的身份提供者類型", + "idpOidcConfigure": "OAuth2/OIDC 配置", + "idpOidcConfigureDescription": "配置 OAuth2/OIDC 供應商端點和憑據", + "idpClientId": "用戶端ID", + "idpClientIdDescription": "來自您身份提供商的 OAuth2 用戶端 ID", + "idpClientSecret": "用戶端金鑰", + "idpClientSecretDescription": "來自身份提供商的 OAuth2 用戶端金鑰", + "idpAuthUrl": "授權 URL", + "idpAuthUrlDescription": "OAuth2 授權端點的 URL", + "idpTokenUrl": "令牌 URL", + "idpTokenUrlDescription": "OAuth2 令牌端點的 URL", + "idpOidcConfigureAlert": "重要提示", + "idpOidcConfigureAlertDescription": "創建身份提供方後,您需要在其設置中配置回調 URL。回調 URL 會在創建成功後提供。", + "idpToken": "令牌配置", + "idpTokenDescription": "配置如何從 ID 令牌中提取用戶資訊", + "idpJmespathAbout": "關於 JMESPath", + "idpJmespathAboutDescription": "以下路徑使用 JMESPath 語法從 ID 令牌中提取值。", + "idpJmespathAboutDescriptionLink": "了解更多 JMESPath 資訊", + "idpJmespathLabel": "標識符路徑", + "idpJmespathLabelDescription": "ID 令牌中用戶標識符的路徑", + "idpJmespathEmailPathOptional": "信箱路徑(可選)", + "idpJmespathEmailPathOptionalDescription": "ID 令牌中用戶信箱的路徑", + "idpJmespathNamePathOptional": "使用者名稱路徑(可選)", + "idpJmespathNamePathOptionalDescription": "ID 令牌中使用者名稱的路徑", + "idpOidcConfigureScopes": "作用域(Scopes)", + "idpOidcConfigureScopesDescription": "以空格分隔的 OAuth2 請求作用域列表", + "idpSubmit": "創建身份提供商", + "orgPolicies": "組織策略", + "idpSettings": "{idpName} 設置", + "idpCreateSettingsDescription": "配置身份提供商的設置", + "roleMapping": "角色映射", + "orgMapping": "組織映射", + "orgPoliciesSearch": "搜索組織策略...", + "orgPoliciesAdd": "添加組織策略", + "orgRequired": "組織是必填項", + "error": "錯誤", + "success": "成功", + "orgPolicyAddedDescription": "策略添加成功", + "orgPolicyUpdatedDescription": "策略更新成功", + "orgPolicyDeletedDescription": "已成功刪除策略", + "defaultMappingsUpdatedDescription": "默認映射更新成功", + "orgPoliciesAbout": "關於組織政策", + "orgPoliciesAboutDescription": "組織策略用於根據用戶的 ID 令牌來控制對組織的訪問。 您可以指定 JMESPath 表達式來提取角色和組織資訊從 ID 令牌中提取資訊。", + "orgPoliciesAboutDescriptionLink": "欲了解更多資訊,請參閱文件。", + "defaultMappingsOptional": "默認映射(可選)", + "defaultMappingsOptionalDescription": "當沒有為某個組織定義組織的政策時,使用默認映射。 您可以指定默認角色和組織映射回到這裡。", + "defaultMappingsRole": "默認角色映射", + "defaultMappingsRoleDescription": "此表達式的結果必須返回組織中定義的角色名稱作為字串。", + "defaultMappingsOrg": "默認組織映射", + "defaultMappingsOrgDescription": "此表達式必須返回 組織ID 或 true 才能允許用戶訪問組織。", + "defaultMappingsSubmit": "保存默認映射", + "orgPoliciesEdit": "編輯組織策略", + "org": "組織", + "orgSelect": "選擇組織", + "orgSearch": "搜索", + "orgNotFound": "找不到組織。", + "roleMappingPathOptional": "角色映射路徑(可選)", + "orgMappingPathOptional": "組織映射路徑(可選)", + "orgPolicyUpdate": "更新策略", + "orgPolicyAdd": "添加策略", + "orgPolicyConfig": "配置組織訪問權限", + "idpUpdatedDescription": "身份提供商更新成功", + "redirectUrl": "重定向網址", + "orgIdpRedirectUrls": "重新導向網址", + "redirectUrlAbout": "關於重定向網址", + "redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。", + "pangolinAuth": "認證 - Pangolin", + "verificationCodeLengthRequirements": "您的驗證碼必須是 8 個字元。", + "errorOccurred": "發生錯誤", + "emailErrorVerify": "驗證電子郵件失敗:", + "emailVerified": "電子郵件驗證成功!重定向您...", + "verificationCodeErrorResend": "無法重新發送驗證碼:", + "verificationCodeResend": "驗證碼已重新發送", + "verificationCodeResendDescription": "我們已將驗證碼重新發送到您的電子郵件地址。請檢查您的收件箱。", + "emailVerify": "驗證電子郵件", + "emailVerifyDescription": "輸入驗證碼發送到您的電子郵件地址。", + "verificationCode": "驗證碼", + "verificationCodeEmailSent": "我們向您的電子郵件地址發送了驗證碼。", + "submit": "提交", + "emailVerifyResendProgress": "正在重新發送...", + "emailVerifyResend": "沒有收到代碼?點擊此處重新發送", + "passwordNotMatch": "密碼不匹配", + "signupError": "註冊時出錯", + "pangolinLogoAlt": "Pangolin 標誌", + "inviteAlready": "看起來您已被邀請!", + "inviteAlreadyDescription": "要接受邀請,您必須登錄或創建一個帳戶。", + "signupQuestion": "已經有一個帳戶?", + "login": "登錄", + "resourceNotFound": "找不到資源", + "resourceNotFoundDescription": "您要訪問的資源不存在。", + "pincodeRequirementsLength": "PIN碼必須是 6 位數字", + "pincodeRequirementsChars": "PIN 必須只包含數字", + "passwordRequirementsLength": "密碼必須至少 1 個字元長", + "passwordRequirementsTitle": "密碼要求:", + "passwordRequirementLength": "至少 8 個字元長", + "passwordRequirementUppercase": "至少一個大寫字母", + "passwordRequirementLowercase": "至少一個小寫字母", + "passwordRequirementNumber": "至少一個數字", + "passwordRequirementSpecial": "至少一個特殊字元", + "passwordRequirementsMet": "✓ 密碼滿足所有要求", + "passwordStrength": "密碼強度", + "passwordStrengthWeak": "弱", + "passwordStrengthMedium": "中", + "passwordStrengthStrong": "強", + "passwordRequirements": "要求:", + "passwordRequirementLengthText": "8+ 個字元", + "passwordRequirementUppercaseText": "大寫字母 (A-Z)", + "passwordRequirementLowercaseText": "小寫字母 (a-z)", + "passwordRequirementNumberText": "數字 (0-9)", + "passwordRequirementSpecialText": "特殊字元 (!@#$%...)", + "passwordsDoNotMatch": "密碼不匹配", + "otpEmailRequirementsLength": "OTP 必須至少 1 個字元長", + "otpEmailSent": "OTP 已發送", + "otpEmailSentDescription": "OTP 已經發送到您的電子郵件", + "otpEmailErrorAuthenticate": "通過電子郵件身份驗證失敗", + "pincodeErrorAuthenticate": "Pincode 驗證失敗", + "passwordErrorAuthenticate": "密碼驗證失敗", + "poweredBy": "支持者:", + "authenticationRequired": "需要身份驗證", + "authenticationMethodChoose": "請選擇您偏好的方式來訪問 {name}", + "authenticationRequest": "您必須通過身份驗證才能訪問 {name}", + "user": "用戶", + "pincodeInput": "6 位數字 PIN 碼", + "pincodeSubmit": "使用 PIN 登錄", + "passwordSubmit": "使用密碼登錄", + "otpEmailDescription": "一次性代碼將發送到此電子郵件。", + "otpEmailSend": "發送一次性代碼", + "otpEmail": "一次性密碼 (OTP)", + "otpEmailSubmit": "提交 OTP", + "backToEmail": "回到電子郵件", + "noSupportKey": "伺服器當前未使用支持者金鑰,歡迎支持本項目!", + "accessDenied": "訪問被拒絕", + "accessDeniedDescription": "當前帳戶無權訪問此資源。如認為這是錯誤,請與管理員聯繫。", + "accessTokenError": "檢查訪問令牌時出錯", + "accessGranted": "已授予訪問", + "accessUrlInvalid": "訪問 URL 無效", + "accessGrantedDescription": "您已獲准訪問此資源,正在為您跳轉...", + "accessUrlInvalidDescription": "此共享訪問URL無效。請聯絡資源所有者獲取新URL。", + "tokenInvalid": "無效的令牌", + "pincodeInvalid": "無效的代碼", + "passwordErrorRequestReset": "請求重設失敗:", + "passwordErrorReset": "重設密碼失敗:", + "passwordResetSuccess": "密碼重設成功!返回登錄...", + "passwordReset": "重設密碼", + "passwordResetDescription": "按照步驟重設您的密碼", + "passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。", + "passwordResetCode": "驗證碼", + "passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。", + "generatePasswordResetCode": "產生密碼重設代碼", + "passwordResetCodeGenerated": "密碼重設代碼已產生", + "passwordResetCodeGeneratedDescription": "請將此代碼分享給使用者。他們可以用它來重設密碼。", + "passwordResetUrl": "重設網址", + "passwordNew": "新密碼", + "passwordNewConfirm": "確認新密碼", + "changePassword": "更改密碼", + "changePasswordDescription": "更新您的帳戶密碼", + "oldPassword": "當前密碼", + "newPassword": "新密碼", + "confirmNewPassword": "確認新密碼", + "changePasswordError": "更改密碼失敗", + "changePasswordErrorDescription": "更改您的密碼時出錯", + "changePasswordSuccess": "密碼修改成功", + "changePasswordSuccessDescription": "您的密碼已成功更新", + "passwordExpiryRequired": "需要密碼過期", + "passwordExpiryDescription": "該機構要求您每 {maxDays} 天更改一次密碼。", + "changePasswordNow": "現在更改密碼", + "pincodeAuth": "驗證器代碼", + "pincodeSubmit2": "提交代碼", + "passwordResetSubmit": "請求重設", + "passwordResetAlreadyHaveCode": "輸入代碼", + "passwordResetSmtpRequired": "請聯絡您的管理員", + "passwordResetSmtpRequiredDescription": "需要密碼重設代碼才能重設您的密碼。請聯絡您的管理員尋求協助。", + "passwordBack": "回到密碼", + "loginBack": "返回登錄", + "signup": "註冊", + "loginStart": "登錄以開始", + "idpOidcTokenValidating": "正在驗證 OIDC 令牌", + "idpOidcTokenResponse": "驗證 OIDC 令牌響應", + "idpErrorOidcTokenValidating": "驗證 OIDC 令牌出錯", + "idpConnectingTo": "連接到{name}", + "idpConnectingToDescription": "正在驗證您的身份", + "idpConnectingToProcess": "正在連接...", + "idpConnectingToFinished": "已連接", + "idpErrorConnectingTo": "無法連接到 {name},請聯絡管理員協助處理。", + "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} TB", + "gigabytes": "{count} GB", + "megabytes": "{count} MB", + "tagsEntered": "已輸入的標籤", + "tagsEnteredDescription": "這些是您輸入的標籤。", + "tagsWarnCannotBeLessThanZero": "最大標籤和最小標籤不能小於 0", + "tagsWarnNotAllowedAutocompleteOptions": "標記不允許為每個自動完成選項", + "tagsWarnInvalid": "無效的標籤,每個有效標籤", + "tagWarnTooShort": "標籤 {tagText} 太短", + "tagWarnTooLong": "標籤 {tagText} 太長", + "tagsWarnReachedMaxNumber": "已達到允許標籤的最大數量", + "tagWarnDuplicate": "未添加重複標籤 {tagText}", + "supportKeyInvalid": "無效金鑰", + "supportKeyInvalidDescription": "您的支持者金鑰無效。", + "supportKeyValid": "有效的金鑰", + "supportKeyValidDescription": "您的支持者金鑰已被驗證。感謝您的支持!", + "supportKeyErrorValidationDescription": "驗證支持者金鑰失敗。", + "supportKey": "支持開發和通過一個 Pangolin !", + "supportKeyDescription": "購買支持者鑰匙,幫助我們繼續為社區發展 Pangolin 。 您的貢獻使我們能夠投入更多的時間來維護和添加所有人的新功能。 我們永遠不會用這個來支付牆上的功能。這與任何商業版是分開的。", + "supportKeyPet": "您還可以領養並見到屬於自己的 Pangolin!", + "supportKeyPurchase": "付款通過 GitHub 進行處理,之後您可以在以下位置獲取您的金鑰:", + "supportKeyPurchaseLink": "我們的網站", + "supportKeyPurchase2": "並在這裡兌換。", + "supportKeyLearnMore": "了解更多。", + "supportKeyOptions": "請選擇最適合您的選項。", + "supportKetOptionFull": "完全支持者", + "forWholeServer": "適用於整個伺服器", + "lifetimePurchase": "終身購買", + "supporterStatus": "支持者狀態", + "buy": "購買", + "supportKeyOptionLimited": "有限支持者", + "forFiveUsers": "適用於 5 或更少用戶", + "supportKeyRedeem": "兌換支持者金鑰", + "supportKeyHideSevenDays": "隱藏 7 天", + "supportKeyEnter": "輸入支持者金鑰", + "supportKeyEnterDescription": "見到你自己的 Pangolin!", + "githubUsername": "GitHub 使用者名稱", + "supportKeyInput": "支持者金鑰", + "supportKeyBuy": "購買支持者金鑰", + "logoutError": "註銷錯誤", + "signingAs": "登錄為", + "serverAdmin": "伺服器管理員", + "managedSelfhosted": "託管自託管", + "otpEnable": "啟用雙因子認證", + "otpDisable": "禁用雙因子認證", + "logout": "登出", + "licenseTierProfessionalRequired": "需要專業版", + "licenseTierProfessionalRequiredDescription": "此功能僅在專業版可用。", + "actionGetOrg": "獲取組織", + "updateOrgUser": "更新組織用戶", + "createOrgUser": "創建組織用戶", + "actionUpdateOrg": "更新組織", + "actionRemoveInvitation": "移除邀請", + "actionUpdateUser": "更新用戶", + "actionGetUser": "獲取用戶", + "actionGetOrgUser": "獲取組織用戶", + "actionListOrgDomains": "列出組織域", + "actionCreateSite": "創建站點", + "actionDeleteSite": "刪除站點", + "actionGetSite": "獲取站點", + "actionListSites": "站點列表", + "actionApplyBlueprint": "應用藍圖", + "actionListBlueprints": "藍圖列表", + "actionGetBlueprint": "獲取藍圖", + "setupToken": "設置令牌", + "setupTokenDescription": "從伺服器控制台輸入設定令牌。", + "setupTokenRequired": "需要設置令牌", + "actionUpdateSite": "更新站點", + "actionListSiteRoles": "允許站點角色列表", + "actionCreateResource": "創建資源", + "actionDeleteResource": "刪除資源", + "actionGetResource": "獲取資源", + "actionListResource": "列出資源", + "actionUpdateResource": "更新資源", + "actionListResourceUsers": "列出資源用戶", + "actionSetResourceUsers": "設置資源用戶", + "actionSetAllowedResourceRoles": "設置允許的資源角色", + "actionListAllowedResourceRoles": "列出允許的資源角色", + "actionSetResourcePassword": "設置資源密碼", + "actionSetResourcePincode": "設置資源粉碼", + "actionSetResourceEmailWhitelist": "設置資源電子郵件白名單", + "actionGetResourceEmailWhitelist": "獲取資源電子郵件白名單", + "actionCreateTarget": "創建目標", + "actionDeleteTarget": "刪除目標", + "actionGetTarget": "獲取目標", + "actionListTargets": "列表目標", + "actionUpdateTarget": "更新目標", + "actionCreateRole": "創建角色", + "actionDeleteRole": "刪除角色", + "actionGetRole": "獲取角色", + "actionListRole": "角色列表", + "actionUpdateRole": "更新角色", + "actionListAllowedRoleResources": "列表允許的角色資源", + "actionInviteUser": "邀請用戶", + "actionRemoveUser": "刪除用戶", + "actionListUsers": "列出用戶", + "actionAddUserRole": "添加用戶角色", + "actionSetUserOrgRoles": "Set User Roles", + "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", + "actionGetIdp": "獲取 IDP", + "actionCreateIdpOrg": "創建 IDP 組織策略", + "actionDeleteIdpOrg": "刪除 IDP 組織策略", + "actionListIdpOrgs": "列出 IDP 組織", + "actionUpdateIdpOrg": "更新 IDP 組織", + "actionCreateClient": "創建用戶端", + "actionDeleteClient": "刪除用戶端", + "actionUpdateClient": "更新用戶端", + "actionListClients": "列出用戶端", + "actionGetClient": "獲取用戶端", + "actionCreateSiteResource": "創建站點資源", + "actionDeleteSiteResource": "刪除站點資源", + "actionGetSiteResource": "獲取站點資源", + "actionListSiteResources": "列出站點資源", + "actionUpdateSiteResource": "更新站點資源", + "actionListInvitations": "邀請列表", + "actionExportLogs": "匯出日誌", + "actionViewLogs": "查看日誌", + "noneSelected": "未選擇", + "orgNotFound2": "未找到組織。", + "searchProgress": "搜索中...", + "create": "創建", + "orgs": "組織", + "loginError": "登錄時出錯", + "loginRequiredForDevice": "需要登入以驗證您的裝置。", + "passwordForgot": "忘記密碼?", + "otpAuth": "兩步驗證", + "otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。", + "otpAuthSubmit": "提交代碼", + "idpContinue": "或者繼續", + "otpAuthBack": "返回登錄", + "navbar": "導航菜單", + "navbarDescription": "應用程式的主導航菜單", + "navbarDocsLink": "文件", + "otpErrorEnable": "無法啟用 2FA", + "otpErrorEnableDescription": "啟用 2FA 時出錯", + "otpSetupCheckCode": "請輸入您的 6 位數字代碼", + "otpSetupCheckCodeRetry": "無效的代碼。請重試。", + "otpSetup": "啟用兩步驗證", + "otpSetupDescription": "用額外的保護層來保護您的帳戶", + "otpSetupScanQr": "用您的身份驗證程序掃描此二維碼或手動輸入金鑰:", + "otpSetupSecretCode": "驗證器代碼", + "otpSetupSuccess": "啟用兩步驗證", + "otpSetupSuccessStoreBackupCodes": "您的帳戶現在更加安全。不要忘記保存您的備份代碼。", + "otpErrorDisable": "無法禁用 2FA", + "otpErrorDisableDescription": "禁用 2FA 時出錯", + "otpRemove": "禁用兩步驗證", + "otpRemoveDescription": "為您的帳戶禁用兩步驗證", + "otpRemoveSuccess": "雙重身份驗證已禁用", + "otpRemoveSuccessMessage": "您的帳戶已禁用雙重身份驗證。您可以隨時再次啟用它。", + "otpRemoveSubmit": "禁用兩步驗證", + "paginator": "第 {current} 頁,共 {last} 頁", + "paginatorToFirst": "轉到第一頁", + "paginatorToPrevious": "轉到上一頁", + "paginatorToNext": "轉到下一頁", + "paginatorToLast": "轉到最後一頁", + "copyText": "複製文本", + "copyTextFailed": "複製文本失敗: ", + "copyTextClipboard": "複製到剪貼簿", + "inviteErrorInvalidConfirmation": "無效確認", + "passwordRequired": "必須填寫密碼", + "allowAll": "允許所有", + "permissionsAllowAll": "允許所有權限", + "githubUsernameRequired": "必須填寫 GitHub 使用者名稱", + "supportKeyRequired": "必須填寫支持者金鑰", + "passwordRequirementsChars": "密碼至少需要 8 個字元", + "language": "語言", + "verificationCodeRequired": "必須輸入代碼", + "userErrorNoUpdate": "沒有要更新的用戶", + "siteErrorNoUpdate": "沒有要更新的站點", + "resourceErrorNoUpdate": "沒有可更新的資源", + "authErrorNoUpdate": "沒有要更新的身份驗證資訊", + "orgErrorNoUpdate": "沒有要更新的組織", + "orgErrorNoProvided": "未提供組織", + "apiKeysErrorNoUpdate": "沒有要更新的 API 金鑰", + "sidebarOverview": "概覽", + "sidebarHome": "首頁", + "sidebarSites": "站點", + "sidebarResources": "資源", + "sidebarProxyResources": "公開", + "sidebarClientResources": "私有", + "sidebarAccessControl": "訪問控制", + "sidebarLogsAndAnalytics": "日誌與分析", + "sidebarUsers": "用戶", + "sidebarAdmin": "管理員", + "sidebarInvitations": "邀請", + "sidebarRoles": "角色", + "sidebarShareableLinks": "分享連結", + "sidebarApiKeys": "API 金鑰", + "sidebarSettings": "設置", + "sidebarAllUsers": "所有用戶", + "sidebarIdentityProviders": "身份提供商", + "sidebarLicense": "證書", + "sidebarClients": "用戶端", + "sidebarUserDevices": "使用者", + "sidebarMachineClients": "機器", + "sidebarDomains": "域", + "sidebarGeneral": "管理", + "sidebarLogAndAnalytics": "日誌與分析", + "sidebarBluePrints": "藍圖", + "sidebarOrganization": "組織", + "sidebarLogsAnalytics": "分析", + "blueprints": "藍圖", + "blueprintsDescription": "應用聲明配置並查看先前運行的", + "blueprintAdd": "添加藍圖", + "blueprintGoBack": "查看所有藍圖", + "blueprintCreate": "創建藍圖", + "blueprintCreateDescription2": "按照下面的步驟創建和應用新的藍圖", + "blueprintDetails": "藍圖詳細資訊", + "blueprintDetailsDescription": "查看應用藍圖的結果和發生的任何錯誤", + "blueprintInfo": "藍圖資訊", + "message": "留言", + "blueprintContentsDescription": "定義描述您基礎設施的 YAML 內容", + "blueprintErrorCreateDescription": "應用藍圖時出錯", + "blueprintErrorCreate": "創建藍圖時出錯", + "searchBlueprintProgress": "搜索藍圖...", + "appliedAt": "應用於", + "source": "來源", + "contents": "目錄", + "parsedContents": "解析內容 (只讀)", + "enableDockerSocket": "啟用 Docker 藍圖", + "enableDockerSocketDescription": "啟用 Docker Socket 標籤擦除藍圖標籤。套接字路徑必須提供給新的。", + "enableDockerSocketLink": "了解更多", + "viewDockerContainers": "查看停靠容器", + "containersIn": "{siteName} 中的容器", + "selectContainerDescription": "選擇任何容器作為目標的主機名。點擊埠使用埠。", + "containerName": "名稱", + "containerImage": "圖片", + "containerState": "狀態", + "containerNetworks": "網路", + "containerHostnameIp": "主機名/IP", + "containerLabels": "標籤", + "containerLabelsCount": "{count, plural, other {# 標籤}}", + "containerLabelsTitle": "容器標籤", + "containerLabelEmpty": "<為空>", + "containerPorts": "埠", + "containerPortsMore": "+{count} 更多", + "containerActions": "行動", + "select": "選擇", + "noContainersMatchingFilters": "沒有找到匹配當前過濾器的容器。", + "showContainersWithoutPorts": "顯示沒有埠的容器", + "showStoppedContainers": "顯示已停止的容器", + "noContainersFound": "未找到容器。請確保 Docker 容器正在運行。", + "searchContainersPlaceholder": "在 {count} 個容器中搜索...", + "searchResultsCount": "{count, plural, other {# 個結果}}", + "filters": "篩選器", + "filterOptions": "過濾器選項", + "filterPorts": "埠", + "filterStopped": "已停止", + "clearAllFilters": "清除所有過濾器", + "columns": "列", + "toggleColumns": "切換列", + "refreshContainersList": "刷新容器列表", + "searching": "搜索中...", + "noContainersFoundMatching": "未找到與 \"{filter}\" 匹配的容器。", + "light": "淺色", + "dark": "深色", + "system": "系統", + "theme": "主題", + "subnetRequired": "子網是必填項", + "initialSetupTitle": "初始伺服器設置", + "initialSetupDescription": "創建初始伺服器管理員帳戶。 只能存在一個伺服器管理員。 您可以隨時更改這些憑據。", + "createAdminAccount": "創建管理員帳戶", + "setupErrorCreateAdmin": "創建伺服器管理員帳戶時發生錯誤。", + "certificateStatus": "證書狀態", + "loading": "載入中", + "restart": "重啟", + "domains": "域", + "domainsDescription": "管理您的組織域", + "domainsSearch": "搜索域...", + "domainAdd": "添加域", + "domainAddDescription": "在您的組織中註冊新域", + "domainCreate": "創建域", + "domainCreatedDescription": "域創建成功", + "domainDeletedDescription": "成功刪除域", + "domainQuestionRemove": "您確定要從您的帳戶中刪除域名嗎?", + "domainMessageRemove": "移除後,該域將不再與您的帳戶關聯。", + "domainConfirmDelete": "確認刪除域", + "domainDelete": "刪除域", + "domain": "域", + "selectDomainTypeNsName": "域委派(NS)", + "selectDomainTypeNsDescription": "此域及其所有子域。當您希望控制整個域區域時使用此選項。", + "selectDomainTypeCnameName": "單個域(CNAME)", + "selectDomainTypeCnameDescription": "僅此特定域。用於單個子域或特定域條目。", + "selectDomainTypeWildcardName": "通配符域", + "selectDomainTypeWildcardDescription": "此域名及其子域名。", + "domainDelegation": "單個域", + "selectType": "選擇一個類型", + "actions": "操作", + "refresh": "刷新", + "refreshError": "刷新數據失敗", + "verified": "已驗證", + "pending": "待定", + "sidebarBilling": "計費", + "billing": "計費", + "orgBillingDescription": "管理您的帳單資訊和訂閱", + "github": "GitHub", + "pangolinHosted": "Pangolin 託管", + "fossorial": "Fossorial", + "completeAccountSetup": "完成帳戶設定", + "completeAccountSetupDescription": "設置您的密碼以開始", + "accountSetupSent": "我們將發送帳號設定代碼到該電子郵件地址。", + "accountSetupCode": "設置代碼", + "accountSetupCodeDescription": "請檢查您的信箱以獲取設置代碼。", + "passwordCreate": "創建密碼", + "passwordCreateConfirm": "確認密碼", + "accountSetupSubmit": "發送設置代碼", + "completeSetup": "完成設置", + "accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!", + "documentation": "文件", + "saveAllSettings": "保存所有設置", + "saveResourceTargets": "儲存目標", + "saveResourceHttp": "儲存代理設定", + "saveProxyProtocol": "儲存代理協定設定", + "settingsUpdated": "設置已更新", + "settingsUpdatedDescription": "所有設置已成功更新", + "settingsErrorUpdate": "設置更新失敗", + "settingsErrorUpdateDescription": "更新設置時發生錯誤", + "sidebarCollapse": "摺疊", + "sidebarExpand": "展開", + "productUpdateMoreInfo": "還有 {noOfUpdates} 項更新", + "productUpdateInfo": "{noOfUpdates} 項更新", + "productUpdateWhatsNew": "新功能", + "productUpdateTitle": "產品更新", + "productUpdateEmpty": "沒有更新", + "dismissAll": "全部關閉", + "pangolinUpdateAvailable": "有可用更新", + "pangolinUpdateAvailableInfo": "版本 {version} 已準備好安裝", + "pangolinUpdateAvailableReleaseNotes": "查看發行說明", + "newtUpdateAvailable": "更新可用", + "newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。", + "domainPickerEnterDomain": "域名", + "domainPickerPlaceholder": "example.com", + "domainPickerDescription": "輸入資源的完整域名以查看可用選項。", + "domainPickerDescriptionSaas": "輸入完整域名、子域或名稱以查看可用選項。", + "domainPickerTabAll": "所有", + "domainPickerTabOrganization": "組織", + "domainPickerTabProvided": "提供的", + "domainPickerSortAsc": "A-Z", + "domainPickerSortDesc": "Z-A", + "domainPickerCheckingAvailability": "檢查可用性...", + "domainPickerNoMatchingDomains": "未找到匹配的域名。嘗試不同的域名或檢查您組織的域名設置。", + "domainPickerOrganizationDomains": "組織域", + "domainPickerProvidedDomains": "提供的域", + "domainPickerSubdomain": "子域:{subdomain}", + "domainPickerNamespace": "命名空間:{namespace}", + "domainPickerShowMore": "顯示更多", + "regionSelectorTitle": "選擇區域", + "regionSelectorInfo": "選擇區域以幫助提升您所在地的性能。您不必與伺服器在相同的區域。", + "regionSelectorPlaceholder": "選擇一個區域", + "regionSelectorComingSoon": "即將推出", + "billingLoadingSubscription": "正在載入訂閱...", + "billingFreeTier": "免費層", + "billingWarningOverLimit": "警告:您已超出一個或多個使用限制。在您修改訂閱或調整使用情況之前,您的站點將無法連接。", + "billingUsageLimitsOverview": "使用限制概覽", + "billingMonitorUsage": "監控您的使用情況以對比已配置的限制。如需提高限制請聯絡我們 support@pangolin.net。", + "billingDataUsage": "數據使用情況", + "billingOnlineTime": "站點在線時間", + "billingUsers": "活躍用戶", + "billingDomains": "活躍域", + "billingRemoteExitNodes": "活躍自託管節點", + "billingNoLimitConfigured": "未配置限制", + "billingEstimatedPeriod": "估計結算週期", + "billingIncludedUsage": "包含的使用量", + "billingIncludedUsageDescription": "您當前訂閱計劃中包含的使用量", + "billingFreeTierIncludedUsage": "免費層使用額度", + "billingIncluded": "包含", + "billingEstimatedTotal": "預計總額:", + "billingNotes": "備註", + "billingEstimateNote": "這是根據您當前使用情況的估算。", + "billingActualChargesMayVary": "實際費用可能會有變化。", + "billingBilledAtEnd": "您將在結算週期結束時被計費。", + "billingModifySubscription": "修改訂閱", + "billingStartSubscription": "開始訂閱", + "billingRecurringCharge": "週期性收費", + "billingManageSubscriptionSettings": "管理您的訂閱設置和偏好", + "billingNoActiveSubscription": "您沒有活躍的訂閱。開始訂閱以增加使用限制。", + "billingFailedToLoadSubscription": "無法載入訂閱", + "billingFailedToLoadUsage": "無法載入使用情況", + "billingFailedToGetCheckoutUrl": "無法獲取結帳網址", + "billingPleaseTryAgainLater": "請稍後再試。", + "billingCheckoutError": "結帳錯誤", + "billingFailedToGetPortalUrl": "無法獲取門戶網址", + "billingPortalError": "門戶錯誤", + "billingDataUsageInfo": "當連接到雲端時,您將為透過安全隧道傳輸的所有數據收取費用。 這包括您所有站點的進出流量。 當您達到上限時,您的站點將斷開連接,直到您升級計劃或減少使用。使用節點時不收取數據。", + "billingOnlineTimeInfo": "您要根據您的網站連接到雲端的時間長短收取費用。 例如,44,640 分鐘等於一個 24/7 全月運行的網站。 當您達到上限時,您的站點將斷開連接,直到您升級計劃或減少使用。使用節點時不收取費用。", + "billingUsersInfo": "根據您組織中的活躍用戶數量收費。按日計算帳單。", + "billingDomainInfo": "根據組織中活躍域的數量收費。按日計算帳單。", + "billingRemoteExitNodesInfo": "根據您組織中已管理節點的數量收費。按日計算帳單。", + "domainNotFound": "域未找到", + "domainNotFoundDescription": "此資源已禁用,因為該域不再在我們的系統中存在。請為此資源設置一個新域。", + "failed": "失敗", + "createNewOrgDescription": "創建一個新組織", + "organization": "組織", + "port": "埠", + "securityKeyManage": "管理安全金鑰", + "securityKeyDescription": "添加或刪除用於無密碼認證的安全金鑰", + "securityKeyRegister": "註冊新的安全金鑰", + "securityKeyList": "您的安全金鑰", + "securityKeyNone": "尚未註冊安全金鑰", + "securityKeyNameRequired": "名稱為必填項", + "securityKeyRemove": "刪除", + "securityKeyLastUsed": "上次使用:{date}", + "securityKeyNameLabel": "名稱", + "securityKeyRegisterSuccess": "安全金鑰註冊成功", + "securityKeyRegisterError": "註冊安全金鑰失敗", + "securityKeyRemoveSuccess": "安全金鑰刪除成功", + "securityKeyRemoveError": "刪除安全金鑰失敗", + "securityKeyLoadError": "載入安全金鑰失敗", + "securityKeyLogin": "使用安全金鑰繼續", + "securityKeyAuthError": "使用安全金鑰認證失敗", + "securityKeyRecommendation": "考慮在其他設備上註冊另一個安全金鑰,以確保不會被鎖定在您的帳戶之外。", + "registering": "註冊中...", + "securityKeyPrompt": "請使用您的安全金鑰驗證身份。確保您的安全金鑰已連接並準備好。", + "securityKeyBrowserNotSupported": "您的瀏覽器不支持安全金鑰。請使用像 Chrome、Firefox 或 Safari 這樣的現代瀏覽器。", + "securityKeyPermissionDenied": "請允許訪問您的安全金鑰以繼續登錄。", + "securityKeyRemovedTooQuickly": "請保持您的安全金鑰連接,直到登錄過程完成。", + "securityKeyNotSupported": "您的安全金鑰可能不相容。請嘗試不同的安全金鑰。", + "securityKeyUnknownError": "使用安全金鑰時出現問題。請再試一次。", + "twoFactorRequired": "註冊安全金鑰需要兩步驗證。", + "twoFactor": "兩步驗證", + "twoFactorAuthentication": "兩步驗證", + "twoFactorDescription": "這個組織需要雙重身份驗證。", + "enableTwoFactor": "啟用兩步驗證", + "organizationSecurityPolicy": "組織安全政策", + "organizationSecurityPolicyDescription": "此機構擁有安全要求,您必須先滿足才能訪問", + "securityRequirements": "安全要求", + "allRequirementsMet": "已滿足所有要求", + "completeRequirementsToContinue": "完成下面的要求以繼續訪問此組織", + "youCanNowAccessOrganization": "您現在可以訪問此組織", + "reauthenticationRequired": "會話長度", + "reauthenticationDescription": "該機構要求您每 {maxDays} 天登錄一次。", + "reauthenticationDescriptionHours": "該機構要求您每 {maxHours} 小時登錄一次。", + "reauthenticateNow": "再次登錄", + "adminEnabled2FaOnYourAccount": "管理員已為 {email} 啟用兩步驗證。請完成設置以繼續。", + "securityKeyAdd": "添加安全金鑰", + "securityKeyRegisterTitle": "註冊新安全金鑰", + "securityKeyRegisterDescription": "連接您的安全金鑰並輸入名稱以便識別", + "securityKeyTwoFactorRequired": "要求兩步驗證", + "securityKeyTwoFactorDescription": "請輸入你的兩步驗證代碼以註冊安全金鑰", + "securityKeyTwoFactorRemoveDescription": "請輸入你的兩步驗證代碼以移除安全金鑰", + "securityKeyTwoFactorCode": "雙因素代碼", + "securityKeyRemoveTitle": "移除安全金鑰", + "securityKeyRemoveDescription": "輸入您的密碼以移除安全金鑰 \"{name}\"", + "securityKeyNoKeysRegistered": "沒有註冊安全金鑰", + "securityKeyNoKeysDescription": "添加安全金鑰以加強您的帳戶安全", + "createDomainRequired": "必須輸入域", + "createDomainAddDnsRecords": "添加 DNS 記錄", + "createDomainAddDnsRecordsDescription": "將以下 DNS 記錄添加到您的域名提供商以完成設置。", + "createDomainNsRecords": "NS 記錄", + "createDomainRecord": "記錄", + "createDomainType": "類型:", + "createDomainName": "名稱:", + "createDomainValue": "值:", + "createDomainCnameRecords": "CNAME 記錄", + "createDomainARecords": "A記錄", + "createDomainRecordNumber": "記錄 {number}", + "createDomainTxtRecords": "TXT 記錄", + "createDomainSaveTheseRecords": "保存這些記錄", + "createDomainSaveTheseRecordsDescription": "務必保存這些 DNS 記錄,因為您將無法再次查看它們。", + "createDomainDnsPropagation": "DNS 傳播", + "createDomainDnsPropagationDescription": "DNS 更改可能需要一些時間才能在網路上傳播。這可能需要從幾分鐘到 48 小時,具體取決於您的 DNS 提供商和 TTL 設置。", + "resourcePortRequired": "非 HTTP 資源必須輸入埠號", + "resourcePortNotAllowed": "HTTP 資源不應設置埠號", + "billingPricingCalculatorLink": "價格計算機", + "signUpTerms": { + "IAgreeToThe": "我同意", + "termsOfService": "服務條款", + "and": "和", + "privacyPolicy": "隱私政策" }, - "adopt": { - "title": "採納現有節點", - "description": "輸入您想要採用的現有節點的憑據", - "nodeIdLabel": "節點 ID", - "nodeIdDescription": "您想要採用的現有節點的 ID", - "secretLabel": "金鑰", - "secretDescription": "現有節點的秘密金鑰", - "submitButton": "採用節點" + "signUpMarketing": { + "keepMeInTheLoop": "透過電子郵件接收新聞、更新和新功能通知。" }, - "generate": { - "title": "生成的憑據", - "description": "使用這些生成的憑據來配置您的節點", - "nodeIdTitle": "節點 ID", - "secretTitle": "金鑰", - "saveCredentialsTitle": "將憑據添加到配置中", - "saveCredentialsDescription": "將這些憑據添加到您的自託管 Pangolin 節點設定檔中以完成連接。", - "submitButton": "創建節點" + "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 私鑰", + "clientCredentialsSave": "保存您的憑據", + "clientCredentialsSaveDescription": "該資訊僅會顯示一次,請確保將其複製到安全位置。", + "generalSettingsDescription": "配置此用戶端的常規設置", + "clientUpdated": "用戶端已更新", + "clientUpdatedDescription": "用戶端已更新。", + "clientUpdateFailed": "更新用戶端失敗", + "clientUpdateError": "更新用戶端時出錯。", + "sitesFetchFailed": "獲取站點失敗", + "sitesFetchError": "獲取站點時出錯。", + "olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。", + "olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。", + "enterCidrRange": "輸入 CIDR 範圍", + "resourceEnableProxy": "啟用公共代理", + "resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。", + "externalProxyEnabled": "外部代理已啟用", + "addNewTarget": "添加新目標", + "targetsList": "目標列表", + "advancedMode": "高級模式", + "advancedSettings": "進階設定", + "targetErrorDuplicateTargetFound": "找到重複的目標", + "healthCheckHealthy": "正常", + "healthCheckUnhealthy": "不正常", + "healthCheckUnknown": "未知", + "healthCheck": "健康檢查", + "configureHealthCheck": "配置健康檢查", + "configureHealthCheckDescription": "為 {target} 設置健康監控", + "enableHealthChecks": "啟用健康檢查", + "enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。", + "healthScheme": "方法", + "healthSelectScheme": "選擇方法", + "healthCheckPortInvalid": "健康檢查連接埠必須介於 1 到 65535 之間", + "healthCheckPath": "路徑", + "healthHostname": "IP / 主機", + "healthPort": "埠", + "healthCheckPathDescription": "用於檢查健康狀態的路徑。", + "healthyIntervalSeconds": "正常間隔", + "unhealthyIntervalSeconds": "不正常間隔", + "IntervalSeconds": "正常間隔", + "timeoutSeconds": "超時", + "timeIsInSeconds": "時間以秒為單位", + "retryAttempts": "重試次數", + "expectedResponseCodes": "期望響應代碼", + "expectedResponseCodesDescription": "HTTP 狀態碼表示健康狀態。如留空,200-300 被視為健康。", + "customHeaders": "自訂 Headers", + "customHeadersDescription": "Header 斷行分隔:Header 名稱:值", + "headersValidationError": "Header 必須是格式:Header 名稱:值。", + "saveHealthCheck": "保存健康檢查", + "healthCheckSaved": "健康檢查已保存", + "healthCheckSavedDescription": "健康檢查配置已成功保存。", + "healthCheckError": "健康檢查錯誤", + "healthCheckErrorDescription": "保存健康檢查配置時出錯", + "healthCheckPathRequired": "健康檢查路徑為必填項", + "healthCheckMethodRequired": "HTTP 方法為必填項", + "healthCheckIntervalMin": "檢查間隔必須至少為 5 秒", + "healthCheckTimeoutMin": "超時必須至少為 1 秒", + "healthCheckRetryMin": "重試次數必須至少為 1 次", + "httpMethod": "HTTP 方法", + "selectHttpMethod": "選擇 HTTP 方法", + "domainPickerSubdomainLabel": "子域名", + "domainPickerBaseDomainLabel": "根域名", + "domainPickerSearchDomains": "搜索域名...", + "domainPickerNoDomainsFound": "未找到域名", + "domainPickerLoadingDomains": "載入域名...", + "domainPickerSelectBaseDomain": "選擇根域名...", + "domainPickerNotAvailableForCname": "不適用於 CNAME 域", + "domainPickerEnterSubdomainOrLeaveBlank": "輸入子域名或留空以使用根域名。", + "domainPickerEnterSubdomainToSearch": "輸入一個子域名以搜索並從可用免費域名中選擇。", + "domainPickerFreeDomains": "免費域名", + "domainPickerSearchForAvailableDomains": "搜索可用域名", + "domainPickerNotWorkSelfHosted": "注意:自託管實例當前不提供免費的域名。", + "resourceDomain": "域名", + "resourceEditDomain": "編輯域名", + "siteName": "站點名稱", + "proxyPort": "埠", + "resourcesTableProxyResources": "代理資源", + "resourcesTableClientResources": "用戶端資源", + "resourcesTableNoProxyResourcesFound": "未找到代理資源。", + "resourcesTableNoInternalResourcesFound": "未找到內部資源。", + "resourcesTableDestination": "目標", + "resourcesTableAlias": "別名", + "resourcesTableClients": "用戶端", + "resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。", + "resourcesTableNoTargets": "無目標", + "resourcesTableHealthy": "健康", + "resourcesTableDegraded": "降級", + "resourcesTableOffline": "離線", + "resourcesTableUnknown": "未知", + "resourcesTableNotMonitored": "未監控", + "editInternalResourceDialogEditClientResource": "編輯用戶端資源", + "editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。", + "editInternalResourceDialogResourceProperties": "資源屬性", + "editInternalResourceDialogName": "名稱", + "editInternalResourceDialogProtocol": "協議", + "editInternalResourceDialogSitePort": "站點埠", + "editInternalResourceDialogTargetConfiguration": "目標配置", + "editInternalResourceDialogCancel": "取消", + "editInternalResourceDialogSaveResource": "保存資源", + "editInternalResourceDialogSuccess": "成功", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "內部資源更新成功", + "editInternalResourceDialogError": "錯誤", + "editInternalResourceDialogFailedToUpdateInternalResource": "更新內部資源失敗", + "editInternalResourceDialogNameRequired": "名稱為必填項", + "editInternalResourceDialogNameMaxLength": "名稱長度必須小於 255 個字元", + "editInternalResourceDialogProxyPortMin": "代理埠必須至少為 1", + "editInternalResourceDialogProxyPortMax": "代理埠必須小於 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", + "editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", + "editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", + "editInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠", + "editInternalResourceDialogMode": "模式", + "editInternalResourceDialogModePort": "連接埠", + "editInternalResourceDialogModeHost": "主機", + "editInternalResourceDialogModeCidr": "CIDR", + "editInternalResourceDialogDestination": "目的地", + "editInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。", + "editInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名稱位址。", + "editInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。", + "editInternalResourceDialogAlias": "別名", + "editInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。", + "createInternalResourceDialogNoSitesAvailable": "暫無可用站點", + "createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。", + "createInternalResourceDialogClose": "關閉", + "createInternalResourceDialogCreateClientResource": "創建用戶端資源", + "createInternalResourceDialogCreateClientResourceDescription": "創建一個新資源,該資源將可供連接到所選站點的用戶端訪問。", + "createInternalResourceDialogResourceProperties": "資源屬性", + "createInternalResourceDialogName": "名稱", + "createInternalResourceDialogSite": "站點", + "selectSite": "選擇站點...", + "noSitesFound": "找不到站點。", + "createInternalResourceDialogProtocol": "協議", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "站點埠", + "createInternalResourceDialogSitePortDescription": "使用此埠在連接到用戶端時訪問站點上的資源。", + "createInternalResourceDialogTargetConfiguration": "目標配置", + "createInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名地址。", + "createInternalResourceDialogDestinationPortDescription": "資源在目標 IP 上可訪問的埠。", + "createInternalResourceDialogCancel": "取消", + "createInternalResourceDialogCreateResource": "創建資源", + "createInternalResourceDialogSuccess": "成功", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "內部資源創建成功", + "createInternalResourceDialogError": "錯誤", + "createInternalResourceDialogFailedToCreateInternalResource": "創建內部資源失敗", + "createInternalResourceDialogNameRequired": "名稱為必填項", + "createInternalResourceDialogNameMaxLength": "名稱長度必須小於 255 個字元", + "createInternalResourceDialogPleaseSelectSite": "請選擇一個站點", + "createInternalResourceDialogProxyPortMin": "代理埠必須至少為 1", + "createInternalResourceDialogProxyPortMax": "代理埠必須小於 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", + "createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", + "createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", + "createInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠", + "createInternalResourceDialogMode": "模式", + "createInternalResourceDialogModePort": "連接埠", + "createInternalResourceDialogModeHost": "主機", + "createInternalResourceDialogModeCidr": "CIDR", + "createInternalResourceDialogDestination": "目的地", + "createInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。", + "createInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。", + "createInternalResourceDialogAlias": "別名", + "createInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。", + "siteConfiguration": "配置", + "siteAcceptClientConnections": "接受用戶端連接", + "siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。", + "siteAddress": "站點地址", + "siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。", + "siteNameDescription": "站點的顯示名稱,可以稍後更改。", + "autoLoginExternalIdp": "自動使用外部 IDP 登錄", + "autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。", + "selectIdp": "選擇 IDP", + "selectIdpPlaceholder": "選擇一個 IDP...", + "selectIdpRequired": "在啟用自動登錄時,請選擇一個 IDP。", + "autoLoginTitle": "重定向中", + "autoLoginDescription": "正在將您重定向到外部身份提供商進行身份驗證。", + "autoLoginProcessing": "準備身份驗證...", + "autoLoginRedirecting": "重定向到登錄...", + "autoLoginError": "自動登錄錯誤", + "autoLoginErrorNoRedirectUrl": "未從身份提供商收到重定向 URL。", + "autoLoginErrorGeneratingUrl": "生成身份驗證 URL 失敗。", + "remoteExitNodeManageRemoteExitNodes": "遠程節點", + "remoteExitNodeDescription": "自我主機一個或多個遠程節點來擴展您的網路連接並減少對雲的依賴性", + "remoteExitNodes": "節點", + "searchRemoteExitNodes": "搜索節點...", + "remoteExitNodeAdd": "添加節點", + "remoteExitNodeErrorDelete": "刪除節點時出錯", + "remoteExitNodeQuestionRemove": "您確定要從組織中刪除該節點嗎?", + "remoteExitNodeMessageRemove": "一旦刪除,該節點將不再能夠訪問。", + "remoteExitNodeConfirmDelete": "確認刪除節點", + "remoteExitNodeDelete": "刪除節點", + "sidebarRemoteExitNodes": "遠程節點", + "remoteExitNodeId": "ID", + "remoteExitNodeSecretKey": "密鑰", + "remoteExitNodeCreate": { + "title": "創建節點", + "description": "創建一個新節點來擴展您的網路連接", + "viewAllButton": "查看所有節點", + "strategy": { + "title": "創建策略", + "description": "選擇此選項以手動配置您的節點或生成新憑據。", + "adopt": { + "title": "採納節點", + "description": "如果您已經擁有該節點的憑據,請選擇此項。" + }, + "generate": { + "title": "生成金鑰", + "description": "如果您想為節點生成新金鑰,請選擇此選項" + } + }, + "adopt": { + "title": "採納現有節點", + "description": "輸入您想要採用的現有節點的憑據", + "nodeIdLabel": "節點 ID", + "nodeIdDescription": "您想要採用的現有節點的 ID", + "secretLabel": "金鑰", + "secretDescription": "現有節點的秘密金鑰", + "submitButton": "採用節點" + }, + "generate": { + "title": "生成的憑據", + "description": "使用這些生成的憑據來配置您的節點", + "nodeIdTitle": "節點 ID", + "secretTitle": "金鑰", + "saveCredentialsTitle": "將憑據添加到配置中", + "saveCredentialsDescription": "將這些憑據添加到您的自託管 Pangolin 節點設定檔中以完成連接。", + "submitButton": "創建節點" + }, + "validation": { + "adoptRequired": "在通過現有節點時需要節點ID和金鑰" + }, + "errors": { + "loadDefaultsFailed": "無法載入預設值", + "defaultsNotLoaded": "預設值未載入", + "createFailed": "創建節點失敗" + }, + "success": { + "created": "節點創建成功" + } }, - "validation": { - "adoptRequired": "在通過現有節點時需要節點ID和金鑰" + "remoteExitNodeSelection": "節點選擇", + "remoteExitNodeSelectionDescription": "為此本地站點選擇要路由流量的節點", + "remoteExitNodeRequired": "必須為本地站點選擇節點", + "noRemoteExitNodesAvailable": "無可用節點", + "noRemoteExitNodesAvailableDescription": "此組織沒有可用的節點。首先創建一個節點來使用本地站點。", + "exitNode": "出口節點", + "country": "國家", + "rulesMatchCountry": "當前基於源 IP", + "managedSelfHosted": { + "title": "託管自託管", + "description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能", + "introTitle": "託管式自架 Pangolin", + "introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。", + "introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 — — 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:", + "benefitSimplerOperations": { + "title": "簡單的操作", + "description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。" + }, + "benefitAutomaticUpdates": { + "title": "自動更新", + "description": "雲儀錶板快速演化,所以您可以獲得新的功能和錯誤修復,而不必每次手動拉取新的容器。" + }, + "benefitLessMaintenance": { + "title": "減少維護時間", + "description": "沒有要管理的資料庫遷移、備份或額外的基礎設施。我們在雲端處理這個問題。" + }, + "benefitCloudFailover": { + "title": "雲端故障轉移", + "description": "如果您的節點發生故障,您的隧道可以暫時故障轉移到我們的雲端存取點,直到您將節點恢復線上狀態。" + }, + "benefitHighAvailability": { + "title": "高可用率(PoPs)", + "description": "您還可以將多個節點添加到您的帳戶中以獲取冗餘和更好的性能。" + }, + "benefitFutureEnhancements": { + "title": "將來的改進", + "description": "我們正在計劃添加更多的分析、警報和管理工具,使你的部署更加有力。" + }, + "docsAlert": { + "text": "在我們中更多地了解管理下的自託管選項", + "documentation": "文件" + }, + "convertButton": "將此節點轉換為管理自託管的" }, - "errors": { - "loadDefaultsFailed": "無法載入預設值", - "defaultsNotLoaded": "預設值未載入", - "createFailed": "創建節點失敗" + "internationaldomaindetected": "檢測到國際域", + "willbestoredas": "儲存為:", + "roleMappingDescription": "確定當用戶啟用自動配送時如何分配他們的角色。", + "selectRole": "選擇角色", + "roleMappingExpression": "表達式", + "selectRolePlaceholder": "選擇角色", + "selectRoleDescription": "選擇一個角色,從此身份提供商分配給所有用戶", + "roleMappingExpressionDescription": "輸入一個 JMESPath 表達式來從 ID 令牌提取角色資訊", + "idpTenantIdRequired": "租戶 ID 是必需的", + "invalidValue": "無效的值", + "idpTypeLabel": "身份提供者類型", + "roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'", + "idpGoogleConfiguration": "Google 配置", + "idpGoogleConfigurationDescription": "配置您的 Google OAuth2 憑據", + "idpGoogleClientIdDescription": "您的 Google OAuth2 用戶端 ID", + "idpGoogleClientSecretDescription": "您的 Google OAuth2 用戶端金鑰", + "idpAzureConfiguration": "Azure Entra ID 配置", + "idpAzureConfigurationDescription": "配置您的 Azure Entra ID OAuth2 憑據", + "idpTenantId": "租戶 ID", + "idpTenantIdPlaceholder": "您的租戶 ID", + "idpAzureTenantIdDescription": "您的 Azure 租戶ID (在 Azure Active Directory 概覽中發現)", + "idpAzureClientIdDescription": "您的 Azure 應用程式註冊用戶端 ID", + "idpAzureClientSecretDescription": "您的 Azure 應用程式註冊用戶端金鑰", + "idpGoogleTitle": "Google", + "idpGoogleAlt": "Google", + "idpAzureTitle": "Azure Entra ID", + "idpAzureAlt": "Azure", + "idpGoogleConfigurationTitle": "Google 配置", + "idpAzureConfigurationTitle": "Azure Entra ID 配置", + "idpTenantIdLabel": "租戶 ID", + "idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID", + "idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰", + "idpGoogleDescription": "Google OAuth2/OIDC 提供商", + "idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供者", + "subnet": "子網", + "subnetDescription": "此組織網路配置的子網。", + "customDomain": "自訂網域", + "authPage": "認證頁面", + "authPageDescription": "配置您的組織認證頁面", + "authPageDomain": "認證頁面域", + "authPageBranding": "自訂品牌", + "authPageBrandingDescription": "設定此組織驗證頁面上顯示的品牌", + "authPageBrandingUpdated": "驗證頁面品牌更新成功", + "authPageBrandingRemoved": "驗證頁面品牌移除成功", + "authPageBrandingRemoveTitle": "移除驗證頁面品牌", + "authPageBrandingQuestionRemove": "您確定要移除驗證頁面的品牌嗎?", + "authPageBrandingDeleteConfirm": "確認刪除品牌", + "brandingLogoURL": "Logo 網址", + "brandingPrimaryColor": "主要顏色", + "brandingLogoWidth": "寬度 (px)", + "brandingLogoHeight": "高度 (px)", + "brandingOrgTitle": "組織驗證頁面標題", + "brandingOrgDescription": "{orgName} 將被替換為組織名稱", + "brandingOrgSubtitle": "組織驗證頁面副標題", + "brandingResourceTitle": "資源驗證頁面標題", + "brandingResourceSubtitle": "資源驗證頁面副標題", + "brandingResourceDescription": "{resourceName} 將被替換為組織名稱", + "saveAuthPageDomain": "儲存網域", + "saveAuthPageBranding": "儲存品牌", + "removeAuthPageBranding": "移除品牌", + "noDomainSet": "沒有域設置", + "changeDomain": "更改域", + "selectDomain": "選擇域", + "restartCertificate": "重新啟動證書", + "editAuthPageDomain": "編輯認證頁面域", + "setAuthPageDomain": "設置認證頁面域", + "failedToFetchCertificate": "獲取證書失敗", + "failedToRestartCertificate": "重新啟動證書失敗", + "addDomainToEnableCustomAuthPages": "為您的組織添加域名以啟用自訂認證頁面", + "selectDomainForOrgAuthPage": "選擇組織認證頁面的域", + "domainPickerProvidedDomain": "提供的域", + "domainPickerFreeProvidedDomain": "免費提供的域", + "domainPickerVerified": "已驗證", + "domainPickerUnverified": "未驗證", + "domainPickerInvalidSubdomainStructure": "此子域包含無效的字元或結構。當您保存時,它將被自動清除。", + "domainPickerError": "錯誤", + "domainPickerErrorLoadDomains": "載入組織域名失敗", + "domainPickerErrorCheckAvailability": "檢查域可用性失敗", + "domainPickerInvalidSubdomain": "無效的子域", + "domainPickerInvalidSubdomainRemoved": "輸入 \"{sub}\" 已被移除,因為其無效。", + "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 無法為 {domain} 變為有效。", + "domainPickerSubdomainSanitized": "子域已淨化", + "domainPickerSubdomainCorrected": "\"{sub}\" 已被更正為 \"{sanitized}\"", + "orgAuthSignInTitle": "登錄到您的組織", + "orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續", + "orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。", + "orgAuthSignInWithPangolin": "使用 Pangolin 登錄", + "orgAuthSignInToOrg": "登入組織", + "orgAuthSelectOrgTitle": "組織登入", + "orgAuthSelectOrgDescription": "輸入您的組織 ID 以繼續", + "orgAuthOrgIdPlaceholder": "your-organization", + "orgAuthOrgIdHelp": "輸入您組織的唯一識別碼", + "orgAuthSelectOrgHelp": "輸入組織 ID 後,您將被導向到組織的登入頁面,在那裡您可以使用 SSO 或組織憑證。", + "orgAuthRememberOrgId": "記住此組織 ID", + "orgAuthBackToSignIn": "返回標準登入", + "orgAuthNoAccount": "沒有帳戶?", + "subscriptionRequiredToUse": "需要訂閱才能使用此功能。", + "idpDisabled": "身份提供者已禁用。", + "orgAuthPageDisabled": "組織認證頁面已禁用。", + "domainRestartedDescription": "域驗證重新啟動成功", + "resourceAddEntrypointsEditFile": "編輯文件:config/traefik/traefik_config.yml", + "resourceExposePortsEditFile": "編輯文件:docker-compose.yml", + "emailVerificationRequired": "需要電子郵件驗證。 請通過 {dashboardUrl}/auth/login 再次登錄以完成此步驟。 然後,回到這裡。", + "twoFactorSetupRequired": "需要設置雙因素身份驗證。 請通過 {dashboardUrl}/auth/login 再次登錄以完成此步驟。 然後,回到這裡。", + "additionalSecurityRequired": "需要額外的安全", + "organizationRequiresAdditionalSteps": "這個組織需要額外的安全步驟才能訪問資源。", + "completeTheseSteps": "完成這些步驟", + "enableTwoFactorAuthentication": "啟用兩步驗證", + "completeSecuritySteps": "完成安全步驟", + "securitySettings": "安全設定", + "dangerSection": "危險區域", + "dangerSectionDescription": "永久刪除與此組織相關的所有資料", + "securitySettingsDescription": "配置您組織的安全策略", + "requireTwoFactorForAllUsers": "所有用戶需要兩步驗證", + "requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。", + "requireTwoFactorDisabledDescription": "此功能需要有效的許可證(企業)或活動訂閱(SaS)", + "requireTwoFactorCannotEnableDescription": "您必須為您的帳戶啟用雙重身份驗證才能對所有用戶", + "maxSessionLength": "最大會話長度", + "maxSessionLengthDescription": "設置用戶會話的最長時間。此後用戶需要重新驗證。", + "maxSessionLengthDisabledDescription": "此功能需要有效的許可證(企業)或活動訂閱(SaS)", + "selectSessionLength": "選擇會話長度", + "unenforced": "未執行", + "1Hour": "1 小時", + "3Hours": "3 小時", + "6Hours": "6 小時", + "12Hours": "12 小時", + "1DaySession": "1天", + "3Days": "3 天", + "7Days": "7 天", + "14Days": "14 天", + "30DaysSession": "30 天", + "90DaysSession": "90 天", + "180DaysSession": "180天", + "passwordExpiryDays": "密碼過期", + "editPasswordExpiryDescription": "設置用戶需要更改密碼之前的天數。", + "selectPasswordExpiry": "選擇密碼過期", + "30Days": "30 天", + "1Day": "1天", + "60Days": "60天", + "90Days": "90 天", + "180Days": "180天", + "1Year": "1 年", + "subscriptionBadge": "需要訂閱", + "securityPolicyChangeWarning": "安全政策更改警告", + "securityPolicyChangeDescription": "您即將更改安全政策設置。保存後,您可能需要重新認證以遵守這些政策更新。 所有不符合要求的用戶也需要重新認證。", + "securityPolicyChangeConfirmMessage": "我確認", + "securityPolicyChangeWarningText": "這將影響組織中的所有用戶", + "authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯", + "authPageErrorUpdate": "無法更新認證頁面", + "authPageDomainUpdated": "驗證頁面網域更新成功", + "healthCheckNotAvailable": "本地的", + "rewritePath": "重寫路徑", + "rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。", + "continueToApplication": "繼續應用", + "checkingInvite": "正在檢查邀請", + "setResourceHeaderAuth": "設置 ResourceHeaderAuth", + "resourceHeaderAuthRemove": "移除 Header 身份驗證", + "resourceHeaderAuthRemoveDescription": "已成功刪除 Header 身份驗證。", + "resourceErrorHeaderAuthRemove": "刪除 Header 身份驗證失敗", + "resourceErrorHeaderAuthRemoveDescription": "無法刪除資源的 Header 身份驗證。", + "resourceHeaderAuthProtectionEnabled": "Header 認證已啟用", + "resourceHeaderAuthProtectionDisabled": "Header 身份驗證已禁用", + "headerAuthRemove": "刪除 Header 認證", + "headerAuthAdd": "添加頁首認證", + "resourceErrorHeaderAuthSetup": "設置頁首認證失敗", + "resourceErrorHeaderAuthSetupDescription": "無法設置資源的 Header 身份驗證。", + "resourceHeaderAuthSetup": "Header 認證設置成功", + "resourceHeaderAuthSetupDescription": "Header 認證已成功設置。", + "resourceHeaderAuthSetupTitle": "設置 Header 身份驗證", + "resourceHeaderAuthSetupTitleDescription": "使用 HTTP 頭身份驗證來設置基本身份驗證資訊(使用者名稱和密碼)。使用 https://username:password@resource.example.com 訪問它", + "resourceHeaderAuthSubmit": "設置 Header 身份驗證", + "actionSetResourceHeaderAuth": "設置 Header 身份驗證", + "enterpriseEdition": "企業版", + "unlicensed": "未授權", + "beta": "測試版", + "manageUserDevices": "使用者裝置", + "manageUserDevicesDescription": "查看和管理使用者用於私密連接資源的裝置", + "downloadClientBannerTitle": "下載 Pangolin 客戶端", + "downloadClientBannerDescription": "下載適用於您系統的 Pangolin 客戶端,以連接到 Pangolin 網路並私密存取資源。", + "manageMachineClients": "管理機器客戶端", + "manageMachineClientsDescription": "建立和管理伺服器和系統用於私密連接資源的客戶端", + "machineClientsBannerTitle": "伺服器與自動化系統", + "machineClientsBannerDescription": "機器客戶端適用於與特定使用者無關的伺服器和自動化系統。它們使用 ID 和密鑰進行驗證,可以透過 Pangolin CLI、Olm CLI 或 Olm 容器執行。", + "machineClientsBannerPangolinCLI": "Pangolin CLI", + "machineClientsBannerOlmCLI": "Olm CLI", + "machineClientsBannerOlmContainer": "Olm 容器", + "clientsTableUserClients": "使用者", + "clientsTableMachineClients": "機器", + "licenseTableValidUntil": "有效期至", + "saasLicenseKeysSettingsTitle": "企業許可證", + "saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰", + "sidebarEnterpriseLicenses": "許可協議", + "generateLicenseKey": "生成許可證金鑰", + "generateLicenseKeyForm": { + "validation": { + "emailRequired": "請輸入一個有效的電子郵件地址", + "useCaseTypeRequired": "請選擇一個使用的案例類型", + "firstNameRequired": "必填名", + "lastNameRequired": "姓氏是必填項", + "primaryUseRequired": "請描述您的主要使用", + "jobTitleRequiredBusiness": "企業使用必須有職位頭銜。", + "industryRequiredBusiness": "商業使用需要工業", + "stateProvinceRegionRequired": "州/省/地區是必填項", + "postalZipCodeRequired": "郵政編碼是必需的", + "companyNameRequiredBusiness": "企業使用需要公司名稱", + "countryOfResidenceRequiredBusiness": "商業使用必須是居住國", + "countryRequiredPersonal": "國家需要個人使用", + "agreeToTermsRequired": "您必須同意條款", + "complianceConfirmationRequired": "您必須確認遵守 Fossorial Commercial License" + }, + "useCaseOptions": { + "personal": { + "title": "個人使用", + "description": "個人非商業用途,如學習、個人項目或實驗。" + }, + "business": { + "title": "商業使用", + "description": "供組織、公司或商業或創收活動使用。" + } + }, + "steps": { + "emailLicenseType": { + "title": "電子郵件和許可證類型", + "description": "輸入您的電子郵件並選擇您的許可證類型" + }, + "personalInformation": { + "title": "個人資訊", + "description": "告訴我們自己的資訊" + }, + "contactInformation": { + "title": "聯繫資訊", + "description": "您的聯繫資訊" + }, + "termsGenerate": { + "title": "條款並生成", + "description": "審閱並接受條款生成您的許可證" + } + }, + "alerts": { + "commercialUseDisclosure": { + "title": "使用情況披露", + "description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境——需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。" + }, + "trialPeriodInformation": { + "title": "試用期資訊", + "description": "此許可證金鑰使企業特性能夠持續 7 天的評價。 在評估期過後繼續訪問付費功能需要在有效的個人或企業許可證下啟用。對於企業許可證,請聯絡 Sales@pangolin.net。" + } + }, + "form": { + "useCaseQuestion": "您是否正在使用 Pangolin 進行個人或商業使用?", + "firstName": "名字", + "lastName": "名字", + "jobTitle": "工作頭銜:", + "primaryUseQuestion": "您主要計劃使用 Pangolin 嗎?", + "industryQuestion": "您的行業是什麼?", + "prospectiveUsersQuestion": "您期望有多少預期用戶?", + "prospectiveSitesQuestion": "您期望有多少站點(隧道)?", + "companyName": "公司名稱", + "countryOfResidence": "居住國", + "stateProvinceRegion": "州/省/地區", + "postalZipCode": "郵政編碼", + "companyWebsite": "公司網站", + "companyPhoneNumber": "公司電話號碼", + "country": "國家", + "phoneNumberOptional": "電話號碼 (可選)", + "complianceConfirmation": "我確認我提供的資料是準確的,我遵守了寄養商業許可證。 報告不準確的資訊或錯誤的產品使用是違反許可證的行為,可能導致您的金鑰被撤銷。" + }, + "buttons": { + "close": "關閉", + "previous": "上一個", + "next": "下一個", + "generateLicenseKey": "生成許可證金鑰" + }, + "toasts": { + "success": { + "title": "許可證金鑰生成成功", + "description": "您的許可證金鑰已經生成並準備使用。" + }, + "error": { + "title": "生成許可證金鑰失敗", + "description": "生成許可證金鑰時出錯。" + } + } }, - "success": { - "created": "節點創建成功" - } - }, - "remoteExitNodeSelection": "節點選擇", - "remoteExitNodeSelectionDescription": "為此本地站點選擇要路由流量的節點", - "remoteExitNodeRequired": "必須為本地站點選擇節點", - "noRemoteExitNodesAvailable": "無可用節點", - "noRemoteExitNodesAvailableDescription": "此組織沒有可用的節點。首先創建一個節點來使用本地站點。", - "exitNode": "出口節點", - "country": "國家", - "rulesMatchCountry": "當前基於源 IP", - "managedSelfHosted": { - "title": "託管自託管", - "description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能", - "introTitle": "託管式自架 Pangolin", - "introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。", - "introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 — — 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:", - "benefitSimplerOperations": { - "title": "簡單的操作", - "description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。" - }, - "benefitAutomaticUpdates": { - "title": "自動更新", - "description": "雲儀錶板快速演化,所以您可以獲得新的功能和錯誤修復,而不必每次手動拉取新的容器。" - }, - "benefitLessMaintenance": { - "title": "減少維護時間", - "description": "沒有要管理的資料庫遷移、備份或額外的基礎設施。我們在雲端處理這個問題。" - }, - "benefitCloudFailover": { - "title": "雲端故障轉移", - "description": "如果您的節點發生故障,您的隧道可以暫時故障轉移到我們的雲端存取點,直到您將節點恢復線上狀態。" - }, - "benefitHighAvailability": { - "title": "高可用率(PoPs)", - "description": "您還可以將多個節點添加到您的帳戶中以獲取冗餘和更好的性能。" - }, - "benefitFutureEnhancements": { - "title": "將來的改進", - "description": "我們正在計劃添加更多的分析、警報和管理工具,使你的部署更加有力。" - }, - "docsAlert": { - "text": "在我們中更多地了解管理下的自託管選項", - "documentation": "文件" - }, - "convertButton": "將此節點轉換為管理自託管的" - }, - "internationaldomaindetected": "檢測到國際域", - "willbestoredas": "儲存為:", - "roleMappingDescription": "確定當用戶啟用自動配送時如何分配他們的角色。", - "selectRole": "選擇角色", - "roleMappingExpression": "表達式", - "selectRolePlaceholder": "選擇角色", - "selectRoleDescription": "選擇一個角色,從此身份提供商分配給所有用戶", - "roleMappingExpressionDescription": "輸入一個 JMESPath 表達式來從 ID 令牌提取角色資訊", - "idpTenantIdRequired": "租戶 ID 是必需的", - "invalidValue": "無效的值", - "idpTypeLabel": "身份提供者類型", - "roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'", - "idpGoogleConfiguration": "Google 配置", - "idpGoogleConfigurationDescription": "配置您的 Google OAuth2 憑據", - "idpGoogleClientIdDescription": "您的 Google OAuth2 用戶端 ID", - "idpGoogleClientSecretDescription": "您的 Google OAuth2 用戶端金鑰", - "idpAzureConfiguration": "Azure Entra ID 配置", - "idpAzureConfigurationDescription": "配置您的 Azure Entra ID OAuth2 憑據", - "idpTenantId": "租戶 ID", - "idpTenantIdPlaceholder": "您的租戶 ID", - "idpAzureTenantIdDescription": "您的 Azure 租戶ID (在 Azure Active Directory 概覽中發現)", - "idpAzureClientIdDescription": "您的 Azure 應用程式註冊用戶端 ID", - "idpAzureClientSecretDescription": "您的 Azure 應用程式註冊用戶端金鑰", - "idpGoogleTitle": "Google", - "idpGoogleAlt": "Google", - "idpAzureTitle": "Azure Entra ID", - "idpAzureAlt": "Azure", - "idpGoogleConfigurationTitle": "Google 配置", - "idpAzureConfigurationTitle": "Azure Entra ID 配置", - "idpTenantIdLabel": "租戶 ID", - "idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID", - "idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰", - "idpGoogleDescription": "Google OAuth2/OIDC 提供商", - "idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供者", - "subnet": "子網", - "subnetDescription": "此組織網路配置的子網。", - "customDomain": "自訂網域", - "authPage": "認證頁面", - "authPageDescription": "配置您的組織認證頁面", - "authPageDomain": "認證頁面域", - "authPageBranding": "自訂品牌", - "authPageBrandingDescription": "設定此組織驗證頁面上顯示的品牌", - "authPageBrandingUpdated": "驗證頁面品牌更新成功", - "authPageBrandingRemoved": "驗證頁面品牌移除成功", - "authPageBrandingRemoveTitle": "移除驗證頁面品牌", - "authPageBrandingQuestionRemove": "您確定要移除驗證頁面的品牌嗎?", - "authPageBrandingDeleteConfirm": "確認刪除品牌", - "brandingLogoURL": "Logo 網址", - "brandingPrimaryColor": "主要顏色", - "brandingLogoWidth": "寬度 (px)", - "brandingLogoHeight": "高度 (px)", - "brandingOrgTitle": "組織驗證頁面標題", - "brandingOrgDescription": "{orgName} 將被替換為組織名稱", - "brandingOrgSubtitle": "組織驗證頁面副標題", - "brandingResourceTitle": "資源驗證頁面標題", - "brandingResourceSubtitle": "資源驗證頁面副標題", - "brandingResourceDescription": "{resourceName} 將被替換為組織名稱", - "saveAuthPageDomain": "儲存網域", - "saveAuthPageBranding": "儲存品牌", - "removeAuthPageBranding": "移除品牌", - "noDomainSet": "沒有域設置", - "changeDomain": "更改域", - "selectDomain": "選擇域", - "restartCertificate": "重新啟動證書", - "editAuthPageDomain": "編輯認證頁面域", - "setAuthPageDomain": "設置認證頁面域", - "failedToFetchCertificate": "獲取證書失敗", - "failedToRestartCertificate": "重新啟動證書失敗", - "addDomainToEnableCustomAuthPages": "為您的組織添加域名以啟用自訂認證頁面", - "selectDomainForOrgAuthPage": "選擇組織認證頁面的域", - "domainPickerProvidedDomain": "提供的域", - "domainPickerFreeProvidedDomain": "免費提供的域", - "domainPickerVerified": "已驗證", - "domainPickerUnverified": "未驗證", - "domainPickerInvalidSubdomainStructure": "此子域包含無效的字元或結構。當您保存時,它將被自動清除。", - "domainPickerError": "錯誤", - "domainPickerErrorLoadDomains": "載入組織域名失敗", - "domainPickerErrorCheckAvailability": "檢查域可用性失敗", - "domainPickerInvalidSubdomain": "無效的子域", - "domainPickerInvalidSubdomainRemoved": "輸入 \"{sub}\" 已被移除,因為其無效。", - "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 無法為 {domain} 變為有效。", - "domainPickerSubdomainSanitized": "子域已淨化", - "domainPickerSubdomainCorrected": "\"{sub}\" 已被更正為 \"{sanitized}\"", - "orgAuthSignInTitle": "登錄到您的組織", - "orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續", - "orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。", - "orgAuthSignInWithPangolin": "使用 Pangolin 登錄", - "orgAuthSignInToOrg": "登入組織", - "orgAuthSelectOrgTitle": "組織登入", - "orgAuthSelectOrgDescription": "輸入您的組織 ID 以繼續", - "orgAuthOrgIdPlaceholder": "your-organization", - "orgAuthOrgIdHelp": "輸入您組織的唯一識別碼", - "orgAuthSelectOrgHelp": "輸入組織 ID 後,您將被導向到組織的登入頁面,在那裡您可以使用 SSO 或組織憑證。", - "orgAuthRememberOrgId": "記住此組織 ID", - "orgAuthBackToSignIn": "返回標準登入", - "orgAuthNoAccount": "沒有帳戶?", - "subscriptionRequiredToUse": "需要訂閱才能使用此功能。", - "idpDisabled": "身份提供者已禁用。", - "orgAuthPageDisabled": "組織認證頁面已禁用。", - "domainRestartedDescription": "域驗證重新啟動成功", - "resourceAddEntrypointsEditFile": "編輯文件:config/traefik/traefik_config.yml", - "resourceExposePortsEditFile": "編輯文件:docker-compose.yml", - "emailVerificationRequired": "需要電子郵件驗證。 請通過 {dashboardUrl}/auth/login 再次登錄以完成此步驟。 然後,回到這裡。", - "twoFactorSetupRequired": "需要設置雙因素身份驗證。 請通過 {dashboardUrl}/auth/login 再次登錄以完成此步驟。 然後,回到這裡。", - "additionalSecurityRequired": "需要額外的安全", - "organizationRequiresAdditionalSteps": "這個組織需要額外的安全步驟才能訪問資源。", - "completeTheseSteps": "完成這些步驟", - "enableTwoFactorAuthentication": "啟用兩步驗證", - "completeSecuritySteps": "完成安全步驟", - "securitySettings": "安全設定", - "dangerSection": "危險區域", - "dangerSectionDescription": "永久刪除與此組織相關的所有資料", - "securitySettingsDescription": "配置您組織的安全策略", - "requireTwoFactorForAllUsers": "所有用戶需要兩步驗證", - "requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。", - "requireTwoFactorDisabledDescription": "此功能需要有效的許可證(企業)或活動訂閱(SaS)", - "requireTwoFactorCannotEnableDescription": "您必須為您的帳戶啟用雙重身份驗證才能對所有用戶", - "maxSessionLength": "最大會話長度", - "maxSessionLengthDescription": "設置用戶會話的最長時間。此後用戶需要重新驗證。", - "maxSessionLengthDisabledDescription": "此功能需要有效的許可證(企業)或活動訂閱(SaS)", - "selectSessionLength": "選擇會話長度", - "unenforced": "未執行", - "1Hour": "1 小時", - "3Hours": "3 小時", - "6Hours": "6 小時", - "12Hours": "12 小時", - "1DaySession": "1天", - "3Days": "3 天", - "7Days": "7 天", - "14Days": "14 天", - "30DaysSession": "30 天", - "90DaysSession": "90 天", - "180DaysSession": "180天", - "passwordExpiryDays": "密碼過期", - "editPasswordExpiryDescription": "設置用戶需要更改密碼之前的天數。", - "selectPasswordExpiry": "選擇密碼過期", - "30Days": "30 天", - "1Day": "1天", - "60Days": "60天", - "90Days": "90 天", - "180Days": "180天", - "1Year": "1 年", - "subscriptionBadge": "需要訂閱", - "securityPolicyChangeWarning": "安全政策更改警告", - "securityPolicyChangeDescription": "您即將更改安全政策設置。保存後,您可能需要重新認證以遵守這些政策更新。 所有不符合要求的用戶也需要重新認證。", - "securityPolicyChangeConfirmMessage": "我確認", - "securityPolicyChangeWarningText": "這將影響組織中的所有用戶", - "authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯", - "authPageErrorUpdate": "無法更新認證頁面", - "authPageDomainUpdated": "驗證頁面網域更新成功", - "healthCheckNotAvailable": "本地的", - "rewritePath": "重寫路徑", - "rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。", - "continueToApplication": "繼續應用", - "checkingInvite": "正在檢查邀請", - "setResourceHeaderAuth": "設置 ResourceHeaderAuth", - "resourceHeaderAuthRemove": "移除 Header 身份驗證", - "resourceHeaderAuthRemoveDescription": "已成功刪除 Header 身份驗證。", - "resourceErrorHeaderAuthRemove": "刪除 Header 身份驗證失敗", - "resourceErrorHeaderAuthRemoveDescription": "無法刪除資源的 Header 身份驗證。", - "resourceHeaderAuthProtectionEnabled": "Header 認證已啟用", - "resourceHeaderAuthProtectionDisabled": "Header 身份驗證已禁用", - "headerAuthRemove": "刪除 Header 認證", - "headerAuthAdd": "添加頁首認證", - "resourceErrorHeaderAuthSetup": "設置頁首認證失敗", - "resourceErrorHeaderAuthSetupDescription": "無法設置資源的 Header 身份驗證。", - "resourceHeaderAuthSetup": "Header 認證設置成功", - "resourceHeaderAuthSetupDescription": "Header 認證已成功設置。", - "resourceHeaderAuthSetupTitle": "設置 Header 身份驗證", - "resourceHeaderAuthSetupTitleDescription": "使用 HTTP 頭身份驗證來設置基本身份驗證資訊(使用者名稱和密碼)。使用 https://username:password@resource.example.com 訪問它", - "resourceHeaderAuthSubmit": "設置 Header 身份驗證", - "actionSetResourceHeaderAuth": "設置 Header 身份驗證", - "enterpriseEdition": "企業版", - "unlicensed": "未授權", - "beta": "測試版", - "manageUserDevices": "使用者裝置", - "manageUserDevicesDescription": "查看和管理使用者用於私密連接資源的裝置", - "downloadClientBannerTitle": "下載 Pangolin 客戶端", - "downloadClientBannerDescription": "下載適用於您系統的 Pangolin 客戶端,以連接到 Pangolin 網路並私密存取資源。", - "manageMachineClients": "管理機器客戶端", - "manageMachineClientsDescription": "建立和管理伺服器和系統用於私密連接資源的客戶端", - "machineClientsBannerTitle": "伺服器與自動化系統", - "machineClientsBannerDescription": "機器客戶端適用於與特定使用者無關的伺服器和自動化系統。它們使用 ID 和密鑰進行驗證,可以透過 Pangolin CLI、Olm CLI 或 Olm 容器執行。", - "machineClientsBannerPangolinCLI": "Pangolin CLI", - "machineClientsBannerOlmCLI": "Olm CLI", - "machineClientsBannerOlmContainer": "Olm 容器", - "clientsTableUserClients": "使用者", - "clientsTableMachineClients": "機器", - "licenseTableValidUntil": "有效期至", - "saasLicenseKeysSettingsTitle": "企業許可證", - "saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰", - "sidebarEnterpriseLicenses": "許可協議", - "generateLicenseKey": "生成許可證金鑰", - "generateLicenseKeyForm": { - "validation": { - "emailRequired": "請輸入一個有效的電子郵件地址", - "useCaseTypeRequired": "請選擇一個使用的案例類型", - "firstNameRequired": "必填名", - "lastNameRequired": "姓氏是必填項", - "primaryUseRequired": "請描述您的主要使用", - "jobTitleRequiredBusiness": "企業使用必須有職位頭銜。", - "industryRequiredBusiness": "商業使用需要工業", - "stateProvinceRegionRequired": "州/省/地區是必填項", - "postalZipCodeRequired": "郵政編碼是必需的", - "companyNameRequiredBusiness": "企業使用需要公司名稱", - "countryOfResidenceRequiredBusiness": "商業使用必須是居住國", - "countryRequiredPersonal": "國家需要個人使用", - "agreeToTermsRequired": "您必須同意條款", - "complianceConfirmationRequired": "您必須確認遵守 Fossorial Commercial License" - }, - "useCaseOptions": { - "personal": { - "title": "個人使用", - "description": "個人非商業用途,如學習、個人項目或實驗。" - }, - "business": { - "title": "商業使用", - "description": "供組織、公司或商業或創收活動使用。" - } - }, - "steps": { - "emailLicenseType": { - "title": "電子郵件和許可證類型", - "description": "輸入您的電子郵件並選擇您的許可證類型" - }, - "personalInformation": { - "title": "個人資訊", - "description": "告訴我們自己的資訊" - }, - "contactInformation": { - "title": "聯繫資訊", - "description": "您的聯繫資訊" - }, - "termsGenerate": { - "title": "條款並生成", - "description": "審閱並接受條款生成您的許可證" - } - }, - "alerts": { - "commercialUseDisclosure": { - "title": "使用情況披露", - "description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境——需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。" - }, - "trialPeriodInformation": { - "title": "試用期資訊", - "description": "此許可證金鑰使企業特性能夠持續 7 天的評價。 在評估期過後繼續訪問付費功能需要在有效的個人或企業許可證下啟用。對於企業許可證,請聯絡 Sales@pangolin.net。" - } - }, - "form": { - "useCaseQuestion": "您是否正在使用 Pangolin 進行個人或商業使用?", - "firstName": "名字", - "lastName": "名字", - "jobTitle": "工作頭銜:", - "primaryUseQuestion": "您主要計劃使用 Pangolin 嗎?", - "industryQuestion": "您的行業是什麼?", - "prospectiveUsersQuestion": "您期望有多少預期用戶?", - "prospectiveSitesQuestion": "您期望有多少站點(隧道)?", - "companyName": "公司名稱", - "countryOfResidence": "居住國", - "stateProvinceRegion": "州/省/地區", - "postalZipCode": "郵政編碼", - "companyWebsite": "公司網站", - "companyPhoneNumber": "公司電話號碼", - "country": "國家", - "phoneNumberOptional": "電話號碼 (可選)", - "complianceConfirmation": "我確認我提供的資料是準確的,我遵守了寄養商業許可證。 報告不準確的資訊或錯誤的產品使用是違反許可證的行為,可能導致您的金鑰被撤銷。" - }, - "buttons": { - "close": "關閉", - "previous": "上一個", - "next": "下一個", - "generateLicenseKey": "生成許可證金鑰" - }, - "toasts": { - "success": { - "title": "許可證金鑰生成成功", - "description": "您的許可證金鑰已經生成並準備使用。" - }, - "error": { - "title": "生成許可證金鑰失敗", - "description": "生成許可證金鑰時出錯。" - } - } - }, - "priority": "優先權", - "priorityDescription": "先評估更高優先度線路。優先度 = 100 意味著自動排序(系統決定). 使用另一個數字強制執行手動優先度。", - "instanceName": "實例名稱", - "pathMatchModalTitle": "配置路徑匹配", - "pathMatchModalDescription": "根據傳入請求的路徑設置匹配方式。", - "pathMatchType": "匹配類型", - "pathMatchPrefix": "前綴", - "pathMatchExact": "精準的", - "pathMatchRegex": "正則表達式", - "pathMatchValue": "路徑值", - "clear": "清空", - "saveChanges": "保存更改", - "pathMatchRegexPlaceholder": "^/api/.*", - "pathMatchDefaultPlaceholder": "/路徑", - "pathMatchPrefixHelp": "範例: /api 匹配/api, /api/users 等。", - "pathMatchExactHelp": "範例:/api 匹配僅限/api", - "pathMatchRegexHelp": "例如:^/api/.* 匹配/api/why", - "pathRewriteModalTitle": "配置路徑重寫", - "pathRewriteModalDescription": "在轉發到目標之前變換匹配的路徑。", - "pathRewriteType": "重寫類型", - "pathRewritePrefixOption": "前綴 - 替換前綴", - "pathRewriteExactOption": "精確-替換整個路徑", - "pathRewriteRegexOption": "正則表達式 - 替換模式", - "pathRewriteStripPrefixOption": "刪除前綴 - 刪除前綴", - "pathRewriteValue": "重寫值", - "pathRewriteRegexPlaceholder": "/new/$1", - "pathRewriteDefaultPlaceholder": "/new-path", - "pathRewritePrefixHelp": "用此值替換匹配的前綴", - "pathRewriteExactHelp": "當路徑匹配時用此值替換整個路徑", - "pathRewriteRegexHelp": "使用抓取組,如$1,$2來替換", - "pathRewriteStripPrefixHelp": "留空以脫離前綴或提供新的前綴", - "pathRewritePrefix": "前綴", - "pathRewriteExact": "精準的", - "pathRewriteRegex": "正則表達式", - "pathRewriteStrip": "帶狀圖", - "pathRewriteStripLabel": "條形圖", - "sidebarEnableEnterpriseLicense": "啟用企業許可證", - "cannotbeUndone": "無法撤消。", - "toConfirm": "確認", - "deleteClientQuestion": "您確定要從站點和組織中刪除客戶嗎?", - "clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。", - "sidebarLogs": "日誌", - "request": "請求", - "requests": "請求", - "logs": "日誌", - "logsSettingsDescription": "監視從此 orginization 中收集的日誌", - "searchLogs": "搜索日誌...", - "action": "行動", - "actor": "執行者", - "timestamp": "時間戳", - "accessLogs": "訪問日誌", - "exportCsv": "導出 CSV", - "exportError": "匯出 CSV 時發生未知錯誤", - "exportCsvTooltip": "在時間範圍內", - "actorId": "執行者 ID", - "allowedByRule": "根據規則允許", - "allowedNoAuth": "無認證", - "validAccessToken": "有效訪問令牌", - "validHeaderAuth": "有效的 Header 身份驗證", - "validPincode": "有效的 Pincode", - "validPassword": "有效密碼", - "validEmail": "有效的 email", - "validSSO": "有效的 SSO", - "resourceBlocked": "資源被阻止", - "droppedByRule": "被規則刪除", - "noSessions": "無會話", - "temporaryRequestToken": "臨時請求令牌", - "noMoreAuthMethods": "無有效授權", - "ip": "IP", - "reason": "原因", - "requestLogs": "請求日誌", - "requestAnalytics": "請求分析", - "host": "主機", - "location": "地點", - "actionLogs": "操作日誌", - "sidebarLogsRequest": "請求日誌", - "sidebarLogsAccess": "訪問日誌", - "sidebarLogsAction": "操作日誌", - "logRetention": "日誌保留", - "logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌", - "requestLogsDescription": "查看此機構資源的詳細請求日誌", - "requestAnalyticsDescription": "查看此組織資源的詳細請求分析", - "logRetentionRequestLabel": "請求日誌保留", - "logRetentionRequestDescription": "保留請求日誌的時間", - "logRetentionAccessLabel": "訪問日誌保留", - "logRetentionAccessDescription": "保留訪問日誌的時間", - "logRetentionActionLabel": "動作日誌保留", - "logRetentionActionDescription": "保留操作日誌的時間", - "logRetentionDisabled": "已禁用", - "logRetention3Days": "3 天", - "logRetention7Days": "7 天", - "logRetention14Days": "14 天", - "logRetention30Days": "30 天", - "logRetention90Days": "90 天", - "logRetentionForever": "永遠的", - "logRetentionEndOfFollowingYear": "次年年底", - "actionLogsDescription": "查看此機構執行的操作歷史", - "accessLogsDescription": "查看此機構資源的訪問認證請求", - "licenseRequiredToUse": "需要企業許可證才能使用此功能。", - "certResolver": "證書解決器", - "certResolverDescription": "選擇用於此資源的證書解析器。", - "selectCertResolver": "選擇證書解析", - "enterCustomResolver": "輸入自訂解析器", - "preferWildcardCert": "喜歡通配符證書", - "unverified": "未驗證", - "domainSetting": "域設置", - "domainSettingDescription": "配置您的域的設置", - "preferWildcardCertDescription": "嘗試生成通配符證書(需要正確配置的證書解析器)。", - "recordName": "記錄名稱", - "auto": "自動操作", - "TTL": "TTL", - "howToAddRecords": "如何添加記錄", - "dnsRecord": "DNS 記錄", - "required": "必填", - "domainSettingsUpdated": "域設置更新成功", - "orgOrDomainIdMissing": "缺少機構或域 ID", - "loadingDNSRecords": "正在載入 DNS 記錄...", - "olmUpdateAvailableInfo": "有最新版本的 Olm 可用。請更新到最新版本以獲取最佳體驗。", - "client": "用戶端:", - "proxyProtocol": "代理協議設置", - "proxyProtocolDescription": "配置代理協議以保留 TCP/UDP 服務的用戶端 IP 位址。", - "enableProxyProtocol": "啟用代理協議", - "proxyProtocolInfo": "為 TCP/UDP 後端保留用戶端 IP 位址", - "proxyProtocolVersion": "代理協議版本", - "version1": " 版本 1 (推薦)", - "version2": "版本 2", - "versionDescription": "版本 1 是基於文本和廣泛支持的版本。版本 2 是二進制和更有效率但不那麼相容。", - "warning": "警告", - "proxyProtocolWarning": "您的後端應用程式必須配置為接受代理協議連接。如果您的後端不支持代理協議,啟用這將會中斷所有連接。 請務必從 Traefik 配置您的後端到信任代理協議標題。", - "restarting": "正在重啟...", - "manual": "手動模式", - "messageSupport": "消息支持", - "supportNotAvailableTitle": "支持不可用", - "supportNotAvailableDescription": "支持現在不可用。您可以發送電子郵件到 support@pangolin.net。", - "supportRequestSentTitle": "支持請求已發送", - "supportRequestSentDescription": "您的消息已成功發送。", - "supportRequestFailedTitle": "發送請求失敗", - "supportRequestFailedDescription": "發送您的支持請求時出錯。", - "supportSubjectRequired": "主題是必填項", - "supportSubjectMaxLength": "主題必須是 255 個或更少的字元", - "supportMessageRequired": "消息是必填項", - "supportReplyTo": "回復給", - "supportSubject": "議題", - "supportSubjectPlaceholder": "輸入主題", - "supportMessage": "留言", - "supportMessagePlaceholder": "輸入您的消息", - "supportSending": "正在發送...", - "supportSend": "發送", - "supportMessageSent": "消息已發送!", - "supportWillContact": "我們很快就會聯繫起來!", - "selectLogRetention": "選擇保留日誌", - "terms": "條款", - "privacy": "隱私權", - "security": "安全性", - "docs": "文件", - "deviceActivation": "裝置啟用", - "deviceCodeInvalidFormat": "代碼必須為 9 個字元(例如:A1AJ-N5JD)", - "deviceCodeInvalidOrExpired": "代碼無效或已過期", - "deviceCodeVerifyFailed": "驗證裝置代碼失敗", - "signedInAs": "已登入為", - "deviceCodeEnterPrompt": "輸入裝置上顯示的代碼", - "continue": "繼續", - "deviceUnknownLocation": "未知位置", - "deviceAuthorizationRequested": "此授權請求來自 {location},時間為 {date}。請確保您信任此裝置,因為它將獲得帳戶存取權限。", - "deviceLabel": "裝置:{deviceName}", - "deviceWantsAccess": "想要存取您的帳戶", - "deviceExistingAccess": "現有存取權限:", - "deviceFullAccess": "完整帳戶存取權限", - "deviceOrganizationsAccess": "存取您帳戶有權限的所有組織", - "deviceAuthorize": "授權 {applicationName}", - "deviceConnected": "裝置已連接!", - "deviceAuthorizedMessage": "裝置已獲授權存取您的帳戶。請返回客戶端應用程式。", - "pangolinCloud": "Pangolin 雲端", - "viewDevices": "查看裝置", - "viewDevicesDescription": "管理您已連接的裝置", - "noDevices": "找不到裝置", - "dateCreated": "建立日期", - "unnamedDevice": "未命名裝置", - "deviceQuestionRemove": "您確定要刪除此裝置嗎?", - "deviceMessageRemove": "此操作無法復原。", - "deviceDeleteConfirm": "刪除裝置", - "deleteDevice": "刪除裝置", - "errorLoadingDevices": "載入裝置時發生錯誤", - "failedToLoadDevices": "載入裝置失敗", - "deviceDeleted": "裝置已刪除", - "deviceDeletedDescription": "裝置已成功刪除。", - "errorDeletingDevice": "刪除裝置時發生錯誤", - "failedToDeleteDevice": "刪除裝置失敗", - "showColumns": "顯示列", - "hideColumns": "隱藏列", - "columnVisibility": "列可見性", - "toggleColumn": "切換 {columnName} 列", - "allColumns": "全部列", - "defaultColumns": "默認列", - "customizeView": "自訂視圖", - "viewOptions": "查看選項", - "selectAll": "選擇所有", - "selectNone": "沒有選擇", - "selectedResources": "選定的資源", - "enableSelected": "啟用選中的", - "disableSelected": "禁用選中的", - "checkSelectedStatus": "檢查選中的狀態", - "clients": "客戶端", - "accessClientSelect": "選擇機器客戶端", - "resourceClientDescription": "可以存取此資源的機器客戶端", - "regenerate": "重新產生", - "credentials": "憑證", - "savecredentials": "儲存憑證", - "regenerateCredentialsButton": "重新產生憑證", - "regenerateCredentials": "重新產生憑證", - "generatedcredentials": "已產生的憑證", - "copyandsavethesecredentials": "複製並儲存這些憑證", - "copyandsavethesecredentialsdescription": "離開此頁面後將不會再顯示這些憑證。請立即安全儲存。", - "credentialsSaved": "憑證已儲存", - "credentialsSavedDescription": "憑證已成功重新產生並儲存。", - "credentialsSaveError": "憑證儲存錯誤", - "credentialsSaveErrorDescription": "重新產生和儲存憑證時發生錯誤。", - "regenerateCredentialsWarning": "重新產生憑證將使先前的憑證失效並導致斷線。請確保更新任何使用這些憑證的設定。", - "confirm": "確認", - "regenerateCredentialsConfirmation": "您確定要重新產生憑證嗎?", - "endpoint": "端點", - "Id": "ID", - "SecretKey": "密鑰", - "niceId": "友善 ID", - "niceIdUpdated": "友善 ID 已更新", - "niceIdUpdatedSuccessfully": "友善 ID 更新成功", - "niceIdUpdateError": "更新友善 ID 時發生錯誤", - "niceIdUpdateErrorDescription": "更新友善 ID 時發生錯誤。", - "niceIdCannotBeEmpty": "友善 ID 不能為空", - "enterIdentifier": "輸入識別碼", - "identifier": "識別碼", - "deviceLoginUseDifferentAccount": "不是您嗎?使用其他帳戶。", - "deviceLoginDeviceRequestingAccessToAccount": "有裝置正在請求存取此帳戶。", - "noData": "無資料", - "machineClients": "機器客戶端", - "install": "安裝", - "run": "執行", - "clientNameDescription": "客戶端的顯示名稱,可以稍後更改。", - "clientAddress": "客戶端位址(進階)", - "setupFailedToFetchSubnet": "取得預設子網路失敗", - "setupSubnetAdvanced": "子網路(進階)", - "setupSubnetDescription": "此組織內部網路的子網路。", - "setupUtilitySubnet": "工具子網路(進階)", - "setupUtilitySubnetDescription": "此組織別名位址和 DNS 伺服器的子網路。", - "siteRegenerateAndDisconnect": "重新產生並斷開連接", - "siteRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此站點的連接嗎?", - "siteRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開站點連接。站點需要使用新憑證重新啟動。", - "siteRegenerateCredentialsConfirmation": "您確定要重新產生此站點的憑證嗎?", - "siteRegenerateCredentialsWarning": "這將重新產生憑證。站點將保持連接,直到您手動重新啟動並使用新憑證。", - "clientRegenerateAndDisconnect": "重新產生並斷開連接", - "clientRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此客戶端的連接嗎?", - "clientRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開客戶端連接。客戶端需要使用新憑證重新啟動。", - "clientRegenerateCredentialsConfirmation": "您確定要重新產生此客戶端的憑證嗎?", - "clientRegenerateCredentialsWarning": "這將重新產生憑證。客戶端將保持連接,直到您手動重新啟動並使用新憑證。", - "remoteExitNodeRegenerateAndDisconnect": "重新產生並斷開連接", - "remoteExitNodeRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此遠端出口節點的連接嗎?", - "remoteExitNodeRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開遠端出口節點連接。遠端出口節點需要使用新憑證重新啟動。", - "remoteExitNodeRegenerateCredentialsConfirmation": "您確定要重新產生此遠端出口節點的憑證嗎?", - "remoteExitNodeRegenerateCredentialsWarning": "這將重新產生憑證。遠端出口節點將保持連接,直到您手動重新啟動並使用新憑證。", - "agent": "代理", - "personalUseOnly": "僅限個人使用", - "loginPageLicenseWatermark": "此實例僅授權個人使用。", - "instanceIsUnlicensed": "此實例未授權。", - "portRestrictions": "連接埠限制", - "allPorts": "全部", - "custom": "自訂", - "allPortsAllowed": "允許所有連接埠", - "allPortsBlocked": "阻擋所有連接埠", - "tcpPortsDescription": "指定此資源允許的 TCP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:80,443,8000-9000)。", - "udpPortsDescription": "指定此資源允許的 UDP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:53,123,500-600)。", - "organizationLoginPageTitle": "組織登入頁面", - "organizationLoginPageDescription": "自訂此組織的登入頁面", - "resourceLoginPageTitle": "資源登入頁面", - "resourceLoginPageDescription": "自訂個別資源的登入頁面", - "enterConfirmation": "輸入確認", - "blueprintViewDetails": "詳細資訊", - "defaultIdentityProvider": "預設身份提供者", - "defaultIdentityProviderDescription": "當選擇預設身份提供者時,使用者將自動被重新導向到該提供者進行驗證。", - "editInternalResourceDialogNetworkSettings": "網路設定", - "editInternalResourceDialogAccessPolicy": "存取策略", - "editInternalResourceDialogAddRoles": "新增角色", - "editInternalResourceDialogAddUsers": "新增使用者", - "editInternalResourceDialogAddClients": "新增客戶端", - "editInternalResourceDialogDestinationLabel": "目的地", - "editInternalResourceDialogDestinationDescription": "指定內部資源的目的地位址。根據所選模式,這可以是主機名稱、IP 位址或 CIDR 範圍。可選擇設定內部 DNS 別名以便識別。", - "editInternalResourceDialogPortRestrictionsDescription": "限制對特定 TCP/UDP 連接埠的存取,或允許/阻擋所有連接埠。", - "editInternalResourceDialogTcp": "TCP", - "editInternalResourceDialogUdp": "UDP", - "editInternalResourceDialogIcmp": "ICMP", - "editInternalResourceDialogAccessControl": "存取控制", - "editInternalResourceDialogAccessControlDescription": "控制哪些角色、使用者和機器客戶端在連接時可以存取此資源。管理員始終擁有存取權限。", - "editInternalResourceDialogPortRangeValidationError": "連接埠範圍必須是「*」表示所有連接埠,或以逗號分隔的連接埠和範圍列表(例如:「80,443,8000-9000」)。連接埠必須介於 1 到 65535 之間。", - "orgAuthWhatsThis": "我在哪裡可以找到我的組織 ID?", - "learnMore": "了解更多", - "backToHome": "返回首頁", - "needToSignInToOrg": "需要使用您組織的身份提供者嗎?", - "maintenanceMode": "維護模式", - "maintenanceModeDescription": "向訪客顯示維護頁面", - "maintenanceModeType": "維護模式類型", - "showMaintenancePage": "向訪客顯示維護頁面", - "enableMaintenanceMode": "啟用維護模式", - "automatic": "自動", - "automaticModeDescription": "僅在所有後端目標都關閉或不健康時顯示維護頁面。只要至少有一個目標健康,您的資源就會正常運作。", - "forced": "強制", - "forcedModeDescription": "無論後端健康狀況如何,始終顯示維護頁面。當您想要阻止所有存取時,用於計劃維護。", - "warning:": "警告:", - "forcedeModeWarning": "所有流量將被導向維護頁面。您的後端資源將不會收到任何請求。", - "pageTitle": "頁面標題", - "pageTitleDescription": "維護頁面上顯示的主標題", - "maintenancePageMessage": "維護訊息", - "maintenancePageMessagePlaceholder": "我們很快就會回來!我們的網站目前正在進行預定維護。", - "maintenancePageMessageDescription": "說明維護的詳細訊息", - "maintenancePageTimeTitle": "預計完成時間(可選)", - "maintenanceTime": "例如:2 小時、11 月 1 日下午 5:00", - "maintenanceEstimatedTimeDescription": "您預計何時完成維護", - "editDomain": "編輯網域", - "editDomainDescription": "為您的資源選擇網域", - "maintenanceModeDisabledTooltip": "此功能需要有效的授權才能啟用。", - "maintenanceScreenTitle": "服務暫時無法使用", - "maintenanceScreenMessage": "我們目前遇到技術問題。請稍後再試。", - "maintenanceScreenEstimatedCompletion": "預計完成時間:", - "createInternalResourceDialogDestinationRequired": "目的地為必填欄位" -} \ No newline at end of file + "priority": "優先權", + "priorityDescription": "先評估更高優先度線路。優先度 = 100 意味著自動排序(系統決定). 使用另一個數字強制執行手動優先度。", + "instanceName": "實例名稱", + "pathMatchModalTitle": "配置路徑匹配", + "pathMatchModalDescription": "根據傳入請求的路徑設置匹配方式。", + "pathMatchType": "匹配類型", + "pathMatchPrefix": "前綴", + "pathMatchExact": "精準的", + "pathMatchRegex": "正則表達式", + "pathMatchValue": "路徑值", + "clear": "清空", + "saveChanges": "保存更改", + "pathMatchRegexPlaceholder": "^/api/.*", + "pathMatchDefaultPlaceholder": "/路徑", + "pathMatchPrefixHelp": "範例: /api 匹配/api, /api/users 等。", + "pathMatchExactHelp": "範例:/api 匹配僅限/api", + "pathMatchRegexHelp": "例如:^/api/.* 匹配/api/why", + "pathRewriteModalTitle": "配置路徑重寫", + "pathRewriteModalDescription": "在轉發到目標之前變換匹配的路徑。", + "pathRewriteType": "重寫類型", + "pathRewritePrefixOption": "前綴 - 替換前綴", + "pathRewriteExactOption": "精確-替換整個路徑", + "pathRewriteRegexOption": "正則表達式 - 替換模式", + "pathRewriteStripPrefixOption": "刪除前綴 - 刪除前綴", + "pathRewriteValue": "重寫值", + "pathRewriteRegexPlaceholder": "/new/$1", + "pathRewriteDefaultPlaceholder": "/new-path", + "pathRewritePrefixHelp": "用此值替換匹配的前綴", + "pathRewriteExactHelp": "當路徑匹配時用此值替換整個路徑", + "pathRewriteRegexHelp": "使用抓取組,如$1,$2來替換", + "pathRewriteStripPrefixHelp": "留空以脫離前綴或提供新的前綴", + "pathRewritePrefix": "前綴", + "pathRewriteExact": "精準的", + "pathRewriteRegex": "正則表達式", + "pathRewriteStrip": "帶狀圖", + "pathRewriteStripLabel": "條形圖", + "sidebarEnableEnterpriseLicense": "啟用企業許可證", + "cannotbeUndone": "無法撤消。", + "toConfirm": "確認", + "deleteClientQuestion": "您確定要從站點和組織中刪除客戶嗎?", + "clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。", + "sidebarLogs": "日誌", + "request": "請求", + "requests": "請求", + "logs": "日誌", + "logsSettingsDescription": "監視從此 orginization 中收集的日誌", + "searchLogs": "搜索日誌...", + "action": "行動", + "actor": "執行者", + "timestamp": "時間戳", + "accessLogs": "訪問日誌", + "exportCsv": "導出 CSV", + "exportError": "匯出 CSV 時發生未知錯誤", + "exportCsvTooltip": "在時間範圍內", + "actorId": "執行者 ID", + "allowedByRule": "根據規則允許", + "allowedNoAuth": "無認證", + "validAccessToken": "有效訪問令牌", + "validHeaderAuth": "有效的 Header 身份驗證", + "validPincode": "有效的 Pincode", + "validPassword": "有效密碼", + "validEmail": "有效的 email", + "validSSO": "有效的 SSO", + "resourceBlocked": "資源被阻止", + "droppedByRule": "被規則刪除", + "noSessions": "無會話", + "temporaryRequestToken": "臨時請求令牌", + "noMoreAuthMethods": "無有效授權", + "ip": "IP", + "reason": "原因", + "requestLogs": "請求日誌", + "requestAnalytics": "請求分析", + "host": "主機", + "location": "地點", + "actionLogs": "操作日誌", + "sidebarLogsRequest": "請求日誌", + "sidebarLogsAccess": "訪問日誌", + "sidebarLogsAction": "操作日誌", + "logRetention": "日誌保留", + "logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌", + "requestLogsDescription": "查看此機構資源的詳細請求日誌", + "requestAnalyticsDescription": "查看此組織資源的詳細請求分析", + "logRetentionRequestLabel": "請求日誌保留", + "logRetentionRequestDescription": "保留請求日誌的時間", + "logRetentionAccessLabel": "訪問日誌保留", + "logRetentionAccessDescription": "保留訪問日誌的時間", + "logRetentionActionLabel": "動作日誌保留", + "logRetentionActionDescription": "保留操作日誌的時間", + "logRetentionDisabled": "已禁用", + "logRetention3Days": "3 天", + "logRetention7Days": "7 天", + "logRetention14Days": "14 天", + "logRetention30Days": "30 天", + "logRetention90Days": "90 天", + "logRetentionForever": "永遠的", + "logRetentionEndOfFollowingYear": "次年年底", + "actionLogsDescription": "查看此機構執行的操作歷史", + "accessLogsDescription": "查看此機構資源的訪問認證請求", + "licenseRequiredToUse": "需要企業許可證才能使用此功能。", + "certResolver": "證書解決器", + "certResolverDescription": "選擇用於此資源的證書解析器。", + "selectCertResolver": "選擇證書解析", + "enterCustomResolver": "輸入自訂解析器", + "preferWildcardCert": "喜歡通配符證書", + "unverified": "未驗證", + "domainSetting": "域設置", + "domainSettingDescription": "配置您的域的設置", + "preferWildcardCertDescription": "嘗試生成通配符證書(需要正確配置的證書解析器)。", + "recordName": "記錄名稱", + "auto": "自動操作", + "TTL": "TTL", + "howToAddRecords": "如何添加記錄", + "dnsRecord": "DNS 記錄", + "required": "必填", + "domainSettingsUpdated": "域設置更新成功", + "orgOrDomainIdMissing": "缺少機構或域 ID", + "loadingDNSRecords": "正在載入 DNS 記錄...", + "olmUpdateAvailableInfo": "有最新版本的 Olm 可用。請更新到最新版本以獲取最佳體驗。", + "client": "用戶端:", + "proxyProtocol": "代理協議設置", + "proxyProtocolDescription": "配置代理協議以保留 TCP/UDP 服務的用戶端 IP 位址。", + "enableProxyProtocol": "啟用代理協議", + "proxyProtocolInfo": "為 TCP/UDP 後端保留用戶端 IP 位址", + "proxyProtocolVersion": "代理協議版本", + "version1": " 版本 1 (推薦)", + "version2": "版本 2", + "versionDescription": "版本 1 是基於文本和廣泛支持的版本。版本 2 是二進制和更有效率但不那麼相容。", + "warning": "警告", + "proxyProtocolWarning": "您的後端應用程式必須配置為接受代理協議連接。如果您的後端不支持代理協議,啟用這將會中斷所有連接。 請務必從 Traefik 配置您的後端到信任代理協議標題。", + "restarting": "正在重啟...", + "manual": "手動模式", + "messageSupport": "消息支持", + "supportNotAvailableTitle": "支持不可用", + "supportNotAvailableDescription": "支持現在不可用。您可以發送電子郵件到 support@pangolin.net。", + "supportRequestSentTitle": "支持請求已發送", + "supportRequestSentDescription": "您的消息已成功發送。", + "supportRequestFailedTitle": "發送請求失敗", + "supportRequestFailedDescription": "發送您的支持請求時出錯。", + "supportSubjectRequired": "主題是必填項", + "supportSubjectMaxLength": "主題必須是 255 個或更少的字元", + "supportMessageRequired": "消息是必填項", + "supportReplyTo": "回復給", + "supportSubject": "議題", + "supportSubjectPlaceholder": "輸入主題", + "supportMessage": "留言", + "supportMessagePlaceholder": "輸入您的消息", + "supportSending": "正在發送...", + "supportSend": "發送", + "supportMessageSent": "消息已發送!", + "supportWillContact": "我們很快就會聯繫起來!", + "selectLogRetention": "選擇保留日誌", + "terms": "條款", + "privacy": "隱私權", + "security": "安全性", + "docs": "文件", + "deviceActivation": "裝置啟用", + "deviceCodeInvalidFormat": "代碼必須為 9 個字元(例如:A1AJ-N5JD)", + "deviceCodeInvalidOrExpired": "代碼無效或已過期", + "deviceCodeVerifyFailed": "驗證裝置代碼失敗", + "signedInAs": "已登入為", + "deviceCodeEnterPrompt": "輸入裝置上顯示的代碼", + "continue": "繼續", + "deviceUnknownLocation": "未知位置", + "deviceAuthorizationRequested": "此授權請求來自 {location},時間為 {date}。請確保您信任此裝置,因為它將獲得帳戶存取權限。", + "deviceLabel": "裝置:{deviceName}", + "deviceWantsAccess": "想要存取您的帳戶", + "deviceExistingAccess": "現有存取權限:", + "deviceFullAccess": "完整帳戶存取權限", + "deviceOrganizationsAccess": "存取您帳戶有權限的所有組織", + "deviceAuthorize": "授權 {applicationName}", + "deviceConnected": "裝置已連接!", + "deviceAuthorizedMessage": "裝置已獲授權存取您的帳戶。請返回客戶端應用程式。", + "pangolinCloud": "Pangolin 雲端", + "viewDevices": "查看裝置", + "viewDevicesDescription": "管理您已連接的裝置", + "noDevices": "找不到裝置", + "dateCreated": "建立日期", + "unnamedDevice": "未命名裝置", + "deviceQuestionRemove": "您確定要刪除此裝置嗎?", + "deviceMessageRemove": "此操作無法復原。", + "deviceDeleteConfirm": "刪除裝置", + "deleteDevice": "刪除裝置", + "errorLoadingDevices": "載入裝置時發生錯誤", + "failedToLoadDevices": "載入裝置失敗", + "deviceDeleted": "裝置已刪除", + "deviceDeletedDescription": "裝置已成功刪除。", + "errorDeletingDevice": "刪除裝置時發生錯誤", + "failedToDeleteDevice": "刪除裝置失敗", + "showColumns": "顯示列", + "hideColumns": "隱藏列", + "columnVisibility": "列可見性", + "toggleColumn": "切換 {columnName} 列", + "allColumns": "全部列", + "defaultColumns": "默認列", + "customizeView": "自訂視圖", + "viewOptions": "查看選項", + "selectAll": "選擇所有", + "selectNone": "沒有選擇", + "selectedResources": "選定的資源", + "enableSelected": "啟用選中的", + "disableSelected": "禁用選中的", + "checkSelectedStatus": "檢查選中的狀態", + "clients": "客戶端", + "accessClientSelect": "選擇機器客戶端", + "resourceClientDescription": "可以存取此資源的機器客戶端", + "regenerate": "重新產生", + "credentials": "憑證", + "savecredentials": "儲存憑證", + "regenerateCredentialsButton": "重新產生憑證", + "regenerateCredentials": "重新產生憑證", + "generatedcredentials": "已產生的憑證", + "copyandsavethesecredentials": "複製並儲存這些憑證", + "copyandsavethesecredentialsdescription": "離開此頁面後將不會再顯示這些憑證。請立即安全儲存。", + "credentialsSaved": "憑證已儲存", + "credentialsSavedDescription": "憑證已成功重新產生並儲存。", + "credentialsSaveError": "憑證儲存錯誤", + "credentialsSaveErrorDescription": "重新產生和儲存憑證時發生錯誤。", + "regenerateCredentialsWarning": "重新產生憑證將使先前的憑證失效並導致斷線。請確保更新任何使用這些憑證的設定。", + "confirm": "確認", + "regenerateCredentialsConfirmation": "您確定要重新產生憑證嗎?", + "endpoint": "端點", + "Id": "ID", + "SecretKey": "密鑰", + "niceId": "友善 ID", + "niceIdUpdated": "友善 ID 已更新", + "niceIdUpdatedSuccessfully": "友善 ID 更新成功", + "niceIdUpdateError": "更新友善 ID 時發生錯誤", + "niceIdUpdateErrorDescription": "更新友善 ID 時發生錯誤。", + "niceIdCannotBeEmpty": "友善 ID 不能為空", + "enterIdentifier": "輸入識別碼", + "identifier": "識別碼", + "deviceLoginUseDifferentAccount": "不是您嗎?使用其他帳戶。", + "deviceLoginDeviceRequestingAccessToAccount": "有裝置正在請求存取此帳戶。", + "noData": "無資料", + "machineClients": "機器客戶端", + "install": "安裝", + "run": "執行", + "clientNameDescription": "客戶端的顯示名稱,可以稍後更改。", + "clientAddress": "客戶端位址(進階)", + "setupFailedToFetchSubnet": "取得預設子網路失敗", + "setupSubnetAdvanced": "子網路(進階)", + "setupSubnetDescription": "此組織內部網路的子網路。", + "setupUtilitySubnet": "工具子網路(進階)", + "setupUtilitySubnetDescription": "此組織別名位址和 DNS 伺服器的子網路。", + "siteRegenerateAndDisconnect": "重新產生並斷開連接", + "siteRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此站點的連接嗎?", + "siteRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開站點連接。站點需要使用新憑證重新啟動。", + "siteRegenerateCredentialsConfirmation": "您確定要重新產生此站點的憑證嗎?", + "siteRegenerateCredentialsWarning": "這將重新產生憑證。站點將保持連接,直到您手動重新啟動並使用新憑證。", + "clientRegenerateAndDisconnect": "重新產生並斷開連接", + "clientRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此客戶端的連接嗎?", + "clientRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開客戶端連接。客戶端需要使用新憑證重新啟動。", + "clientRegenerateCredentialsConfirmation": "您確定要重新產生此客戶端的憑證嗎?", + "clientRegenerateCredentialsWarning": "這將重新產生憑證。客戶端將保持連接,直到您手動重新啟動並使用新憑證。", + "remoteExitNodeRegenerateAndDisconnect": "重新產生並斷開連接", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此遠端出口節點的連接嗎?", + "remoteExitNodeRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開遠端出口節點連接。遠端出口節點需要使用新憑證重新啟動。", + "remoteExitNodeRegenerateCredentialsConfirmation": "您確定要重新產生此遠端出口節點的憑證嗎?", + "remoteExitNodeRegenerateCredentialsWarning": "這將重新產生憑證。遠端出口節點將保持連接,直到您手動重新啟動並使用新憑證。", + "agent": "代理", + "personalUseOnly": "僅限個人使用", + "loginPageLicenseWatermark": "此實例僅授權個人使用。", + "instanceIsUnlicensed": "此實例未授權。", + "portRestrictions": "連接埠限制", + "allPorts": "全部", + "custom": "自訂", + "allPortsAllowed": "允許所有連接埠", + "allPortsBlocked": "阻擋所有連接埠", + "tcpPortsDescription": "指定此資源允許的 TCP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:80,443,8000-9000)。", + "udpPortsDescription": "指定此資源允許的 UDP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:53,123,500-600)。", + "organizationLoginPageTitle": "組織登入頁面", + "organizationLoginPageDescription": "自訂此組織的登入頁面", + "resourceLoginPageTitle": "資源登入頁面", + "resourceLoginPageDescription": "自訂個別資源的登入頁面", + "enterConfirmation": "輸入確認", + "blueprintViewDetails": "詳細資訊", + "defaultIdentityProvider": "預設身份提供者", + "defaultIdentityProviderDescription": "當選擇預設身份提供者時,使用者將自動被重新導向到該提供者進行驗證。", + "editInternalResourceDialogNetworkSettings": "網路設定", + "editInternalResourceDialogAccessPolicy": "存取策略", + "editInternalResourceDialogAddRoles": "新增角色", + "editInternalResourceDialogAddUsers": "新增使用者", + "editInternalResourceDialogAddClients": "新增客戶端", + "editInternalResourceDialogDestinationLabel": "目的地", + "editInternalResourceDialogDestinationDescription": "指定內部資源的目的地位址。根據所選模式,這可以是主機名稱、IP 位址或 CIDR 範圍。可選擇設定內部 DNS 別名以便識別。", + "editInternalResourceDialogPortRestrictionsDescription": "限制對特定 TCP/UDP 連接埠的存取,或允許/阻擋所有連接埠。", + "editInternalResourceDialogTcp": "TCP", + "editInternalResourceDialogUdp": "UDP", + "editInternalResourceDialogIcmp": "ICMP", + "editInternalResourceDialogAccessControl": "存取控制", + "editInternalResourceDialogAccessControlDescription": "控制哪些角色、使用者和機器客戶端在連接時可以存取此資源。管理員始終擁有存取權限。", + "editInternalResourceDialogPortRangeValidationError": "連接埠範圍必須是「*」表示所有連接埠,或以逗號分隔的連接埠和範圍列表(例如:「80,443,8000-9000」)。連接埠必須介於 1 到 65535 之間。", + "orgAuthWhatsThis": "我在哪裡可以找到我的組織 ID?", + "learnMore": "了解更多", + "backToHome": "返回首頁", + "needToSignInToOrg": "需要使用您組織的身份提供者嗎?", + "maintenanceMode": "維護模式", + "maintenanceModeDescription": "向訪客顯示維護頁面", + "maintenanceModeType": "維護模式類型", + "showMaintenancePage": "向訪客顯示維護頁面", + "enableMaintenanceMode": "啟用維護模式", + "automatic": "自動", + "automaticModeDescription": "僅在所有後端目標都關閉或不健康時顯示維護頁面。只要至少有一個目標健康,您的資源就會正常運作。", + "forced": "強制", + "forcedModeDescription": "無論後端健康狀況如何,始終顯示維護頁面。當您想要阻止所有存取時,用於計劃維護。", + "warning:": "警告:", + "forcedeModeWarning": "所有流量將被導向維護頁面。您的後端資源將不會收到任何請求。", + "pageTitle": "頁面標題", + "pageTitleDescription": "維護頁面上顯示的主標題", + "maintenancePageMessage": "維護訊息", + "maintenancePageMessagePlaceholder": "我們很快就會回來!我們的網站目前正在進行預定維護。", + "maintenancePageMessageDescription": "說明維護的詳細訊息", + "maintenancePageTimeTitle": "預計完成時間(可選)", + "maintenanceTime": "例如:2 小時、11 月 1 日下午 5:00", + "maintenanceEstimatedTimeDescription": "您預計何時完成維護", + "editDomain": "編輯網域", + "editDomainDescription": "為您的資源選擇網域", + "maintenanceModeDisabledTooltip": "此功能需要有效的授權才能啟用。", + "maintenanceScreenTitle": "服務暫時無法使用", + "maintenanceScreenMessage": "我們目前遇到技術問題。請稍後再試。", + "maintenanceScreenEstimatedCompletion": "預計完成時間:", + "createInternalResourceDialogDestinationRequired": "目的地為必填欄位" +} diff --git a/package-lock.json b/package-lock.json index 69a0b5a91..f5b422b89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,12 +70,12 @@ "lucide-react": "0.577.0", "maxmind": "5.0.5", "moment": "2.30.1", - "next": "15.5.12", + "next": "15.5.14", "next-intl": "4.8.3", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "nodemailer": "8.0.1", + "nodemailer": "8.0.4", "oslo": "1.2.1", "pg": "8.20.0", "posthog-node": "5.28.0", @@ -90,20 +90,20 @@ "reodotdev": "1.1.0", "resend": "6.9.2", "semver": "7.7.4", - "sshpk": "^1.18.0", + "sshpk": "1.18.0", "stripe": "20.4.1", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.5.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", - "use-debounce": "^10.1.0", + "use-debounce": "10.1.0", "uuid": "13.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.19.0", - "yaml": "2.8.2", + "yaml": "2.8.3", "yargs": "18.0.0", "zod": "4.3.6", "zod-validation-error": "5.0.0" @@ -112,7 +112,7 @@ "@dotenvx/dotenvx": "1.54.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "5.2.10", - "@tailwindcss/postcss": "4.2.1", + "@tailwindcss/postcss": "4.2.2", "@tanstack/react-query-devtools": "5.91.3", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -131,21 +131,21 @@ "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", - "@types/sshpk": "^1.17.4", + "@types/sshpk": "1.17.4", "@types/swagger-ui-express": "4.1.8", "@types/topojson-client": "3.1.5", "@types/ws": "8.18.1", "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "esbuild": "0.27.3", + "esbuild": "0.27.4", "esbuild-node-externals": "1.20.1", "eslint": "10.0.3", "eslint-config-next": "16.1.7", "postcss": "8.5.8", "prettier": "3.8.1", "react-email": "5.2.10", - "tailwindcss": "4.2.1", + "tailwindcss": "4.2.2", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", @@ -1006,13 +1006,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.12.tgz", - "integrity": "sha512-xjyucfn+F+kMf25c+LIUnvX3oyLSlj9T0Vncs5WMQI6G36JdnSwC8g0qf8RajfmSClXr660EpTz7FFKluZ4BqQ==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", + "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.6", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { @@ -1579,9 +1579,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -1596,9 +1596,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -1613,9 +1613,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -1630,9 +1630,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -1647,9 +1647,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -1664,9 +1664,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -1681,9 +1681,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -1698,9 +1698,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -1715,9 +1715,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -1732,9 +1732,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -1749,9 +1749,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -1766,9 +1766,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -1783,9 +1783,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -1800,9 +1800,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -1817,9 +1817,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -1834,9 +1834,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -1851,9 +1851,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -1868,9 +1868,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -1885,9 +1885,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -1902,9 +1902,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -1919,9 +1919,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -1936,9 +1936,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -1953,9 +1953,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -1970,9 +1970,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -1987,9 +1987,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -2004,9 +2004,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -2886,9 +2886,10 @@ } }, "node_modules/@next/env": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", - "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==" + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", + "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "16.1.7", @@ -2901,12 +2902,13 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", - "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", + "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2916,12 +2918,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", - "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", + "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2931,12 +2934,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", - "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", + "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2946,12 +2950,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", - "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", + "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2961,12 +2966,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", - "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", + "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2976,12 +2982,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", - "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", + "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2991,12 +2998,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", - "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", + "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3006,12 +3014,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", - "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", + "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -8075,49 +8084,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.31.1", + "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -8132,9 +8141,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -8149,9 +8158,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -8166,9 +8175,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -8183,9 +8192,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -8200,9 +8209,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ "arm64" ], @@ -8217,9 +8226,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -8234,9 +8243,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ "x64" ], @@ -8251,9 +8260,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ "x64" ], @@ -8268,9 +8277,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -8362,9 +8371,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -8379,9 +8388,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -8396,17 +8405,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", - "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tanstack/query-core": { @@ -9826,9 +9835,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10329,9 +10338,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -12141,9 +12150,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { @@ -12354,9 +12363,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12368,32 +12377,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/esbuild-node-externals": { @@ -12532,9 +12541,9 @@ "license": "MIT" }, "node_modules/eslint-config-next/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -13158,9 +13167,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", - "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", "funding": [ { "type": "github", @@ -13170,8 +13179,8 @@ "license": "MIT", "dependencies": { "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -13304,9 +13313,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -14748,9 +14757,9 @@ } }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -14764,23 +14773,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -14799,9 +14808,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -14820,9 +14829,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -14841,9 +14850,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -14862,9 +14871,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -14883,9 +14892,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -14904,9 +14913,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -14925,9 +14934,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -14946,9 +14955,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -14967,9 +14976,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -14988,9 +14997,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -15252,9 +15261,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -15489,12 +15498,13 @@ } }, "node_modules/next": { - "version": "15.5.12", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", - "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", + "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "license": "MIT", "peer": true, "dependencies": { - "@next/env": "15.5.12", + "@next/env": "15.5.14", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -15507,14 +15517,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.12", - "@next/swc-darwin-x64": "15.5.12", - "@next/swc-linux-arm64-gnu": "15.5.12", - "@next/swc-linux-arm64-musl": "15.5.12", - "@next/swc-linux-x64-gnu": "15.5.12", - "@next/swc-linux-x64-musl": "15.5.12", - "@next/swc-win32-arm64-msvc": "15.5.12", - "@next/swc-win32-x64-msvc": "15.5.12", + "@next/swc-darwin-arm64": "15.5.14", + "@next/swc-darwin-x64": "15.5.14", + "@next/swc-linux-arm64-gnu": "15.5.14", + "@next/swc-linux-arm64-musl": "15.5.14", + "@next/swc-linux-x64-gnu": "15.5.14", + "@next/swc-linux-x64-musl": "15.5.14", + "@next/swc-win32-arm64-msvc": "15.5.14", + "@next/swc-win32-x64-msvc": "15.5.14", "sharp": "^0.34.3" }, "peerDependencies": { @@ -15710,9 +15720,10 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", - "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz", + "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } @@ -16355,9 +16366,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", "funding": [ { "type": "github", @@ -16411,9 +16422,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "license": "MIT", "funding": { "type": "opencollective", @@ -16543,9 +16554,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -18599,9 +18610,9 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", "funding": [ { "type": "github", @@ -18740,16 +18751,16 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT", "peer": true }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", "engines": { @@ -18970,9 +18981,9 @@ } }, "node_modules/tsc-alias/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -19760,9 +19771,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 7f7900eb5..596bc91c0 100644 --- a/package.json +++ b/package.json @@ -93,12 +93,12 @@ "lucide-react": "0.577.0", "maxmind": "5.0.5", "moment": "2.30.1", - "next": "15.5.12", + "next": "15.5.14", "next-intl": "4.8.3", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "nodemailer": "8.0.1", + "nodemailer": "8.0.4", "oslo": "1.2.1", "pg": "8.20.0", "posthog-node": "5.28.0", @@ -113,20 +113,20 @@ "reodotdev": "1.1.0", "resend": "6.9.2", "semver": "7.7.4", - "sshpk": "^1.18.0", + "sshpk": "1.18.0", "stripe": "20.4.1", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.5.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", - "use-debounce": "^10.1.0", + "use-debounce": "10.1.0", "uuid": "13.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.19.0", - "yaml": "2.8.2", + "yaml": "2.8.3", "yargs": "18.0.0", "zod": "4.3.6", "zod-validation-error": "5.0.0" @@ -135,7 +135,7 @@ "@dotenvx/dotenvx": "1.54.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "5.2.10", - "@tailwindcss/postcss": "4.2.1", + "@tailwindcss/postcss": "4.2.2", "@tanstack/react-query-devtools": "5.91.3", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -154,28 +154,28 @@ "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", - "@types/sshpk": "^1.17.4", + "@types/sshpk": "1.17.4", "@types/swagger-ui-express": "4.1.8", "@types/topojson-client": "3.1.5", "@types/ws": "8.18.1", "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "esbuild": "0.27.3", + "esbuild": "0.27.4", "esbuild-node-externals": "1.20.1", "eslint": "10.0.3", "eslint-config-next": "16.1.7", "postcss": "8.5.8", "prettier": "3.8.1", "react-email": "5.2.10", - "tailwindcss": "4.2.1", + "tailwindcss": "4.2.2", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", "typescript-eslint": "8.56.1" }, "overrides": { - "esbuild": "0.27.3", + "esbuild": "0.27.4", "dompurify": "3.3.2" } } diff --git a/public/screenshots/hero.png b/public/screenshots/hero.png index f42a830e6..c33d2924b 100644 Binary files a/public/screenshots/hero.png and b/public/screenshots/hero.png differ diff --git a/public/screenshots/private-resources.png b/public/screenshots/private-resources.png index f48d9279c..7e5b05f40 100644 Binary files a/public/screenshots/private-resources.png and b/public/screenshots/private-resources.png differ diff --git a/public/screenshots/public-resources.png b/public/screenshots/public-resources.png index f42a830e6..c33d2924b 100644 Binary files a/public/screenshots/public-resources.png and b/public/screenshots/public-resources.png differ diff --git a/public/screenshots/sites.png b/public/screenshots/sites.png index 86b32b81b..fae86ceeb 100644 Binary files a/public/screenshots/sites.png and b/public/screenshots/sites.png differ diff --git a/public/screenshots/users.png b/public/screenshots/users.png index 91286e02a..3b47e8bbc 100644 Binary files a/public/screenshots/users.png and b/public/screenshots/users.png differ diff --git a/public/third-party/dd.png b/public/third-party/dd.png new file mode 100644 index 000000000..598771157 Binary files /dev/null and b/public/third-party/dd.png differ diff --git a/public/third-party/s3.png b/public/third-party/s3.png new file mode 100644 index 000000000..f86959a93 Binary files /dev/null and b/public/third-party/s3.png differ diff --git a/server/auth/actions.ts b/server/auth/actions.ts index fc5daa4f8..213dab9d3 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -140,7 +140,11 @@ export enum ActionsEnum { exportLogs = "exportLogs", listApprovals = "listApprovals", updateApprovals = "updateApprovals", - signSshKey = "signSshKey" + signSshKey = "signSshKey", + createEventStreamingDestination = "createEventStreamingDestination", + updateEventStreamingDestination = "updateEventStreamingDestination", + deleteEventStreamingDestination = "deleteEventStreamingDestination", + listEventStreamingDestinations = "listEventStreamingDestinations" } export async function checkUserActionPermission( diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9366e32e1..86a0c0352 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -60,8 +60,7 @@ function createDb() { }) ); } else { - const maxReplicaConnections = - poolConfig?.max_replica_connections || 20; + const maxReplicaConnections = poolConfig?.max_replica_connections || 20; for (const conn of replicaConnections) { const replicaPool = createPool( conn.connection_string, @@ -91,4 +90,5 @@ export default db; export const primaryDb = db.$primary; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] ->[0]; \ No newline at end of file +>[0]; +export const DB_TYPE: "pg" | "sqlite" = "pg"; diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index bb1e866c4..4122fb5b5 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -8,7 +8,8 @@ import { real, text, index, - primaryKey + primaryKey, + uniqueIndex } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { @@ -291,6 +292,7 @@ export const accessAuditLog = pgTable( actor: varchar("actor", { length: 255 }), actorId: varchar("actorId", { length: 255 }), resourceId: integer("resourceId"), + siteResourceId: integer("siteResourceId"), ip: varchar("ip", { length: 45 }), type: varchar("type", { length: 100 }).notNull(), action: boolean("action").notNull(), @@ -391,7 +393,8 @@ export const siteProvisioningKeys = pgTable("siteProvisioningKeys", { lastUsed: varchar("lastUsed", { length: 255 }), maxBatchSize: integer("maxBatchSize"), // null = no limit numUsed: integer("numUsed").notNull().default(0), - validUntil: varchar("validUntil", { length: 255 }) + validUntil: varchar("validUntil", { length: 255 }), + approveNewSites: boolean("approveNewSites").notNull().default(true) }); export const siteProvisioningKeyOrg = pgTable( @@ -415,6 +418,46 @@ export const siteProvisioningKeyOrg = pgTable( ] ); +export const eventStreamingDestinations = pgTable( + "eventStreamingDestinations", + { + destinationId: serial("destinationId").primaryKey(), + orgId: varchar("orgId", { length: 255 }) + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + sendConnectionLogs: boolean("sendConnectionLogs").notNull().default(false), + sendRequestLogs: boolean("sendRequestLogs").notNull().default(false), + sendActionLogs: boolean("sendActionLogs").notNull().default(false), + sendAccessLogs: boolean("sendAccessLogs").notNull().default(false), + type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc. + config: text("config").notNull(), // JSON string with the configuration for the destination + enabled: boolean("enabled").notNull().default(true), + createdAt: bigint("createdAt", { mode: "number" }).notNull(), + updatedAt: bigint("updatedAt", { mode: "number" }).notNull() + } +); + +export const eventStreamingCursors = pgTable( + "eventStreamingCursors", + { + cursorId: serial("cursorId").primaryKey(), + destinationId: integer("destinationId") + .notNull() + .references(() => eventStreamingDestinations.destinationId, { + onDelete: "cascade" + }), + logType: varchar("logType", { length: 50 }).notNull(), // "request" | "action" | "access" | "connection" + lastSentId: bigint("lastSentId", { mode: "number" }).notNull().default(0), + lastSentAt: bigint("lastSentAt", { mode: "number" }) // epoch milliseconds, null if never sent + }, + (table) => [ + uniqueIndex("idx_eventStreamingCursors_dest_type").on( + table.destinationId, + table.logType + ) + ] +); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -437,3 +480,18 @@ export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; export type ConnectionAuditLog = InferSelectModel; +export type SessionTransferToken = InferSelectModel< + typeof sessionTransferToken +>; +export type BannedEmail = InferSelectModel; +export type BannedIp = InferSelectModel; +export type SiteProvisioningKey = InferSelectModel; +export type SiteProvisioningKeyOrg = InferSelectModel< + typeof siteProvisioningKeyOrg +>; +export type EventStreamingDestination = InferSelectModel< + typeof eventStreamingDestinations +>; +export type EventStreamingCursor = InferSelectModel< + typeof eventStreamingCursors +>; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index bb05ca358..acc3bb17f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -100,7 +100,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), + status: varchar("status").$type<"pending" | "approved">().default("approved") }); export const resources = pgTable("resources", { @@ -292,7 +293,8 @@ export const users = pgTable("user", { termsVersion: varchar("termsVersion"), marketingEmailConsent: boolean("marketingEmailConsent").default(false), serverAdmin: boolean("serverAdmin").notNull().default(false), - lastPasswordChange: bigint("lastPasswordChange", { mode: "number" }) + lastPasswordChange: bigint("lastPasswordChange", { mode: "number" }), + locale: varchar("locale") }); export const newts = pgTable("newt", { @@ -1078,6 +1080,7 @@ export type ResourceWhitelist = InferSelectModel; export type VersionMigration = InferSelectModel; export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; +export type DnsRecord = InferSelectModel; export type SupporterKey = InferSelectModel; export type Idp = InferSelectModel; export type ApiKey = InferSelectModel; diff --git a/server/db/regions.ts b/server/db/regions.ts new file mode 100644 index 000000000..90a380a29 --- /dev/null +++ b/server/db/regions.ts @@ -0,0 +1,196 @@ +// Regions of the World +// as of 2025-10-25 +// +// Adapted according to the United Nations Geoscheme +// see https://www.unicode.org/cldr/charts/48/supplemental/territory_containment_un_m_49.html +// see https://unstats.un.org/unsd/methodology/m49 + +export const REGIONS = [ + { + name: "regionAfrica", + id: "002", + includes: [ + { + name: "regionNorthernAfrica", + id: "015", + countries: ["DZ", "EG", "LY", "MA", "SD", "TN", "EH"] + }, + { + name: "regionEasternAfrica", + id: "014", + countries: ["IO", "BI", "KM", "DJ", "ER", "ET", "TF", "KE", "MG", "MW", "MU", "YT", "MZ", "RE", "RW", "SC", "SO", "SS", "UG", "ZM", "ZW"] + }, + { + name: "regionMiddleAfrica", + id: "017", + countries: ["AO", "CM", "CF", "TD", "CG", "CD", "GQ", "GA", "ST"] + }, + { + name: "regionSouthernAfrica", + id: "018", + countries: ["BW", "SZ", "LS", "NA", "ZA"] + }, + { + name: "regionWesternAfrica", + id: "011", + countries: ["BJ", "BF", "CV", "CI", "GM", "GH", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SN", "SL", "TG"] + } + ] + }, + { + name: "regionAmericas", + id: "019", + includes: [ + { + name: "regionCaribbean", + id: "029", + countries: ["AI", "AG", "AW", "BS", "BB", "BQ", "VG", "KY", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "MQ", "MS", "PR", "BL", "KN", "LC", "MF", "VC", "SX", "TT", "TC", "VI"] + }, + { + name: "regionCentralAmerica", + id: "013", + countries: ["BZ", "CR", "SV", "GT", "HN", "MX", "NI", "PA"] + }, + { + name: "regionSouthAmerica", + id: "005", + countries: ["AR", "BO", "BV", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PY", "PE", "GS", "SR", "UY", "VE"] + }, + { + name: "regionNorthernAmerica", + id: "021", + countries: ["BM", "CA", "GL", "PM", "US"] + } + ] + }, + { + name: "regionAsia", + id: "142", + includes: [ + { + name: "regionCentralAsia", + id: "143", + countries: ["KZ", "KG", "TJ", "TM", "UZ"] + }, + { + name: "regionEasternAsia", + id: "030", + countries: ["CN", "HK", "MO", "KP", "JP", "MN", "KR"] + }, + { + name: "regionSouthEasternAsia", + id: "035", + countries: ["BN", "KH", "ID", "LA", "MY", "MM", "PH", "SG", "TH", "TL", "VN"] + }, + { + name: "regionSouthernAsia", + id: "034", + countries: ["AF", "BD", "BT", "IN", "IR", "MV", "NP", "PK", "LK"] + }, + { + name: "regionWesternAsia", + id: "145", + countries: ["AM", "AZ", "BH", "CY", "GE", "IQ", "IL", "JO", "KW", "LB", "OM", "QA", "SA", "PS", "SY", "TR", "AE", "YE"] + } + ] + }, + { + name: "regionEurope", + id: "150", + includes: [ + { + name: "regionEasternEurope", + id: "151", + countries: ["BY", "BG", "CZ", "HU", "PL", "MD", "RO", "RU", "SK", "UA"] + }, + { + name: "regionNorthernEurope", + id: "154", + countries: ["AX", "DK", "EE", "FO", "FI", "GG", "IS", "IE", "IM", "JE", "LV", "LT", "NO", "SJ", "SE", "GB"] + }, + { + name: "regionSouthernEurope", + id: "039", + countries: ["AL", "AD", "BA", "HR", "GI", "GR", "VA", "IT", "MT", "ME", "MK", "PT", "SM", "RS", "SI", "ES"] + }, + { + name: "regionWesternEurope", + id: "155", + countries: ["AT", "BE", "FR", "DE", "LI", "LU", "MC", "NL", "CH"] + } + ] + }, + { + name: "regionOceania", + id: "009", + includes: [ + { + name: "regionAustraliaAndNewZealand", + id: "053", + countries: ["AU", "CX", "CC", "HM", "NZ", "NF"] + }, + { + name: "regionMelanesia", + id: "054", + countries: ["FJ", "NC", "PG", "SB", "VU"] + }, + { + name: "regionMicronesia", + id: "057", + countries: ["GU", "KI", "MH", "FM", "NR", "MP", "PW", "UM"] + }, + { + name: "regionPolynesia", + id: "061", + countries: ["AS", "CK", "PF", "NU", "PN", "WS", "TK", "TO", "TV", "WF"] + } + ] + } +]; + +type Subregion = { + name: string; + id: string; + countries: string[]; +}; + +type Region = { + name: string; + id: string; + includes: Subregion[]; +}; + +export function getRegionNameById(regionId: string): string | undefined { + // Check top-level regions + const region = REGIONS.find((r) => r.id === regionId); + if (region) { + return region.name; + } + + // Check subregions + for (const region of REGIONS) { + for (const subregion of region.includes) { + if (subregion.id === regionId) { + return subregion.name; + } + } + } + + return undefined; +} + +export function isValidRegionId(regionId: string): boolean { + // Check top-level regions + if (REGIONS.find((r) => r.id === regionId)) { + return true; + } + + // Check subregions + for (const region of REGIONS) { + if (region.includes.find((s) => s.id === regionId)) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 9cbc8d7be..832ff16f9 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -23,7 +23,8 @@ export default db; export const primaryDb = db; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] ->[0]; + >[0]; +export const DB_TYPE: "pg" | "sqlite" = "sqlite"; function checkFileExists(filePath: string): boolean { try { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 5913497b3..c1aa084a2 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -5,9 +5,19 @@ import { primaryKey, real, sqliteTable, - text + text, + uniqueIndex } from "drizzle-orm/sqlite-core"; -import { clients, domains, exitNodes, orgs, sessions, siteResources, sites, users } from "./schema"; +import { + clients, + domains, + exitNodes, + orgs, + sessions, + siteResources, + sites, + users +} from "./schema"; export const certificates = sqliteTable("certificates", { certId: integer("certId").primaryKey({ autoIncrement: true }), @@ -279,6 +289,7 @@ export const accessAuditLog = sqliteTable( actor: text("actor"), actorId: text("actorId"), resourceId: integer("resourceId"), + siteResourceId: integer("siteResourceId"), ip: text("ip"), location: text("location"), type: text("type").notNull(), @@ -375,7 +386,10 @@ export const siteProvisioningKeys = sqliteTable("siteProvisioningKeys", { lastUsed: text("lastUsed"), maxBatchSize: integer("maxBatchSize"), // null = no limit numUsed: integer("numUsed").notNull().default(0), - validUntil: text("validUntil") + validUntil: text("validUntil"), + approveNewSites: integer("approveNewSites", { mode: "boolean" }) + .notNull() + .default(true) }); export const siteProvisioningKeyOrg = sqliteTable( @@ -397,6 +411,50 @@ export const siteProvisioningKeyOrg = sqliteTable( ] ); +export const eventStreamingDestinations = sqliteTable( + "eventStreamingDestinations", + { + destinationId: integer("destinationId").primaryKey({ + autoIncrement: true + }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + sendConnectionLogs: integer("sendConnectionLogs", { mode: "boolean" }).notNull().default(false), + sendRequestLogs: integer("sendRequestLogs", { mode: "boolean" }).notNull().default(false), + sendActionLogs: integer("sendActionLogs", { mode: "boolean" }).notNull().default(false), + sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" }).notNull().default(false), + type: text("type").notNull(), // e.g. "http", "kafka", etc. + config: text("config").notNull(), // JSON string with the configuration for the destination + enabled: integer("enabled", { mode: "boolean" }) + .notNull() + .default(true), + createdAt: integer("createdAt").notNull(), + updatedAt: integer("updatedAt").notNull() + } +); + +export const eventStreamingCursors = sqliteTable( + "eventStreamingCursors", + { + cursorId: integer("cursorId").primaryKey({ autoIncrement: true }), + destinationId: integer("destinationId") + .notNull() + .references(() => eventStreamingDestinations.destinationId, { + onDelete: "cascade" + }), + logType: text("logType").notNull(), // "request" | "action" | "access" | "connection" + lastSentId: integer("lastSentId").notNull().default(0), + lastSentAt: integer("lastSentAt") // epoch milliseconds, null if never sent + }, + (table) => [ + uniqueIndex("idx_eventStreamingCursors_dest_type").on( + table.destinationId, + table.logType + ) + ] +); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -419,3 +477,12 @@ export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; export type ConnectionAuditLog = InferSelectModel; +export type BannedEmail = InferSelectModel; +export type BannedIp = InferSelectModel; +export type SiteProvisioningKey = InferSelectModel; +export type EventStreamingDestination = InferSelectModel< + typeof eventStreamingDestinations +>; +export type EventStreamingCursor = InferSelectModel< + typeof eventStreamingCursors +>; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 5d7c01377..1fb04ef14 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -110,7 +110,8 @@ export const sites = sqliteTable("sites", { listenPort: integer("listenPort"), dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) .notNull() - .default(true) + .default(true), + status: text("status").$type<"pending" | "approved">().default("approved") }); export const resources = sqliteTable("resources", { @@ -332,7 +333,8 @@ export const users = sqliteTable("user", { serverAdmin: integer("serverAdmin", { mode: "boolean" }) .notNull() .default(false), - lastPasswordChange: integer("lastPasswordChange") + lastPasswordChange: integer("lastPasswordChange"), + locale: text("locale") }); export const securityKeys = sqliteTable("webauthnCredentials", { diff --git a/server/lib/billing/licenses.ts b/server/lib/billing/licenses.ts index 3fecb32b5..ff942d11b 100644 --- a/server/lib/billing/licenses.ts +++ b/server/lib/billing/licenses.ts @@ -9,8 +9,8 @@ export type LicensePriceSet = { export const licensePriceSet: LicensePriceSet = { // Free license matches the freeLimitSet - [LicenseId.SMALL_LICENSE]: "price_1SxKHiD3Ee2Ir7WmvtEh17A8", - [LicenseId.BIG_LICENSE]: "price_1SxKHiD3Ee2Ir7WmMUiP0H6Y" + [LicenseId.SMALL_LICENSE]: "price_1TMJzmD3Ee2Ir7Wm05NlGImT", + [LicenseId.BIG_LICENSE]: "price_1TMJzzD3Ee2Ir7WmzJw9TerS" }; export const licensePriceSetSandbox: LicensePriceSet = { diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index 2aa38e1ef..0756ea665 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -18,7 +18,9 @@ export enum TierFeature { AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning SshPam = "sshPam", FullRbac = "fullRbac", - SiteProvisioningKeys = "siteProvisioningKeys" // handle downgrade by revoking keys if needed + SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed + SIEM = "siem", // handle downgrade by disabling SIEM integrations + DomainNamespaces = "domainNamespaces" // handle downgrade by removing custom domain namespaces } export const tierMatrix: Record = { @@ -54,5 +56,7 @@ export const tierMatrix: Record = { [TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"], [TierFeature.SshPam]: ["tier1", "tier3", "enterprise"], [TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"], - [TierFeature.SiteProvisioningKeys]: ["enterprise"] + [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"], + [TierFeature.SIEM]: ["enterprise"], + [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"] }; diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 2696b68c8..e16da2ea5 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -31,6 +31,7 @@ import { pickPort } from "@server/routers/target/helpers"; import { resourcePassword } from "@server/db"; import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; +import { isValidRegionId } from "@server/db/regions"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "../billing/tierMatrix"; @@ -863,6 +864,10 @@ function validateRule(rule: any) { if (!isValidUrlGlobPattern(rule.value)) { throw new Error(`Invalid URL glob pattern: ${rule.value}`); } + } else if (rule.match === "region") { + if (!isValidRegionId(rule.value)) { + throw new Error(`Invalid region ID provided: ${rule.value}`); + } } } diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 2239e4f9a..6ebc509b8 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { portRangeStringSchema } from "@server/lib/ip"; import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema"; +import { isValidRegionId } from "@server/db/regions"; export const SiteSchema = z.object({ name: z.string().min(1).max(100), @@ -77,7 +78,7 @@ export const AuthSchema = z.object({ export const RuleSchema = z .object({ action: z.enum(["allow", "deny", "pass"]), - match: z.enum(["cidr", "path", "ip", "country", "asn"]), + match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]), value: z.string(), priority: z.int().optional() }) @@ -137,6 +138,19 @@ export const RuleSchema = z message: "Value must be 'AS' format or 'ALL' when match is 'asn'" } + ) + .refine( + (rule) => { + if (rule.match === "region") { + return isValidRegionId(rule.value); + } + return true; + }, + { + path: ["value"], + message: + "Value must be a valid UN M.49 region or subregion ID when match is 'region'" + } ); export const HeaderSchema = z.object({ diff --git a/server/lib/consts.ts b/server/lib/consts.ts index d53bd70bb..8ad4f48e9 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.16.0"; +export const APP_VERSION = "1.17.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 7f829bcef..633983629 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -591,7 +591,7 @@ export function generateSubnetProxyTargetV2( pubKey: string | null; subnet: string | null; }[] -): SubnetProxyTargetV2 | undefined { +): SubnetProxyTargetV2[] | undefined { if (clients.length === 0) { logger.debug( `No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.` @@ -599,7 +599,7 @@ export function generateSubnetProxyTargetV2( return; } - let target: SubnetProxyTargetV2 | null = null; + let targets: SubnetProxyTargetV2[] = []; const portRange = [ ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), @@ -614,52 +614,54 @@ export function generateSubnetProxyTargetV2( if (ipSchema.safeParse(destination).success) { destination = `${destination}/32`; - target = { + targets.push({ sourcePrefixes: [], destPrefix: destination, portRange, disableIcmp, - resourceId: siteResource.siteResourceId, - }; + resourceId: siteResource.siteResourceId + }); } if (siteResource.alias && siteResource.aliasAddress) { // also push a match for the alias address - target = { + targets.push({ sourcePrefixes: [], destPrefix: `${siteResource.aliasAddress}/32`, rewriteTo: destination, portRange, disableIcmp, - resourceId: siteResource.siteResourceId, - }; + resourceId: siteResource.siteResourceId + }); } } else if (siteResource.mode == "cidr") { - target = { + targets.push({ sourcePrefixes: [], destPrefix: siteResource.destination, portRange, disableIcmp, - resourceId: siteResource.siteResourceId, - }; + resourceId: siteResource.siteResourceId + }); } - if (!target) { + if (targets.length == 0) { return; } - for (const clientSite of clients) { - if (!clientSite.subnet) { - logger.debug( - `Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.` - ); - continue; + for (const target of targets) { + for (const clientSite of clients) { + if (!clientSite.subnet) { + logger.debug( + `Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.` + ); + continue; + } + + const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + + // add client prefix to source prefixes + target.sourcePrefixes.push(clientPrefix); } - - const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; - - // add client prefix to source prefixes - target.sourcePrefixes.push(clientPrefix); } // print a nice representation of the targets @@ -667,36 +669,34 @@ export function generateSubnetProxyTargetV2( // `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}` // ); - return target; + return targets; } - /** * Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1) * by expanding each source prefix into its own target entry. * @param targetV2 - The v2 target to convert * @returns Array of v1 SubnetProxyTarget objects */ - export function convertSubnetProxyTargetsV2ToV1( - targetsV2: SubnetProxyTargetV2[] - ): SubnetProxyTarget[] { - return targetsV2.flatMap((targetV2) => - targetV2.sourcePrefixes.map((sourcePrefix) => ({ - sourcePrefix, - destPrefix: targetV2.destPrefix, - ...(targetV2.disableIcmp !== undefined && { - disableIcmp: targetV2.disableIcmp - }), - ...(targetV2.rewriteTo !== undefined && { - rewriteTo: targetV2.rewriteTo - }), - ...(targetV2.portRange !== undefined && { - portRange: targetV2.portRange - }) - })) - ); - } - +export function convertSubnetProxyTargetsV2ToV1( + targetsV2: SubnetProxyTargetV2[] +): SubnetProxyTarget[] { + return targetsV2.flatMap((targetV2) => + targetV2.sourcePrefixes.map((sourcePrefix) => ({ + sourcePrefix, + destPrefix: targetV2.destPrefix, + ...(targetV2.disableIcmp !== undefined && { + disableIcmp: targetV2.disableIcmp + }), + ...(targetV2.rewriteTo !== undefined && { + rewriteTo: targetV2.rewriteTo + }), + ...(targetV2.portRange !== undefined && { + portRange: targetV2.portRange + }) + })) + ); +} // Custom schema for validating port range strings // Format: "80,443,8000-9000" or "*" for all ports, or empty string diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 8459ce249..d636a2f2b 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -661,16 +661,16 @@ async function handleSubnetProxyTargetUpdates( ); if (addedClients.length > 0) { - const targetToAdd = generateSubnetProxyTargetV2( + const targetsToAdd = generateSubnetProxyTargetV2( siteResource, addedClients ); - if (targetToAdd) { + if (targetsToAdd) { proxyJobs.push( addSubnetProxyTargets( newt.newtId, - [targetToAdd], + targetsToAdd, newt.version ) ); @@ -698,16 +698,16 @@ async function handleSubnetProxyTargetUpdates( ); if (removedClients.length > 0) { - const targetToRemove = generateSubnetProxyTargetV2( + const targetsToRemove = generateSubnetProxyTargetV2( siteResource, removedClients ); - if (targetToRemove) { + if (targetsToRemove) { proxyJobs.push( removeSubnetProxyTargets( newt.newtId, - [targetToRemove], + targetsToRemove, newt.version ) ); @@ -1164,7 +1164,7 @@ async function handleMessagesForClientResources( } for (const resource of resources) { - const target = generateSubnetProxyTargetV2(resource, [ + const targets = generateSubnetProxyTargetV2(resource, [ { clientId: client.clientId, pubKey: client.pubKey, @@ -1172,11 +1172,11 @@ async function handleMessagesForClientResources( } ]); - if (target) { + if (targets) { proxyJobs.push( addSubnetProxyTargets( newt.newtId, - [target], + targets, newt.version ) ); @@ -1241,7 +1241,7 @@ async function handleMessagesForClientResources( } for (const resource of resources) { - const target = generateSubnetProxyTargetV2(resource, [ + const targets = generateSubnetProxyTargetV2(resource, [ { clientId: client.clientId, pubKey: client.pubKey, @@ -1249,11 +1249,11 @@ async function handleMessagesForClientResources( } ]); - if (target) { + if (targets) { proxyJobs.push( removeSubnetProxyTargets( newt.newtId, - [target], + targets, newt.version ) ); diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index abd0a8de0..7379cad7f 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -479,10 +479,7 @@ export async function getTraefikConfig( // TODO: HOW TO HANDLE ^^^^^^ BETTER const anySitesOnline = targets.some( - (target) => - target.site.online || - target.site.type === "local" || - target.site.type === "wireguard" + (target) => target.site.online ); return ( @@ -495,7 +492,7 @@ export async function getTraefikConfig( if (target.health == "unhealthy") { return false; } - + // If any sites are online, exclude offline sites if (anySitesOnline && !target.site.online) { return false; @@ -610,10 +607,7 @@ export async function getTraefikConfig( servers: (() => { // Check if any sites are online const anySitesOnline = targets.some( - (target) => - target.site.online || - target.site.type === "local" || - target.site.type === "wireguard" + (target) => target.site.online ); return targets @@ -621,7 +615,7 @@ export async function getTraefikConfig( if (!target.enabled) { return false; } - + // If any sites are online, exclude offline sites if (anySitesOnline && !target.site.online) { return false; diff --git a/server/middlewares/verifySiteProvisioningKeyAccess.ts b/server/middlewares/verifySiteProvisioningKeyAccess.ts index e0d446de6..bdf12c821 100644 --- a/server/middlewares/verifySiteProvisioningKeyAccess.ts +++ b/server/middlewares/verifySiteProvisioningKeyAccess.ts @@ -4,6 +4,7 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; +import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; export async function verifySiteProvisioningKeyAccess( req: Request, @@ -116,8 +117,11 @@ export async function verifySiteProvisioningKeyAccess( } } - const userOrgRoleId = req.userOrg.roleId; - req.userOrgRoleId = userOrgRoleId; + req.userOrgRoleIds = await getUserOrgRoleIds( + req.userOrg.userId, + row.siteProvisioningKeyOrg.orgId + ); + req.userOrgId = row.siteProvisioningKeyOrg.orgId; return next(); } catch (error) { diff --git a/server/private/cleanup.ts b/server/private/cleanup.ts index 17d823491..af4238721 100644 --- a/server/private/cleanup.ts +++ b/server/private/cleanup.ts @@ -12,9 +12,10 @@ */ import { rateLimitService } from "#private/lib/rateLimit"; +import { logStreamingManager } from "#private/lib/logStreaming"; import { cleanup as wsCleanup } from "#private/routers/ws"; import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage"; -import { flushConnectionLogToDb } from "#dynamic/routers/newt"; +import { flushConnectionLogToDb } from "#private/routers/newt"; import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth"; import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator"; @@ -25,6 +26,7 @@ async function cleanup() { await flushSiteBandwidthToDb(); await rateLimitService.cleanup(); await wsCleanup(); + await logStreamingManager.shutdown(); process.exit(0); } diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 91db548f7..e56490795 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -74,6 +74,7 @@ export async function logAccessAudit(data: { type: string; orgId: string; resourceId?: number; + siteResourceId?: number; user?: { username: string; userId: string }; apiKey?: { name: string | null; apiKeyId: string }; metadata?: any; @@ -134,6 +135,7 @@ export async function logAccessAudit(data: { type: data.type, metadata, resourceId: data.resourceId, + siteResourceId: data.siteResourceId, userAgent: data.userAgent, ip: clientIp, location: countryCode diff --git a/server/private/lib/logConnectionAudit.ts b/server/private/lib/logConnectionAudit.ts new file mode 100644 index 000000000..c7e786280 --- /dev/null +++ b/server/private/lib/logConnectionAudit.ts @@ -0,0 +1,234 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { logsDb, connectionAuditLog } from "@server/db"; +import logger from "@server/logger"; +import { and, eq, lt } from "drizzle-orm"; +import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; + +// --------------------------------------------------------------------------- +// Retry configuration for deadlock handling +// --------------------------------------------------------------------------- + +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 50; + +// --------------------------------------------------------------------------- +// Buffer / flush configuration +// --------------------------------------------------------------------------- + +/** How often to flush accumulated connection log data to the database. */ +const FLUSH_INTERVAL_MS = 30_000; // 30 seconds + +/** Maximum number of records to buffer before forcing a flush. */ +const MAX_BUFFERED_RECORDS = 500; + +/** Maximum number of records to insert in a single database batch. */ +const INSERT_BATCH_SIZE = 100; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface ConnectionLogRecord { + sessionId: string; + siteResourceId: number; + orgId: string; + siteId: number; + clientId: number | null; + userId: string | null; + sourceAddr: string; + destAddr: string; + protocol: string; + startedAt: number; // epoch seconds + endedAt: number | null; + bytesTx: number | null; + bytesRx: number | null; +} + +// --------------------------------------------------------------------------- +// In-memory buffer +// --------------------------------------------------------------------------- + +let buffer: ConnectionLogRecord[] = []; + +// --------------------------------------------------------------------------- +// Deadlock helpers +// --------------------------------------------------------------------------- + +function isDeadlockError(error: any): boolean { + return ( + error?.code === "40P01" || + error?.cause?.code === "40P01" || + (error?.message && error.message.includes("deadlock")) + ); +} + +async function withDeadlockRetry( + operation: () => Promise, + context: string +): Promise { + let attempt = 0; + while (true) { + try { + return await operation(); + } catch (error: any) { + if (isDeadlockError(error) && attempt < MAX_RETRIES) { + attempt++; + const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS; + const jitter = Math.random() * baseDelay; + const delay = baseDelay + jitter; + logger.warn( + `Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms` + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + throw error; + } + } +} + +// --------------------------------------------------------------------------- +// Flush +// --------------------------------------------------------------------------- + +/** + * Flush all buffered connection log records to the database. + * + * Swaps out the buffer before writing so that any records added during the + * flush are captured in the new buffer rather than being lost. Entries that + * fail to write are re-queued back into the buffer so they will be retried + * on the next flush. + * + * This function is exported so that the application's graceful-shutdown + * cleanup handler can call it before the process exits. + */ +export async function flushConnectionLogToDb(): Promise { + if (buffer.length === 0) { + return; + } + + // Atomically swap out the buffer so new data keeps flowing in + const snapshot = buffer; + buffer = []; + + logger.debug( + `Flushing ${snapshot.length} connection log record(s) to the database` + ); + + for (let i = 0; i < snapshot.length; i += INSERT_BATCH_SIZE) { + const batch = snapshot.slice(i, i + INSERT_BATCH_SIZE); + + try { + await withDeadlockRetry(async () => { + await logsDb.insert(connectionAuditLog).values(batch); + }, `flush connection log batch (${batch.length} records)`); + } catch (error) { + logger.error( + `Failed to flush connection log batch of ${batch.length} records:`, + error + ); + + // Re-queue the failed batch so it is retried on the next flush + buffer = [...batch, ...buffer]; + + // Cap buffer to prevent unbounded growth if the DB is unreachable + const hardLimit = MAX_BUFFERED_RECORDS * 5; + if (buffer.length > hardLimit) { + const dropped = buffer.length - hardLimit; + buffer = buffer.slice(0, hardLimit); + logger.warn( + `Connection log buffer overflow, dropped ${dropped} oldest records` + ); + } + + // Stop processing further batches from this snapshot — they will + // be picked up via the re-queued records on the next flush. + const remaining = snapshot.slice(i + INSERT_BATCH_SIZE); + if (remaining.length > 0) { + buffer = [...remaining, ...buffer]; + } + break; + } + } +} + +// --------------------------------------------------------------------------- +// Periodic flush timer +// --------------------------------------------------------------------------- + +const flushTimer = setInterval(async () => { + try { + await flushConnectionLogToDb(); + } catch (error) { + logger.error( + "Unexpected error during periodic connection log flush:", + error + ); + } +}, FLUSH_INTERVAL_MS); + +// Calling unref() means this timer will not keep the Node.js event loop alive +// on its own — the process can still exit normally when there is no other work +// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly +// before process.exit(), so no data is lost. +flushTimer.unref(); + +// --------------------------------------------------------------------------- +// Cleanup +// --------------------------------------------------------------------------- + +export async function cleanUpOldLogs( + orgId: string, + retentionDays: number +): Promise { + const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); + + try { + await logsDb + .delete(connectionAuditLog) + .where( + and( + lt(connectionAuditLog.startedAt, cutoffTimestamp), + eq(connectionAuditLog.orgId, orgId) + ) + ); + } catch (error) { + logger.error("Error cleaning up old connection audit logs:", error); + } +} + +// --------------------------------------------------------------------------- +// Public logging entry-point +// --------------------------------------------------------------------------- + +/** + * Buffer a single connection log record for eventual persistence. + * + * Records are written to the database in batches either when the buffer + * reaches MAX_BUFFERED_RECORDS or when the periodic flush timer fires. + */ +export function logConnectionAudit(record: ConnectionLogRecord): void { + buffer.push(record); + + if (buffer.length >= MAX_BUFFERED_RECORDS) { + // Fire and forget — errors are handled inside flushConnectionLogToDb + flushConnectionLogToDb().catch((error) => { + logger.error( + "Unexpected error during size-triggered connection log flush:", + error + ); + }); + } +} \ No newline at end of file diff --git a/server/private/lib/logStreaming/LogStreamingManager.ts b/server/private/lib/logStreaming/LogStreamingManager.ts new file mode 100644 index 000000000..39eae031a --- /dev/null +++ b/server/private/lib/logStreaming/LogStreamingManager.ts @@ -0,0 +1,776 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { + db, + logsDb, + eventStreamingDestinations, + eventStreamingCursors, + requestAuditLog, + actionAuditLog, + accessAuditLog, + connectionAuditLog +} from "@server/db"; +import logger from "@server/logger"; +import { and, eq, gt, desc, max, sql } from "drizzle-orm"; +import { decrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; +import { + LogType, + LOG_TYPES, + LogEvent, + DestinationFailureState, + HttpConfig +} from "./types"; +import { LogDestinationProvider } from "./providers/LogDestinationProvider"; +import { HttpLogDestination } from "./providers/HttpLogDestination"; +import type { EventStreamingDestination } from "@server/db"; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +/** + * How often (ms) the manager polls all destinations for new log records. + * Destinations that were behind (full batch returned) will be re-polled + * immediately without waiting for this interval. + */ +const POLL_INTERVAL_MS = 30_000; + +/** + * Maximum number of log records fetched from the DB in a single query. + * This also controls the maximum size of one HTTP POST body. + */ +const BATCH_SIZE = 250; + +/** + * Minimum delay (ms) between consecutive HTTP requests to the same destination + * during a catch-up run. Prevents bursting thousands of requests back-to-back + * when a destination has fallen behind. + */ +const INTER_BATCH_DELAY_MS = 100; + +/** + * Maximum number of consecutive back-to-back batches to process for a single + * destination per poll cycle. After this limit the destination will wait for + * the next scheduled poll before continuing, giving other destinations a turn. + */ +const MAX_CATCHUP_BATCHES = 20; + +/** + * Back-off schedule (ms) indexed by consecutive failure count. + * After the last entry the max value is re-used. + */ +const BACKOFF_SCHEDULE_MS = [ + 60_000, // 1 min (failure 1) + 2 * 60_000, // 2 min (failure 2) + 5 * 60_000, // 5 min (failure 3) + 10 * 60_000, // 10 min (failure 4) + 30 * 60_000 // 30 min (failure 5+) +]; + +/** + * If a destination has been continuously unreachable for this long, its + * cursors are advanced to the current max row id and the backlog is silently + * discarded. This prevents unbounded queue growth when a webhook endpoint is + * down for an extended period. A prominent warning is logged so operators are + * aware logs were dropped. + * + * Default: 24 hours. + */ +const MAX_BACKLOG_DURATION_MS = 24 * 60 * 60_000; + +// --------------------------------------------------------------------------- +// LogStreamingManager +// --------------------------------------------------------------------------- + +/** + * Orchestrates periodic polling of the four audit-log tables and forwards new + * records to every enabled event-streaming destination. + * + * ### Design + * - **Interval-based**: a timer fires every `POLL_INTERVAL_MS`. On each tick + * every enabled destination is processed in sequence. + * - **Cursor-based**: the last successfully forwarded row `id` is persisted in + * the `eventStreamingCursors` table so state survives restarts. + * - **Catch-up**: if a full batch is returned the destination is immediately + * re-queried (up to `MAX_CATCHUP_BATCHES` times) before yielding. + * - **Smoothing**: `INTER_BATCH_DELAY_MS` is inserted between consecutive + * catch-up batches to avoid hammering the remote endpoint. + * - **Back-off**: consecutive send failures trigger exponential back-off + * (tracked in-memory per destination). Successful sends reset the counter. + * - **Backlog abandonment**: if a destination remains unreachable for longer + * than `MAX_BACKLOG_DURATION_MS`, all cursors for that destination are + * advanced to the current max id so the backlog is discarded and streaming + * resumes from the present moment on recovery. + */ +export class LogStreamingManager { + private pollTimer: ReturnType | null = null; + private isRunning = false; + private isPolling = false; + + /** In-memory back-off state keyed by destinationId. */ + private readonly failures = new Map(); + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + start(): void { + if (this.isRunning) return; + this.isRunning = true; + logger.debug("LogStreamingManager: started"); + this.schedulePoll(POLL_INTERVAL_MS); + } + + // ------------------------------------------------------------------------- + // Cursor initialisation (call this when a destination is first created) + // ------------------------------------------------------------------------- + + /** + * Eagerly seed cursors for every log type at the **current** max row id of + * each table, scoped to the destination's org. + * + * Call this immediately after inserting a new row into + * `eventStreamingDestinations` so the destination only receives events + * that were written *after* it was created. If a cursor row already exists + * (e.g. the method is called twice) it is left untouched. + * + * The manager also has a lazy fallback inside `getOrCreateCursor` for + * destinations that existed before this method was introduced. + */ + async initializeCursorsForDestination( + destinationId: number, + orgId: string + ): Promise { + for (const logType of LOG_TYPES) { + const currentMaxId = await this.getCurrentMaxId(logType, orgId); + try { + await db + .insert(eventStreamingCursors) + .values({ + destinationId, + logType, + lastSentId: currentMaxId, + lastSentAt: null + }) + .onConflictDoNothing(); + } catch (err) { + logger.warn( + `LogStreamingManager: could not initialise cursor for ` + + `destination ${destinationId} logType="${logType}"`, + err + ); + } + } + + logger.debug( + `LogStreamingManager: cursors initialised for destination ${destinationId} ` + + `(org=${orgId})` + ); + } + + async shutdown(): Promise { + this.isRunning = false; + if (this.pollTimer !== null) { + clearTimeout(this.pollTimer); + this.pollTimer = null; + } + // Wait for any in-progress poll to finish before returning so that + // callers (graceful-shutdown handlers) can safely exit afterward. + const deadline = Date.now() + 15_000; + while (this.isPolling && Date.now() < deadline) { + await sleep(100); + } + logger.info("LogStreamingManager: stopped"); + } + + // ------------------------------------------------------------------------- + // Scheduling + // ------------------------------------------------------------------------- + + private schedulePoll(delayMs: number): void { + this.pollTimer = setTimeout(() => { + this.pollTimer = null; + this.runPoll() + .catch((err) => + logger.error("LogStreamingManager: unexpected poll error", err) + ) + .finally(() => { + if (this.isRunning) { + this.schedulePoll(POLL_INTERVAL_MS); + } + }); + }, delayMs); + + // Do not keep the event loop alive just for the poll timer – the + // graceful-shutdown path calls shutdown() explicitly. + this.pollTimer.unref?.(); + } + + // ------------------------------------------------------------------------- + // Poll cycle + // ------------------------------------------------------------------------- + + private async runPoll(): Promise { + if (this.isPolling) return; // previous poll still running – skip + this.isPolling = true; + + try { + const destinations = await this.loadEnabledDestinations(); + if (destinations.length === 0) return; + + for (const dest of destinations) { + if (!this.isRunning) break; + await this.processDestination(dest).catch((err) => { + // Individual destination errors must never abort the whole cycle + logger.error( + `LogStreamingManager: unhandled error for destination ${dest.destinationId}`, + err + ); + }); + } + } finally { + this.isPolling = false; + } + } + + // ------------------------------------------------------------------------- + // Per-destination processing + // ------------------------------------------------------------------------- + + private async processDestination( + dest: EventStreamingDestination + ): Promise { + const failState = this.failures.get(dest.destinationId); + + // Check whether this destination has been unreachable long enough that + // we should give up on the accumulated backlog. + if (failState) { + const failingForMs = Date.now() - failState.firstFailedAt; + if (failingForMs >= MAX_BACKLOG_DURATION_MS) { + await this.abandonBacklog(dest, failState); + this.failures.delete(dest.destinationId); + // Cursors now point to the current head – retry on next poll. + return; + } + } + + // Check regular exponential back-off window + if (failState && Date.now() < failState.nextRetryAt) { + logger.debug( + `LogStreamingManager: destination ${dest.destinationId} in back-off, skipping` + ); + return; + } + + // Decrypt and parse config – skip destination if either step fails + let configFromDb: HttpConfig; + try { + const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!); + configFromDb = JSON.parse(decryptedConfig) as HttpConfig; + } catch (err) { + logger.error( + `LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`, + err + ); + return; + } + + const provider = this.createProvider(dest.type, configFromDb); + if (!provider) { + logger.warn( + `LogStreamingManager: unsupported destination type "${dest.type}" ` + + `for destination ${dest.destinationId} – skipping` + ); + return; + } + + const enabledTypes: LogType[] = []; + if (dest.sendRequestLogs) enabledTypes.push("request"); + if (dest.sendActionLogs) enabledTypes.push("action"); + if (dest.sendAccessLogs) enabledTypes.push("access"); + if (dest.sendConnectionLogs) enabledTypes.push("connection"); + + if (enabledTypes.length === 0) return; + + let anyFailure = false; + + for (const logType of enabledTypes) { + if (!this.isRunning) break; + try { + await this.processLogType(dest, provider, logType); + } catch (err) { + anyFailure = true; + logger.error( + `LogStreamingManager: failed to process "${logType}" logs ` + + `for destination ${dest.destinationId}`, + err + ); + } + } + + if (anyFailure) { + this.recordFailure(dest.destinationId); + } else { + // Any success resets the failure/back-off state + if (this.failures.has(dest.destinationId)) { + this.failures.delete(dest.destinationId); + logger.info( + `LogStreamingManager: destination ${dest.destinationId} recovered` + ); + } + } + } + + /** + * Advance every cursor for the destination to the current max row id, + * effectively discarding the accumulated backlog. Called when the + * destination has been unreachable for longer than MAX_BACKLOG_DURATION_MS. + */ + private async abandonBacklog( + dest: EventStreamingDestination, + failState: DestinationFailureState + ): Promise { + const failingForHours = ( + (Date.now() - failState.firstFailedAt) / + 3_600_000 + ).toFixed(1); + + let totalDropped = 0; + + for (const logType of LOG_TYPES) { + try { + const currentMaxId = await this.getCurrentMaxId( + logType, + dest.orgId + ); + + // Find out how many rows are being skipped for this type + const cursor = await db + .select({ lastSentId: eventStreamingCursors.lastSentId }) + .from(eventStreamingCursors) + .where( + and( + eq(eventStreamingCursors.destinationId, dest.destinationId), + eq(eventStreamingCursors.logType, logType) + ) + ) + .limit(1); + + const prevId = cursor[0]?.lastSentId ?? currentMaxId; + totalDropped += Math.max(0, currentMaxId - prevId); + + await this.updateCursor( + dest.destinationId, + logType, + currentMaxId + ); + } catch (err) { + logger.error( + `LogStreamingManager: failed to advance cursor for ` + + `destination ${dest.destinationId} logType="${logType}" ` + + `during backlog abandonment`, + err + ); + } + } + + logger.warn( + `LogStreamingManager: destination ${dest.destinationId} has been ` + + `unreachable for ${failingForHours}h ` + + `(${failState.consecutiveFailures} consecutive failures). ` + + `Discarding backlog of ~${totalDropped} log event(s) and ` + + `resuming from the current position. ` + + `Verify the destination URL and credentials.` + ); + } + + /** + * Forward all pending log records of a specific type for a destination. + * + * Fetches up to `BATCH_SIZE` records at a time. If the batch is full + * (indicating more records may exist) it loops immediately, inserting a + * short delay between consecutive requests to the remote endpoint. + * The loop is capped at `MAX_CATCHUP_BATCHES` to keep the poll cycle + * bounded. + */ + private async processLogType( + dest: EventStreamingDestination, + provider: LogDestinationProvider, + logType: LogType + ): Promise { + // Ensure a cursor row exists (creates one pointing at the current max + // id so we do not replay historical logs on first run) + const cursor = await this.getOrCreateCursor( + dest.destinationId, + logType, + dest.orgId + ); + + let lastSentId = cursor.lastSentId; + let batchCount = 0; + + while (batchCount < MAX_CATCHUP_BATCHES) { + const rows = await this.fetchLogs( + logType, + dest.orgId, + lastSentId, + BATCH_SIZE + ); + + if (rows.length === 0) break; + + const events = rows.map((row) => + this.rowToLogEvent(logType, row) + ); + + // Throws on failure – caught by the caller which applies back-off + await provider.send(events); + + lastSentId = rows[rows.length - 1].id; + await this.updateCursor(dest.destinationId, logType, lastSentId); + + batchCount++; + + if (rows.length < BATCH_SIZE) { + // Partial batch means we have caught up + break; + } + + // Full batch – there are likely more records; pause briefly before + // fetching the next batch to smooth out the HTTP request rate + if (batchCount < MAX_CATCHUP_BATCHES) { + await sleep(INTER_BATCH_DELAY_MS); + } + } + } + + // ------------------------------------------------------------------------- + // Cursor management + // ------------------------------------------------------------------------- + + private async getOrCreateCursor( + destinationId: number, + logType: LogType, + orgId: string + ): Promise<{ lastSentId: number }> { + // Try to read an existing cursor + const existing = await db + .select({ + lastSentId: eventStreamingCursors.lastSentId + }) + .from(eventStreamingCursors) + .where( + and( + eq(eventStreamingCursors.destinationId, destinationId), + eq(eventStreamingCursors.logType, logType) + ) + ) + .limit(1); + + if (existing.length > 0) { + return { lastSentId: existing[0].lastSentId }; + } + + // No cursor yet – this destination pre-dates the eager initialisation + // path (initializeCursorsForDestination). Seed at the current max id + // so we do not replay historical logs. + const initialId = await this.getCurrentMaxId(logType, orgId); + + // Use onConflictDoNothing in case of a rare race between two poll + // cycles both hitting this branch simultaneously. + await db + .insert(eventStreamingCursors) + .values({ + destinationId, + logType, + lastSentId: initialId, + lastSentAt: null + }) + .onConflictDoNothing(); + + logger.debug( + `LogStreamingManager: lazily initialised cursor for destination ${destinationId} ` + + `logType="${logType}" at id=${initialId} ` + + `(prefer initializeCursorsForDestination at creation time)` + ); + + return { lastSentId: initialId }; + } + + private async updateCursor( + destinationId: number, + logType: LogType, + lastSentId: number + ): Promise { + await db + .update(eventStreamingCursors) + .set({ + lastSentId, + lastSentAt: Date.now() + }) + .where( + and( + eq(eventStreamingCursors.destinationId, destinationId), + eq(eventStreamingCursors.logType, logType) + ) + ); + } + + /** + * Returns the current maximum `id` in the given log table for the org. + * Returns 0 when the table is empty. + */ + private async getCurrentMaxId( + logType: LogType, + orgId: string + ): Promise { + try { + switch (logType) { + case "request": { + const [row] = await logsDb + .select({ maxId: max(requestAuditLog.id) }) + .from(requestAuditLog) + .where(eq(requestAuditLog.orgId, orgId)); + return row?.maxId ?? 0; + } + case "action": { + const [row] = await logsDb + .select({ maxId: max(actionAuditLog.id) }) + .from(actionAuditLog) + .where(eq(actionAuditLog.orgId, orgId)); + return row?.maxId ?? 0; + } + case "access": { + const [row] = await logsDb + .select({ maxId: max(accessAuditLog.id) }) + .from(accessAuditLog) + .where(eq(accessAuditLog.orgId, orgId)); + return row?.maxId ?? 0; + } + case "connection": { + const [row] = await logsDb + .select({ maxId: max(connectionAuditLog.id) }) + .from(connectionAuditLog) + .where(eq(connectionAuditLog.orgId, orgId)); + return row?.maxId ?? 0; + } + } + } catch (err) { + logger.warn( + `LogStreamingManager: could not determine current max id for ` + + `logType="${logType}", defaulting to 0`, + err + ); + return 0; + } + } + + // ------------------------------------------------------------------------- + // Log fetching + // ------------------------------------------------------------------------- + + /** + * Fetch up to `limit` log rows with `id > afterId`, ordered by id ASC, + * filtered to the given organisation. + */ + private async fetchLogs( + logType: LogType, + orgId: string, + afterId: number, + limit: number + ): Promise & { id: number }>> { + switch (logType) { + case "request": + return (await logsDb + .select() + .from(requestAuditLog) + .where( + and( + eq(requestAuditLog.orgId, orgId), + gt(requestAuditLog.id, afterId) + ) + ) + .orderBy(requestAuditLog.id) + .limit(limit)) as Array< + Record & { id: number } + >; + + case "action": + return (await logsDb + .select() + .from(actionAuditLog) + .where( + and( + eq(actionAuditLog.orgId, orgId), + gt(actionAuditLog.id, afterId) + ) + ) + .orderBy(actionAuditLog.id) + .limit(limit)) as Array< + Record & { id: number } + >; + + case "access": + return (await logsDb + .select() + .from(accessAuditLog) + .where( + and( + eq(accessAuditLog.orgId, orgId), + gt(accessAuditLog.id, afterId) + ) + ) + .orderBy(accessAuditLog.id) + .limit(limit)) as Array< + Record & { id: number } + >; + + case "connection": + return (await logsDb + .select() + .from(connectionAuditLog) + .where( + and( + eq(connectionAuditLog.orgId, orgId), + gt(connectionAuditLog.id, afterId) + ) + ) + .orderBy(connectionAuditLog.id) + .limit(limit)) as Array< + Record & { id: number } + >; + } + } + + // ------------------------------------------------------------------------- + // Row → LogEvent conversion + // ------------------------------------------------------------------------- + + private rowToLogEvent( + logType: LogType, + row: Record & { id: number } + ): LogEvent { + // Determine the epoch-seconds timestamp for this row type + let timestamp: number; + switch (logType) { + case "request": + case "action": + case "access": + timestamp = + typeof row.timestamp === "number" ? row.timestamp : 0; + break; + case "connection": + timestamp = + typeof row.startedAt === "number" ? row.startedAt : 0; + break; + } + + const orgId = + typeof row.orgId === "string" ? row.orgId : ""; + + return { + id: row.id, + logType, + orgId, + timestamp, + data: row as Record + }; + } + + // ------------------------------------------------------------------------- + // Provider factory + // ------------------------------------------------------------------------- + + /** + * Instantiate the correct LogDestinationProvider for the given destination + * type string. Returns `null` for unknown types. + * + * To add a new provider: + * 1. Implement `LogDestinationProvider` in a new file under `providers/` + * 2. Add a `case` here + */ + private createProvider( + type: string, + config: unknown + ): LogDestinationProvider | null { + switch (type) { + case "http": + return new HttpLogDestination(config as HttpConfig); + // Future providers: + // case "datadog": return new DatadogLogDestination(config as DatadogConfig); + default: + return null; + } + } + + // ------------------------------------------------------------------------- + // Back-off tracking + // ------------------------------------------------------------------------- + + private recordFailure(destinationId: number): void { + const current = this.failures.get(destinationId) ?? { + consecutiveFailures: 0, + nextRetryAt: 0, + // Stamp the very first failure so we can measure total outage duration + firstFailedAt: Date.now() + }; + + current.consecutiveFailures += 1; + + const scheduleIdx = Math.min( + current.consecutiveFailures - 1, + BACKOFF_SCHEDULE_MS.length - 1 + ); + const backoffMs = BACKOFF_SCHEDULE_MS[scheduleIdx]; + current.nextRetryAt = Date.now() + backoffMs; + + this.failures.set(destinationId, current); + + logger.warn( + `LogStreamingManager: destination ${destinationId} failed ` + + `(consecutive #${current.consecutiveFailures}), ` + + `backing off for ${backoffMs / 1000}s` + ); + } + + // ------------------------------------------------------------------------- + // DB helpers + // ------------------------------------------------------------------------- + + private async loadEnabledDestinations(): Promise< + EventStreamingDestination[] + > { + try { + return await db + .select() + .from(eventStreamingDestinations) + .where(eq(eventStreamingDestinations.enabled, true)); + } catch (err) { + logger.error( + "LogStreamingManager: failed to load destinations", + err + ); + return []; + } + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/server/private/lib/logStreaming/index.ts b/server/private/lib/logStreaming/index.ts new file mode 100644 index 000000000..619809771 --- /dev/null +++ b/server/private/lib/logStreaming/index.ts @@ -0,0 +1,34 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { build } from "@server/build"; +import { LogStreamingManager } from "./LogStreamingManager"; + +/** + * Module-level singleton. Importing this module is sufficient to start the + * streaming manager – no explicit init call required by the caller. + * + * The manager registers a non-blocking timer (unref'd) so it will not keep + * the Node.js event loop alive on its own. Call `logStreamingManager.shutdown()` + * during graceful shutdown to drain any in-progress poll and release resources. + */ +export const logStreamingManager = new LogStreamingManager(); + +if (build != "saas") { // this is handled separately in the saas build, so we don't want to start it here + logStreamingManager.start(); +} + +export { LogStreamingManager } from "./LogStreamingManager"; +export type { LogDestinationProvider } from "./providers/LogDestinationProvider"; +export { HttpLogDestination } from "./providers/HttpLogDestination"; +export * from "./types"; diff --git a/server/private/lib/logStreaming/providers/HttpLogDestination.ts b/server/private/lib/logStreaming/providers/HttpLogDestination.ts new file mode 100644 index 000000000..5e149f814 --- /dev/null +++ b/server/private/lib/logStreaming/providers/HttpLogDestination.ts @@ -0,0 +1,322 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import logger from "@server/logger"; +import { LogEvent, HttpConfig, PayloadFormat } from "../types"; +import { LogDestinationProvider } from "./LogDestinationProvider"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/** Maximum time (ms) to wait for a single HTTP response. */ +const REQUEST_TIMEOUT_MS = 30_000; + +/** Default payload format when none is specified in the config. */ +const DEFAULT_FORMAT: PayloadFormat = "json_array"; + +// --------------------------------------------------------------------------- +// HttpLogDestination +// --------------------------------------------------------------------------- + +/** + * Forwards a batch of log events to an arbitrary HTTP endpoint via a single + * POST request per batch. + * + * **Payload format** + * + * **Payload formats** (controlled by `config.format`): + * + * - `json_array` (default) — one POST per batch, body is a JSON array: + * ```json + * [ + * { "event": "request", "timestamp": "2024-01-01T00:00:00.000Z", "data": { … } }, + * … + * ] + * ``` + * `Content-Type: application/json` + * + * - `ndjson` — one POST per batch, body is newline-delimited JSON (one object + * per line, no outer array). Required by Splunk HEC, Elastic/OpenSearch, + * and Grafana Loki: + * ``` + * {"event":"request","timestamp":"…","data":{…}} + * {"event":"action","timestamp":"…","data":{…}} + * ``` + * `Content-Type: application/x-ndjson` + * + * - `json_single` — one POST **per event**, body is a plain JSON object. + * Use only for endpoints that cannot handle batches at all. + * + * With a body template each event is rendered through the template before + * serialisation. Template placeholders: + * - `{{event}}` → the LogType string ("request", "action", etc.) + * - `{{timestamp}}` → ISO-8601 UTC datetime string + * - `{{data}}` → raw inline JSON object (**no surrounding quotes**) + * + * Example template: + * ``` + * { "event": "{{event}}", "ts": "{{timestamp}}", "payload": {{data}} } + * ``` + */ +export class HttpLogDestination implements LogDestinationProvider { + readonly type = "http"; + + private readonly config: HttpConfig; + + constructor(config: HttpConfig) { + this.config = config; + } + + // ----------------------------------------------------------------------- + // LogDestinationProvider implementation + // ----------------------------------------------------------------------- + + async send(events: LogEvent[]): Promise { + if (events.length === 0) return; + + const format = this.config.format ?? DEFAULT_FORMAT; + + if (format === "json_single") { + // One HTTP POST per event – send sequentially so a failure on one + // event throws and lets the manager retry the whole batch from the + // same cursor position. + for (const event of events) { + await this.postRequest( + this.buildSingleBody(event), + "application/json" + ); + } + return; + } + + if (format === "ndjson") { + const body = this.buildNdjsonBody(events); + await this.postRequest(body, "application/x-ndjson"); + return; + } + + // json_array (default) + const body = JSON.stringify(this.buildArrayPayload(events)); + await this.postRequest(body, "application/json"); + } + + // ----------------------------------------------------------------------- + // Internal HTTP sender + // ----------------------------------------------------------------------- + + private async postRequest( + body: string, + contentType: string + ): Promise { + const headers = this.buildHeaders(contentType); + + const controller = new AbortController(); + const timeoutHandle = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS + ); + + let response: Response; + try { + response = await fetch(this.config.url, { + method: "POST", + headers, + body, + signal: controller.signal + }); + } catch (err: unknown) { + const isAbort = + err instanceof Error && err.name === "AbortError"; + if (isAbort) { + throw new Error( + `HttpLogDestination: request to "${this.config.url}" timed out after ${REQUEST_TIMEOUT_MS} ms` + ); + } + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `HttpLogDestination: request to "${this.config.url}" failed – ${msg}` + ); + } finally { + clearTimeout(timeoutHandle); + } + + if (!response.ok) { + // Try to include a snippet of the response body in the error so + // operators can diagnose auth or schema rejections. + let responseSnippet = ""; + try { + const text = await response.text(); + responseSnippet = text.slice(0, 300); + } catch { + // ignore – best effort + } + + throw new Error( + `HttpLogDestination: server at "${this.config.url}" returned ` + + `HTTP ${response.status} ${response.statusText}` + + (responseSnippet ? ` – ${responseSnippet}` : "") + ); + } + } + + // ----------------------------------------------------------------------- + // Header construction + // ----------------------------------------------------------------------- + + private buildHeaders(contentType: string): Record { + const headers: Record = { + "Content-Type": contentType + }; + + // Authentication + switch (this.config.authType) { + case "bearer": { + const token = this.config.bearerToken?.trim(); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + break; + } + case "basic": { + const creds = this.config.basicCredentials?.trim(); + if (creds) { + const encoded = Buffer.from(creds).toString("base64"); + headers["Authorization"] = `Basic ${encoded}`; + } + break; + } + case "custom": { + const name = this.config.customHeaderName?.trim(); + const value = this.config.customHeaderValue ?? ""; + if (name) { + headers[name] = value; + } + break; + } + case "none": + default: + // No Authorization header + break; + } + + // Additional static headers (user-defined; may override Content-Type + // if the operator explicitly sets it, which is intentional). + for (const { key, value } of this.config.headers ?? []) { + const trimmedKey = key?.trim(); + if (trimmedKey) { + headers[trimmedKey] = value ?? ""; + } + } + + return headers; + } + + // ----------------------------------------------------------------------- + // Payload construction + // ----------------------------------------------------------------------- + + /** Single default event object (no surrounding array). */ + private buildEventObject(event: LogEvent): unknown { + if (this.config.useBodyTemplate && this.config.bodyTemplate?.trim()) { + return this.renderTemplate(this.config.bodyTemplate!, event); + } + return { + event: event.logType, + timestamp: epochSecondsToIso(event.timestamp), + data: event.data + }; + } + + /** JSON array payload – used for `json_array` format. */ + private buildArrayPayload(events: LogEvent[]): unknown[] { + return events.map((e) => this.buildEventObject(e)); + } + + /** + * NDJSON payload – one JSON object per line, no outer array. + * Each line must be a complete, valid JSON object. + */ + private buildNdjsonBody(events: LogEvent[]): string { + return events + .map((e) => JSON.stringify(this.buildEventObject(e))) + .join("\n"); + } + + /** Single-event body – used for `json_single` format. */ + private buildSingleBody(event: LogEvent): string { + return JSON.stringify(this.buildEventObject(event)); + } + + /** + * Render a single event through the body template. + * + * The three placeholder tokens are replaced in a specific order to avoid + * accidental double-replacement: + * + * 1. `{{data}}` → raw JSON (may contain `{{` characters in values) + * 2. `{{event}}` → safe string + * 3. `{{timestamp}}` → safe ISO string + * + * If the rendered string is not valid JSON we fall back to returning it as + * a plain string so the batch still makes it out and the operator can + * inspect the template. + */ + private renderTemplate(template: string, event: LogEvent): unknown { + const isoTimestamp = epochSecondsToIso(event.timestamp); + const dataJson = JSON.stringify(event.data); + + // Replace {{data}} first because its JSON value might legitimately + // contain the substrings "{{event}}" or "{{timestamp}}" inside string + // fields – those should NOT be re-expanded. + const rendered = template + .replace(/\{\{data\}\}/g, dataJson) + .replace(/\{\{event\}\}/g, escapeJsonString(event.logType)) + .replace( + /\{\{timestamp\}\}/g, + escapeJsonString(isoTimestamp) + ); + + try { + return JSON.parse(rendered); + } catch { + logger.warn( + `HttpLogDestination: body template produced invalid JSON for ` + + `event type "${event.logType}" destined for "${this.config.url}". ` + + `Sending rendered template as a raw string. ` + + `Check your template syntax – specifically that {{data}} is ` + + `NOT wrapped in quotes.` + ); + return rendered; + } + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function epochSecondsToIso(epochSeconds: number): string { + return new Date(epochSeconds * 1000).toISOString(); +} + +/** + * Escape a string value so it can be safely substituted into the interior of + * a JSON string literal (i.e. between existing `"` quotes in the template). + * This prevents a crafted logType or timestamp from breaking out of its + * string context in the rendered template. + */ +function escapeJsonString(value: string): string { + // JSON.stringify produces `""` – strip the outer quotes. + return JSON.stringify(value).slice(1, -1); +} \ No newline at end of file diff --git a/server/private/lib/logStreaming/providers/LogDestinationProvider.ts b/server/private/lib/logStreaming/providers/LogDestinationProvider.ts new file mode 100644 index 000000000..d09be320b --- /dev/null +++ b/server/private/lib/logStreaming/providers/LogDestinationProvider.ts @@ -0,0 +1,44 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { LogEvent } from "../types"; + +/** + * Common interface that every log-forwarding backend must implement. + * + * Adding a new destination type (e.g. Datadog, Splunk, Kafka) is as simple as + * creating a class that satisfies this interface and registering it inside + * LogStreamingManager.createProvider(). + */ +export interface LogDestinationProvider { + /** + * The string identifier that matches the `type` column in the + * `eventStreamingDestinations` table (e.g. "http", "datadog"). + */ + readonly type: string; + + /** + * Forward a batch of log events to the destination. + * + * Implementations should: + * - Treat the call as atomic: either all events are accepted or an error + * is thrown so the caller can retry / back off. + * - Respect the timeout contract expected by the manager (default 30 s). + * - NOT swallow errors – the manager relies on thrown exceptions to track + * failure state and apply exponential back-off. + * + * @param events A non-empty array of normalised log events to forward. + * @throws Any network, authentication, or serialisation error. + */ + send(events: LogEvent[]): Promise; +} \ No newline at end of file diff --git a/server/private/lib/logStreaming/types.ts b/server/private/lib/logStreaming/types.ts new file mode 100644 index 000000000..03fe88cad --- /dev/null +++ b/server/private/lib/logStreaming/types.ts @@ -0,0 +1,134 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +// --------------------------------------------------------------------------- +// Log type identifiers +// --------------------------------------------------------------------------- + +export type LogType = "request" | "action" | "access" | "connection"; + +export const LOG_TYPES: LogType[] = [ + "request", + "action", + "access", + "connection" +]; + +// --------------------------------------------------------------------------- +// A normalised event ready to be forwarded to a destination +// --------------------------------------------------------------------------- + +export interface LogEvent { + /** The auto-increment primary key from the source table */ + id: number; + /** Which log table this event came from */ + logType: LogType; + /** The organisation that owns this event */ + orgId: string; + /** Unix epoch seconds – taken from the record's own timestamp field */ + timestamp: number; + /** Full row data from the source table, serialised as a plain object */ + data: Record; +} + +// --------------------------------------------------------------------------- +// A batch of events destined for a single streaming target +// --------------------------------------------------------------------------- + +export interface LogBatch { + destinationId: number; + logType: LogType; + events: LogEvent[]; +} + +// --------------------------------------------------------------------------- +// HTTP destination configuration (mirrors HttpConfig in the UI component) +// --------------------------------------------------------------------------- + +export type AuthType = "none" | "bearer" | "basic" | "custom"; + +/** + * Controls how the batch of events is serialised into the HTTP request body. + * + * - `json_array` – `[{…}, {…}]` — default; one POST per batch wrapped in a + * JSON array. Works with most generic webhooks and Datadog. + * - `ndjson` – `{…}\n{…}` — newline-delimited JSON, one object per + * line. Required by Splunk HEC, Elastic/OpenSearch, Loki. + * - `json_single` – one HTTP POST per event, body is a plain JSON object. + * Use only for endpoints that cannot handle batches at all. + */ +export type PayloadFormat = "json_array" | "ndjson" | "json_single"; + +export interface HttpConfig { + /** Human-readable label for the destination */ + name: string; + /** Target URL that will receive POST requests */ + url: string; + /** Authentication strategy to use */ + authType: AuthType; + /** Used when authType === "bearer" */ + bearerToken?: string; + /** Used when authType === "basic" – must be "username:password" */ + basicCredentials?: string; + /** Used when authType === "custom" – header name */ + customHeaderName?: string; + /** Used when authType === "custom" – header value */ + customHeaderValue?: string; + /** Additional static headers appended to every request */ + headers: Array<{ key: string; value: string }>; + /** Whether to render a custom body template instead of the default shape */ + /** + * How events are serialised into the request body. + * Defaults to `"json_array"` when absent. + */ + format?: PayloadFormat; + useBodyTemplate: boolean; + /** + * Handlebars-style template for the JSON body of each event. + * + * Supported placeholders: + * {{event}} – the LogType string ("request", "action", etc.) + * {{timestamp}} – ISO-8601 UTC string derived from the event's timestamp + * {{data}} – raw JSON object (no surrounding quotes) of the full row + * + * Example: + * { "event": "{{event}}", "ts": "{{timestamp}}", "payload": {{data}} } + */ + bodyTemplate?: string; +} + +// --------------------------------------------------------------------------- +// Per-destination per-log-type cursor (reflects the DB table) +// --------------------------------------------------------------------------- + +export interface StreamingCursor { + destinationId: number; + logType: LogType; + /** The `id` of the last row that was successfully forwarded */ + lastSentId: number; + /** Epoch milliseconds of the last successful send (or null if never sent) */ + lastSentAt: number | null; +} + +// --------------------------------------------------------------------------- +// In-memory failure / back-off state tracked per destination +// --------------------------------------------------------------------------- + +export interface DestinationFailureState { + /** How many consecutive send failures have occurred */ + consecutiveFailures: number; + /** Date.now() value after which the destination may be retried */ + nextRetryAt: number; + /** Date.now() value of the very first failure in the current streak */ + firstFailedAt: number; +} \ No newline at end of file diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 7fc0ae647..adc3d965b 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -671,10 +671,7 @@ export async function getTraefikConfig( // TODO: HOW TO HANDLE ^^^^^^ BETTER const anySitesOnline = targets.some( - (target) => - target.site.online || - target.site.type === "local" || - target.site.type === "wireguard" + (target) => target.site.online ); return ( @@ -802,10 +799,7 @@ export async function getTraefikConfig( servers: (() => { // Check if any sites are online const anySitesOnline = targets.some( - (target) => - target.site.online || - target.site.type === "local" || - target.site.type === "wireguard" + (target) => target.site.online ); return targets diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index f0f45a826..f9951c1ab 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -11,11 +11,11 @@ * This file is not licensed under the AGPLv3. */ -import { accessAuditLog, logsDb, resources, db, primaryDb } from "@server/db"; +import { accessAuditLog, logsDb, resources, siteResources, db, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; -import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm"; +import { eq, gt, lt, and, count, desc, inArray, isNull } from "drizzle-orm"; import { OpenAPITags } from "@server/openApi"; import { z } from "zod"; import createHttpError from "http-errors"; @@ -122,6 +122,7 @@ export function queryAccess(data: Q) { actorType: accessAuditLog.actorType, actorId: accessAuditLog.actorId, resourceId: accessAuditLog.resourceId, + siteResourceId: accessAuditLog.siteResourceId, ip: accessAuditLog.ip, location: accessAuditLog.location, userAgent: accessAuditLog.userAgent, @@ -136,37 +137,73 @@ export function queryAccess(data: Q) { } async function enrichWithResourceDetails(logs: Awaited>) { - // If logs database is the same as main database, we can do a join - // Otherwise, we need to fetch resource details separately const resourceIds = logs .map(log => log.resourceId) .filter((id): id is number => id !== null && id !== undefined); - if (resourceIds.length === 0) { + const siteResourceIds = logs + .filter(log => log.resourceId == null && log.siteResourceId != null) + .map(log => log.siteResourceId) + .filter((id): id is number => id !== null && id !== undefined); + + if (resourceIds.length === 0 && siteResourceIds.length === 0) { return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null })); } - // Fetch resource details from main database - const resourceDetails = await primaryDb - .select({ - resourceId: resources.resourceId, - name: resources.name, - niceId: resources.niceId - }) - .from(resources) - .where(inArray(resources.resourceId, resourceIds)); + const resourceMap = new Map(); - // Create a map for quick lookup - const resourceMap = new Map( - resourceDetails.map(r => [r.resourceId, { name: r.name, niceId: r.niceId }]) - ); + if (resourceIds.length > 0) { + const resourceDetails = await primaryDb + .select({ + resourceId: resources.resourceId, + name: resources.name, + niceId: resources.niceId + }) + .from(resources) + .where(inArray(resources.resourceId, resourceIds)); + + for (const r of resourceDetails) { + resourceMap.set(r.resourceId, { name: r.name, niceId: r.niceId }); + } + } + + const siteResourceMap = new Map(); + + if (siteResourceIds.length > 0) { + const siteResourceDetails = await primaryDb + .select({ + siteResourceId: siteResources.siteResourceId, + name: siteResources.name, + niceId: siteResources.niceId + }) + .from(siteResources) + .where(inArray(siteResources.siteResourceId, siteResourceIds)); + + for (const r of siteResourceDetails) { + siteResourceMap.set(r.siteResourceId, { name: r.name, niceId: r.niceId }); + } + } // Enrich logs with resource details - return logs.map(log => ({ - ...log, - resourceName: log.resourceId ? resourceMap.get(log.resourceId)?.name ?? null : null, - resourceNiceId: log.resourceId ? resourceMap.get(log.resourceId)?.niceId ?? null : null - })); + return logs.map(log => { + if (log.resourceId != null) { + const details = resourceMap.get(log.resourceId); + return { + ...log, + resourceName: details?.name ?? null, + resourceNiceId: details?.niceId ?? null + }; + } else if (log.siteResourceId != null) { + const details = siteResourceMap.get(log.siteResourceId); + return { + ...log, + resourceId: log.siteResourceId, + resourceName: details?.name ?? null, + resourceNiceId: details?.niceId ?? null + }; + } + return { ...log, resourceName: null, resourceNiceId: null }; + }); } export function countAccessQuery(data: Q) { @@ -212,11 +249,23 @@ async function queryUniqueFilterAttributes( .from(accessAuditLog) .where(baseConditions); + // Get unique siteResources (only for logs where resourceId is null) + const uniqueSiteResources = await logsDb + .selectDistinct({ + id: accessAuditLog.siteResourceId + }) + .from(accessAuditLog) + .where(and(baseConditions, isNull(accessAuditLog.resourceId))); + // Fetch resource names from main database for the unique resource IDs const resourceIds = uniqueResources .map(row => row.id) .filter((id): id is number => id !== null); + const siteResourceIds = uniqueSiteResources + .map(row => row.id) + .filter((id): id is number => id !== null); + let resourcesWithNames: Array<{ id: number; name: string | null }> = []; if (resourceIds.length > 0) { @@ -228,10 +277,31 @@ async function queryUniqueFilterAttributes( .from(resources) .where(inArray(resources.resourceId, resourceIds)); - resourcesWithNames = resourceDetails.map(r => ({ - id: r.resourceId, - name: r.name - })); + resourcesWithNames = [ + ...resourcesWithNames, + ...resourceDetails.map(r => ({ + id: r.resourceId, + name: r.name + })) + ]; + } + + if (siteResourceIds.length > 0) { + const siteResourceDetails = await primaryDb + .select({ + siteResourceId: siteResources.siteResourceId, + name: siteResources.name + }) + .from(siteResources) + .where(inArray(siteResources.siteResourceId, siteResourceIds)); + + resourcesWithNames = [ + ...resourcesWithNames, + ...siteResourceDetails.map(r => ({ + id: r.siteResourceId, + name: r.name + })) + ]; } return { diff --git a/server/private/routers/billing/featureLifecycle.ts b/server/private/routers/billing/featureLifecycle.ts index f6f2d513a..d86e23cf0 100644 --- a/server/private/routers/billing/featureLifecycle.ts +++ b/server/private/routers/billing/featureLifecycle.ts @@ -120,6 +120,18 @@ async function capRetentionDays( ); } + // Cap action log retention if it exceeds the limit + if ( + org.settingsLogRetentionDaysConnection !== null && + org.settingsLogRetentionDaysConnection > maxRetentionDays + ) { + updates.settingsLogRetentionDaysConnection = maxRetentionDays; + needsUpdate = true; + logger.info( + `Capping connection log retention from ${org.settingsLogRetentionDaysConnection} to ${maxRetentionDays} days for org ${orgId}` + ); + } + // Apply updates if needed if (needsUpdate) { await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId)); @@ -262,6 +274,10 @@ async function disableFeature( await disableActionLogs(orgId); break; + case TierFeature.ConnectionLogs: + await disableConnectionLogs(orgId); + break; + case TierFeature.RotateCredentials: await disableRotateCredentials(orgId); break; @@ -458,6 +474,15 @@ async function disableActionLogs(orgId: string): Promise { logger.info(`Disabled action logs for org ${orgId}`); } +async function disableConnectionLogs(orgId: string): Promise { + await db + .update(orgs) + .set({ settingsLogRetentionDaysConnection: 0 }) + .where(eq(orgs.orgId, orgId)); + + logger.info(`Disabled connection logs for org ${orgId}`); +} + async function disableRotateCredentials(orgId: string): Promise {} async function disableMaintencePage(orgId: string): Promise { diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index a40142526..8e87cd769 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -217,7 +217,7 @@ export async function handleSubscriptionCreated( subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] ) { numUsers = 50; - numSites = 50; + numSites = 100; } else { logger.error( `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts index db9a4b46a..0bb7f8704 100644 --- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -22,11 +22,15 @@ import { OpenAPITags, registry } from "@server/openApi"; import { db, domainNamespaces, resources } from "@server/db"; import { inArray } from "drizzle-orm"; import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; +import { build } from "@server/build"; +import { isSubscribed } from "#private/lib/isSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const paramsSchema = z.strictObject({}); const querySchema = z.strictObject({ - subdomain: z.string() + subdomain: z.string(), + // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise }); registry.registerPath({ @@ -58,6 +62,23 @@ export async function checkDomainNamespaceAvailability( } const { subdomain } = parsedQuery.data; + // if ( + // build == "saas" && + // !isSubscribed(orgId!, tierMatrix.domainNamespaces) + // ) { + // // return not available + // return response(res, { + // data: { + // available: false, + // options: [] + // }, + // success: true, + // error: false, + // message: "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.", + // status: HttpCode.OK + // }); + // } + const namespaces = await db.select().from(domainNamespaces); let possibleDomains = namespaces.map((ns) => { const desired = `${subdomain}.${ns.domainNamespaceId}`; diff --git a/server/private/routers/domain/listDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts index 180613a85..5bbd25b1a 100644 --- a/server/private/routers/domain/listDomainNamespaces.ts +++ b/server/private/routers/domain/listDomainNamespaces.ts @@ -22,6 +22,9 @@ import { eq, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { isSubscribed } from "#private/lib/isSubscribed"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const paramsSchema = z.strictObject({}); @@ -37,7 +40,8 @@ const querySchema = z.strictObject({ .optional() .default("0") .transform(Number) - .pipe(z.int().nonnegative()) + .pipe(z.int().nonnegative()), + // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise }); async function query(limit: number, offset: number) { @@ -99,6 +103,26 @@ export async function listDomainNamespaces( ); } + // if ( + // build == "saas" && + // !isSubscribed(orgId!, tierMatrix.domainNamespaces) + // ) { + // return response(res, { + // data: { + // domainNamespaces: [], + // pagination: { + // total: 0, + // limit, + // offset + // } + // }, + // success: true, + // error: false, + // message: "No namespaces found. Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.", + // status: HttpCode.OK + // }); + // } + const domainNamespacesList = await query(limit, offset); const [{ count }] = await db diff --git a/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts new file mode 100644 index 000000000..bef7ba7e9 --- /dev/null +++ b/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts @@ -0,0 +1,143 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eventStreamingDestinations } from "@server/db"; +import { logStreamingManager } from "#private/lib/logStreaming"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { encrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty() +}); + +const bodySchema = z.strictObject({ + type: z.string().nonempty(), + config: z.string().nonempty(), + enabled: z.boolean().optional().default(true), + sendConnectionLogs: z.boolean().optional().default(false), + sendRequestLogs: z.boolean().optional().default(false), + sendActionLogs: z.boolean().optional().default(false), + sendAccessLogs: z.boolean().optional().default(false) +}); + +export type CreateEventStreamingDestinationResponse = { + destinationId: number; +}; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/event-streaming-destination", + description: "Create an event streaming destination for a specific organization.", + tags: [OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createEventStreamingDestination( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { type, config: configToSet, enabled } = parsedBody.data; + + const key = config.getRawConfig().server.secret!; + const encryptedConfig = encrypt(configToSet, key); + + const now = Date.now(); + + const [destination] = await db + .insert(eventStreamingDestinations) + .values({ + orgId, + type, + config: encryptedConfig, + enabled, + createdAt: now, + updatedAt: now, + sendAccessLogs: parsedBody.data.sendAccessLogs, + sendActionLogs: parsedBody.data.sendActionLogs, + sendConnectionLogs: parsedBody.data.sendConnectionLogs, + sendRequestLogs: parsedBody.data.sendRequestLogs + }) + .returning(); + + // Seed cursors at the current max row id for every log type so this + // destination only receives events written *after* it was created. + // Fire-and-forget: a failure here is non-fatal; the manager has a lazy + // fallback that will seed at the next poll if these rows are missing. + logStreamingManager + .initializeCursorsForDestination(destination.destinationId, orgId) + .catch((err) => + logger.error( + "createEventStreamingDestination: failed to initialise streaming cursors", + err + ) + ); + + return response(res, { + data: { + destinationId: destination.destinationId + }, + success: true, + error: false, + message: "Event streaming destination created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts new file mode 100644 index 000000000..d93bc4405 --- /dev/null +++ b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts @@ -0,0 +1,103 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eventStreamingDestinations } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { and, eq } from "drizzle-orm"; + +const paramsSchema = z + .object({ + orgId: z.string().nonempty(), + destinationId: z.coerce.number() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/event-streaming-destination/{destinationId}", + description: "Delete an event streaming destination for a specific organization.", + tags: [OpenAPITags.Org], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteEventStreamingDestination( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, destinationId } = parsedParams.data; + + const [existing] = await db + .select() + .from(eventStreamingDestinations) + .where( + and( + eq(eventStreamingDestinations.destinationId, destinationId), + eq(eventStreamingDestinations.orgId, orgId) + ) + ); + + if (!existing) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Event streaming destination not found" + ) + ); + } + + await db + .delete(eventStreamingDestinations) + .where( + and( + eq(eventStreamingDestinations.destinationId, destinationId), + eq(eventStreamingDestinations.orgId, orgId) + ) + ); + + return response(res, { + data: null, + success: true, + error: false, + message: "Event streaming destination deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/private/routers/eventStreamingDestination/index.ts b/server/private/routers/eventStreamingDestination/index.ts new file mode 100644 index 000000000..595e9595b --- /dev/null +++ b/server/private/routers/eventStreamingDestination/index.ts @@ -0,0 +1,17 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./createEventStreamingDestination"; +export * from "./updateEventStreamingDestination"; +export * from "./deleteEventStreamingDestination"; +export * from "./listEventStreamingDestinations"; \ No newline at end of file diff --git a/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts new file mode 100644 index 000000000..ac3f14e62 --- /dev/null +++ b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts @@ -0,0 +1,159 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eventStreamingDestinations } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { eq, sql } from "drizzle-orm"; +import { decrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty() +}); + +const querySchema = z.strictObject({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); + +export type ListEventStreamingDestinationsResponse = { + destinations: { + destinationId: number; + orgId: string; + type: string; + config: string; + enabled: boolean; + createdAt: number; + updatedAt: number; + sendConnectionLogs: boolean; + sendRequestLogs: boolean; + sendActionLogs: boolean; + sendAccessLogs: boolean; + }[]; + pagination: { + total: number; + limit: number; + offset: number; + }; +}; + +async function query(orgId: string, limit: number, offset: number) { + const res = await db + .select() + .from(eventStreamingDestinations) + .where(eq(eventStreamingDestinations.orgId, orgId)) + .orderBy(sql`${eventStreamingDestinations.createdAt} DESC`) + .limit(limit) + .offset(offset); + return res; +} + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/event-streaming-destination", + description: "List all event streaming destinations for a specific organization.", + tags: [OpenAPITags.Org], + request: { + query: querySchema, + params: paramsSchema + }, + responses: {} +}); + +export async function listEventStreamingDestinations( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + const { orgId } = parsedParams.data; + + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const list = await query(orgId, limit, offset); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(eventStreamingDestinations) + .where(eq(eventStreamingDestinations.orgId, orgId)); + + const key = config.getRawConfig().server.secret!; + const decryptedList = list.map((dest) => { + try { + return { ...dest, config: decrypt(dest.config, key) }; + } catch (err) { + logger.error( + `listEventStreamingDestinations: failed to decrypt config for destination ${dest.destinationId}`, + err + ); + return { ...dest, config: "" }; + } + }); + + return response(res, { + data: { + destinations: decryptedList, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Event streaming destinations retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts new file mode 100644 index 000000000..24dc68aef --- /dev/null +++ b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts @@ -0,0 +1,157 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { eventStreamingDestinations } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { and, eq } from "drizzle-orm"; +import { encrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; + +const paramsSchema = z + .object({ + orgId: z.string().nonempty(), + destinationId: z.coerce.number() + }) + .strict(); + +const bodySchema = z.strictObject({ + type: z.string().optional(), + config: z.string().optional(), + enabled: z.boolean().optional(), + sendConnectionLogs: z.boolean().optional(), + sendRequestLogs: z.boolean().optional(), + sendActionLogs: z.boolean().optional(), + sendAccessLogs: z.boolean().optional() +}); + +export type UpdateEventStreamingDestinationResponse = { + destinationId: number; +}; + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/event-streaming-destination/{destinationId}", + description: "Update an event streaming destination for a specific organization.", + tags: [OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function updateEventStreamingDestination( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, destinationId } = parsedParams.data; + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const [existing] = await db + .select() + .from(eventStreamingDestinations) + .where( + and( + eq(eventStreamingDestinations.destinationId, destinationId), + eq(eventStreamingDestinations.orgId, orgId) + ) + ); + + if (!existing) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Event streaming destination not found" + ) + ); + } + + const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data; + + const updateData: Record = { + updatedAt: Date.now() + }; + + if (type !== undefined) updateData.type = type; + if (configToUpdate !== undefined) { + const key = config.getRawConfig().server.secret!; + updateData.config = encrypt(configToUpdate, key); + } + if (enabled !== undefined) updateData.enabled = enabled; + if (sendAccessLogs !== undefined) updateData.sendAccessLogs = sendAccessLogs; + if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs; + if (sendConnectionLogs !== undefined) updateData.sendConnectionLogs = sendConnectionLogs; + if (sendRequestLogs !== undefined) updateData.sendRequestLogs = sendRequestLogs; + + await db + .update(eventStreamingDestinations) + .set(updateData) + .where( + and( + eq(eventStreamingDestinations.destinationId, destinationId), + eq(eventStreamingDestinations.orgId, orgId) + ) + ); + + + return response(res, { + data: { + destinationId + }, + success: true, + error: false, + message: "Event streaming destination updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 412895a41..4410a44c8 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -28,6 +28,7 @@ import * as approval from "#private/routers/approvals"; import * as ssh from "#private/routers/ssh"; import * as user from "#private/routers/user"; import * as siteProvisioning from "#private/routers/siteProvisioning"; +import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import { verifyOrgAccess, @@ -615,3 +616,39 @@ authenticated.patch( logActionAudit(ActionsEnum.updateSiteProvisioningKey), siteProvisioning.updateSiteProvisioningKey ); + +authenticated.put( + "/org/:orgId/event-streaming-destination", + verifyValidLicense, + verifyOrgAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.createEventStreamingDestination), + logActionAudit(ActionsEnum.createEventStreamingDestination), + eventStreamingDestination.createEventStreamingDestination +); + +authenticated.post( + "/org/:orgId/event-streaming-destination/:destinationId", + verifyValidLicense, + verifyOrgAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.updateEventStreamingDestination), + logActionAudit(ActionsEnum.updateEventStreamingDestination), + eventStreamingDestination.updateEventStreamingDestination +); + +authenticated.delete( + "/org/:orgId/event-streaming-destination/:destinationId", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteEventStreamingDestination), + logActionAudit(ActionsEnum.deleteEventStreamingDestination), + eventStreamingDestination.deleteEventStreamingDestination +); + +authenticated.get( + "/org/:orgId/event-streaming-destinations", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listEventStreamingDestinations), + eventStreamingDestination.listEventStreamingDestinations +); diff --git a/server/private/routers/newt/handleConnectionLogMessage.ts b/server/private/routers/newt/handleConnectionLogMessage.ts index 2ac7153b5..e980f85c9 100644 --- a/server/private/routers/newt/handleConnectionLogMessage.ts +++ b/server/private/routers/newt/handleConnectionLogMessage.ts @@ -1,27 +1,33 @@ -import { db, logsDb } from "@server/db"; +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { db } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; -import { connectionAuditLog, sites, Newt, clients, orgs } from "@server/db"; -import { and, eq, lt, inArray } from "drizzle-orm"; +import { sites, Newt, clients, orgs } from "@server/db"; +import { and, eq, inArray } from "drizzle-orm"; import logger from "@server/logger"; import { inflate } from "zlib"; import { promisify } from "util"; -import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; +import { + logConnectionAudit, + flushConnectionLogToDb, + cleanUpOldLogs +} from "#private/lib/logConnectionAudit"; + +export { flushConnectionLogToDb, cleanUpOldLogs }; const zlibInflate = promisify(inflate); -// Retry configuration for deadlock handling -const MAX_RETRIES = 3; -const BASE_DELAY_MS = 50; - -// How often to flush accumulated connection log data to the database -const FLUSH_INTERVAL_MS = 30_000; // 30 seconds - -// Maximum number of records to buffer before forcing a flush -const MAX_BUFFERED_RECORDS = 500; - -// Maximum number of records to insert in a single batch -const INSERT_BATCH_SIZE = 100; - interface ConnectionSessionData { sessionId: string; resourceId: number; @@ -34,64 +40,6 @@ interface ConnectionSessionData { bytesRx?: number; } -interface ConnectionLogRecord { - sessionId: string; - siteResourceId: number; - orgId: string; - siteId: number; - clientId: number | null; - userId: string | null; - sourceAddr: string; - destAddr: string; - protocol: string; - startedAt: number; // epoch seconds - endedAt: number | null; - bytesTx: number | null; - bytesRx: number | null; -} - -// In-memory buffer of records waiting to be flushed -let buffer: ConnectionLogRecord[] = []; - -/** - * Check if an error is a deadlock error - */ -function isDeadlockError(error: any): boolean { - return ( - error?.code === "40P01" || - error?.cause?.code === "40P01" || - (error?.message && error.message.includes("deadlock")) - ); -} - -/** - * Execute a function with retry logic for deadlock handling - */ -async function withDeadlockRetry( - operation: () => Promise, - context: string -): Promise { - let attempt = 0; - while (true) { - try { - return await operation(); - } catch (error: any) { - if (isDeadlockError(error) && attempt < MAX_RETRIES) { - attempt++; - const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS; - const jitter = Math.random() * baseDelay; - const delay = baseDelay + jitter; - logger.warn( - `Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms` - ); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - throw error; - } - } -} - /** * Decompress a base64-encoded zlib-compressed string into parsed JSON. */ @@ -125,105 +73,6 @@ function toEpochSeconds(isoString: string | undefined | null): number | null { return Math.floor(ms / 1000); } -/** - * Flush all buffered connection log records to the database. - * - * Swaps out the buffer before writing so that any records added during the - * flush are captured in the new buffer rather than being lost. Entries that - * fail to write are re-queued back into the buffer so they will be retried - * on the next flush. - * - * This function is exported so that the application's graceful-shutdown - * cleanup handler can call it before the process exits. - */ -export async function flushConnectionLogToDb(): Promise { - if (buffer.length === 0) { - return; - } - - // Atomically swap out the buffer so new data keeps flowing in - const snapshot = buffer; - buffer = []; - - logger.debug( - `Flushing ${snapshot.length} connection log record(s) to the database` - ); - - // Insert in batches to avoid overly large SQL statements - for (let i = 0; i < snapshot.length; i += INSERT_BATCH_SIZE) { - const batch = snapshot.slice(i, i + INSERT_BATCH_SIZE); - - try { - await withDeadlockRetry(async () => { - await logsDb.insert(connectionAuditLog).values(batch); - }, `flush connection log batch (${batch.length} records)`); - } catch (error) { - logger.error( - `Failed to flush connection log batch of ${batch.length} records:`, - error - ); - - // Re-queue the failed batch so it is retried on the next flush - buffer = [...batch, ...buffer]; - - // Cap buffer to prevent unbounded growth if DB is unreachable - if (buffer.length > MAX_BUFFERED_RECORDS * 5) { - const dropped = buffer.length - MAX_BUFFERED_RECORDS * 5; - buffer = buffer.slice(0, MAX_BUFFERED_RECORDS * 5); - logger.warn( - `Connection log buffer overflow, dropped ${dropped} oldest records` - ); - } - - // Stop trying further batches from this snapshot — they'll be - // picked up by the next flush via the re-queued records above - const remaining = snapshot.slice(i + INSERT_BATCH_SIZE); - if (remaining.length > 0) { - buffer = [...remaining, ...buffer]; - } - break; - } - } -} - -const flushTimer = setInterval(async () => { - try { - await flushConnectionLogToDb(); - } catch (error) { - logger.error( - "Unexpected error during periodic connection log flush:", - error - ); - } -}, FLUSH_INTERVAL_MS); - -// Calling unref() means this timer will not keep the Node.js event loop alive -// on its own — the process can still exit normally when there is no other work -// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly -// before process.exit(), so no data is lost. -flushTimer.unref(); - -export async function cleanUpOldLogs(orgId: string, retentionDays: number) { - const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); - - try { - await logsDb - .delete(connectionAuditLog) - .where( - and( - lt(connectionAuditLog.startedAt, cutoffTimestamp), - eq(connectionAuditLog.orgId, orgId) - ) - ); - - // logger.debug( - // `Cleaned up connection audit logs older than ${retentionDays} days` - // ); - } catch (error) { - logger.error("Error cleaning up old connection audit logs:", error); - } -} - export const handleConnectionLogMessage: MessageHandler = async (context) => { const { message, client } = context; const newt = client as Newt; @@ -277,13 +126,16 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { return; } - logger.debug(`Sessions: ${JSON.stringify(sessions)}`) + logger.debug(`Sessions: ${JSON.stringify(sessions)}`); // Build a map from sourceAddr → { clientId, userId } by querying clients // whose subnet field matches exactly. Client subnets are stored with the // org's CIDR suffix (e.g. "100.90.128.5/16"), so we reconstruct that from // each unique sourceAddr + the org's CIDR suffix and do a targeted IN query. - const ipToClient = new Map(); + const ipToClient = new Map< + string, + { clientId: number; userId: string | null } + >(); if (cidrSuffix) { // Collect unique source addresses so we only query for what we need @@ -296,13 +148,11 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { if (uniqueSourceAddrs.size > 0) { // Construct the exact subnet strings as stored in the DB - const subnetQueries = Array.from(uniqueSourceAddrs).map( - (addr) => { - // Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1") - const ip = addr.includes(":") ? addr.split(":")[0] : addr; - return `${ip}${cidrSuffix}`; - } - ); + const subnetQueries = Array.from(uniqueSourceAddrs).map((addr) => { + // Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1") + const ip = addr.includes(":") ? addr.split(":")[0] : addr; + return `${ip}${cidrSuffix}`; + }); logger.debug(`Subnet queries: ${JSON.stringify(subnetQueries)}`); @@ -322,13 +172,18 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { for (const c of matchedClients) { const ip = c.subnet.split("/")[0]; - logger.debug(`Client ${c.clientId} subnet ${c.subnet} matches ${ip}`); - ipToClient.set(ip, { clientId: c.clientId, userId: c.userId }); + logger.debug( + `Client ${c.clientId} subnet ${c.subnet} matches ${ip}` + ); + ipToClient.set(ip, { + clientId: c.clientId, + userId: c.userId + }); } } } - // Convert to DB records and add to the buffer + // Convert to DB records and hand off to the audit logger for (const session of sessions) { // Validate required fields if ( @@ -356,11 +211,12 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { // client's IP on the WireGuard network, which corresponds to the IP // portion of the client's subnet CIDR (e.g. "100.90.128.5/24"). // Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1") - const sourceIp = session.sourceAddr.includes(":") ? session.sourceAddr.split(":")[0] : session.sourceAddr; + const sourceIp = session.sourceAddr.includes(":") + ? session.sourceAddr.split(":")[0] + : session.sourceAddr; const clientInfo = ipToClient.get(sourceIp) ?? null; - - buffer.push({ + logConnectionAudit({ sessionId: session.sessionId, siteResourceId: session.resourceId, orgId, @@ -380,15 +236,4 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { logger.debug( `Buffered ${sessions.length} connection log session(s) from newt ${newt.newtId} (site ${newt.siteId})` ); - - // If the buffer has grown large enough, trigger an immediate flush - if (buffer.length >= MAX_BUFFERED_RECORDS) { - // Fire and forget — errors are handled inside flushConnectionLogToDb - flushConnectionLogToDb().catch((error) => { - logger.error( - "Unexpected error during size-triggered connection log flush:", - error - ); - }); - } }; diff --git a/server/private/routers/newt/index.ts b/server/private/routers/newt/index.ts index cc182cf7d..256d19cb7 100644 --- a/server/private/routers/newt/index.ts +++ b/server/private/routers/newt/index.ts @@ -1 +1,14 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + export * from "./handleConnectionLogMessage"; diff --git a/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts b/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts index abed27550..e521eaa22 100644 --- a/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts +++ b/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts @@ -38,7 +38,8 @@ const bodySchema = z z.null(), z.coerce.number().int().positive().max(1_000_000) ]), - validUntil: z.string().max(255).optional() + validUntil: z.string().max(255).optional(), + approveNewSites: z.boolean().optional().default(true) }) .superRefine((data, ctx) => { const v = data.validUntil; @@ -82,7 +83,7 @@ export async function createSiteProvisioningKey( } const { orgId } = parsedParams.data; - const { name, maxBatchSize } = parsedBody.data; + const { name, maxBatchSize, approveNewSites } = parsedBody.data; const vuRaw = parsedBody.data.validUntil; const validUntil = vuRaw == null || vuRaw.trim() === "" @@ -106,7 +107,8 @@ export async function createSiteProvisioningKey( lastUsed: null, maxBatchSize, numUsed: 0, - validUntil + validUntil, + approveNewSites }); await trx.insert(siteProvisioningKeyOrg).values({ @@ -127,7 +129,8 @@ export async function createSiteProvisioningKey( lastUsed: null, maxBatchSize, numUsed: 0, - validUntil + validUntil, + approveNewSites }, success: true, error: false, diff --git a/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts b/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts index 5f7531a2c..dd51179d3 100644 --- a/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts +++ b/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts @@ -57,7 +57,8 @@ function querySiteProvisioningKeys(orgId: string) { lastUsed: siteProvisioningKeys.lastUsed, maxBatchSize: siteProvisioningKeys.maxBatchSize, numUsed: siteProvisioningKeys.numUsed, - validUntil: siteProvisioningKeys.validUntil + validUntil: siteProvisioningKeys.validUntil, + approveNewSites: siteProvisioningKeys.approveNewSites }) .from(siteProvisioningKeyOrg) .innerJoin( diff --git a/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts b/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts index 526d8bfb8..2f4dafbdf 100644 --- a/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts +++ b/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts @@ -39,16 +39,18 @@ const bodySchema = z z.coerce.number().int().positive().max(1_000_000) ]) .optional(), - validUntil: z.string().max(255).optional() + validUntil: z.string().max(255).optional(), + approveNewSites: z.boolean().optional() }) .superRefine((data, ctx) => { if ( data.maxBatchSize === undefined && - data.validUntil === undefined + data.validUntil === undefined && + data.approveNewSites === undefined ) { ctx.addIssue({ code: "custom", - message: "Provide maxBatchSize and/or validUntil", + message: "Provide maxBatchSize and/or validUntil and/or approveNewSites", path: ["maxBatchSize"] }); } @@ -129,6 +131,7 @@ export async function updateSiteProvisioningKey( const setValues: { maxBatchSize?: number | null; validUntil?: string | null; + approveNewSites?: boolean; } = {}; if (body.maxBatchSize !== undefined) { setValues.maxBatchSize = body.maxBatchSize; @@ -139,6 +142,9 @@ export async function updateSiteProvisioningKey( ? null : new Date(Date.parse(body.validUntil)).toISOString(); } + if (body.approveNewSites !== undefined) { + setValues.approveNewSites = body.approveNewSites; + } await db .update(siteProvisioningKeys) @@ -160,7 +166,8 @@ export async function updateSiteProvisioningKey( lastUsed: siteProvisioningKeys.lastUsed, maxBatchSize: siteProvisioningKeys.maxBatchSize, numUsed: siteProvisioningKeys.numUsed, - validUntil: siteProvisioningKeys.validUntil + validUntil: siteProvisioningKeys.validUntil, + approveNewSites: siteProvisioningKeys.approveNewSites }) .from(siteProvisioningKeys) .where( diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index e8de55c54..b02d2b23c 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -488,7 +488,7 @@ export async function signSshKey( action: true, type: "ssh", orgId: orgId, - resourceId: resource.siteResourceId, + siteResourceId: resource.siteResourceId, user: req.user ? { username: req.user.username ?? "", userId: req.user.userId } : undefined, diff --git a/server/private/routers/ws/messageHandlers.ts b/server/private/routers/ws/messageHandlers.ts index a3c9c5bdb..5021cb966 100644 --- a/server/private/routers/ws/messageHandlers.ts +++ b/server/private/routers/ws/messageHandlers.ts @@ -18,7 +18,7 @@ import { } from "#private/routers/remoteExitNode"; import { MessageHandler } from "@server/routers/ws"; import { build } from "@server/build"; -import { handleConnectionLogMessage } from "#dynamic/routers/newt"; +import { handleConnectionLogMessage } from "#private/routers/newt"; export const messageHandlers: Record = { "remoteExitNode/register": handleRemoteExitNodeRegisterMessage, diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index 7c967acef..8333a4578 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,4 +1,37 @@ import { assertEquals } from "@test/assert"; +import { REGIONS } from "@server/db/regions"; + +function isIpInRegion( + ipCountryCode: string | undefined, + checkRegionCode: string +): boolean { + if (!ipCountryCode) { + return false; + } + + const upperCode = ipCountryCode.toUpperCase(); + + for (const region of REGIONS) { + // Check if it's a top-level region (continent) + if (region.id === checkRegionCode) { + for (const subregion of region.includes) { + if (subregion.countries.includes(upperCode)) { + return true; + } + } + return false; + } + + // Check subregions + for (const subregion of region.includes) { + if (subregion.id === checkRegionCode) { + return subregion.countries.includes(upperCode); + } + } + } + + return false; +} function isPathAllowed(pattern: string, path: string): boolean { // Normalize and split paths into segments @@ -272,12 +305,71 @@ function runTests() { "Root path should not match non-root path" ); - console.log("All tests passed!"); + console.log("All path matching tests passed!"); +} + +function runRegionTests() { + console.log("\nRunning isIpInRegion tests..."); + + // Test undefined country code + assertEquals( + isIpInRegion(undefined, "150"), + false, + "Undefined country code should return false" + ); + + // Test subregion matching (Western Europe) + assertEquals( + isIpInRegion("DE", "155"), + true, + "Country should match its subregion" + ); + assertEquals( + isIpInRegion("GB", "155"), + false, + "Country should NOT match wrong subregion" + ); + + // Test continent matching (Europe) + assertEquals( + isIpInRegion("DE", "150"), + true, + "Country should match its continent" + ); + assertEquals( + isIpInRegion("GB", "150"), + true, + "Different European country should match Europe" + ); + assertEquals( + isIpInRegion("US", "150"), + false, + "Non-European country should NOT match Europe" + ); + + // Test case insensitivity + assertEquals( + isIpInRegion("de", "155"), + true, + "Lowercase country code should work" + ); + + // Test invalid region code + assertEquals( + isIpInRegion("DE", "999"), + false, + "Invalid region code should return false" + ); + + console.log("All region tests passed!"); } // Run all tests try { runTests(); + runRegionTests(); + console.log("\n✅ All tests passed!"); } catch (error) { - console.error("Test failed:", error); + console.error("❌ Test failed:", error); + process.exit(1); } diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 0b1cc1183..e2e5f6766 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -36,6 +36,7 @@ import { enforceResourceSessionLength } from "#dynamic/lib/checkOrgAccessPolicy"; import { logRequestAudit } from "./logRequestAudit"; +import { REGIONS } from "@server/db/regions"; import { localCache } from "#dynamic/lib/cache"; import { APP_VERSION } from "@server/lib/consts"; import { isSubscribed } from "#dynamic/lib/isSubscribed"; @@ -1022,6 +1023,12 @@ async function checkRules( (await isIpInAsn(ipAsn, rule.value)) ) { return rule.action as any; + } else if ( + clientIp && + rule.match == "REGION" && + (await isIpInRegion(ipCC, rule.value)) + ) { + return rule.action as any; } } @@ -1207,6 +1214,45 @@ async function isIpInAsn( return match; } +export async function isIpInRegion( + ipCountryCode: string | undefined, + checkRegionCode: string +): Promise { + if (!ipCountryCode) { + return false; + } + + const upperCode = ipCountryCode.toUpperCase(); + + for (const region of REGIONS) { + // Check if it's a top-level region (continent) + if (region.id === checkRegionCode) { + for (const subregion of region.includes) { + if (subregion.countries.includes(upperCode)) { + logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`); + return true; + } + } + logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`); + return false; + } + + // Check subregions + for (const subregion of region.includes) { + if (subregion.id === checkRegionCode) { + if (subregion.countries.includes(upperCode)) { + logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`); + return true; + } + logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`); + return false; + } + } + } + + return false; +} + async function getAsnFromIp(ip: string): Promise { const asnCacheKey = `asn:${ip}`; diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 0bf798509..f5d69857d 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -29,65 +29,9 @@ import { } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; -import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const olmVersionCache = new NodeCache({ stdTTL: 3600 }); - -async function getLatestOlmVersion(): Promise { - try { - const cachedVersion = olmVersionCache.get("latestOlmVersion"); - if (cachedVersion) { - return cachedVersion; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 1500); - - const response = await fetch( - "https://api.github.com/repos/fosrl/olm/tags", - { - signal: controller.signal - } - ); - - clearTimeout(timeoutId); - - if (!response.ok) { - logger.warn( - `Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}` - ); - return null; - } - - let tags = await response.json(); - if (!Array.isArray(tags) || tags.length === 0) { - logger.warn("No tags found for Olm repository"); - return null; - } - tags = tags.filter((version) => !version.name.includes("rc")); - const latestVersion = tags[0].name; - - olmVersionCache.set("latestOlmVersion", latestVersion, 3600); - - return latestVersion; - } catch (error: any) { - if (error.name === "AbortError") { - logger.warn("Request to fetch latest Olm version timed out (1.5s)"); - } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { - logger.warn("Connection timeout while fetching latest Olm version"); - } else { - logger.warn( - "Error fetching latest Olm version:", - error.message || error - ); - } - return null; - } -} - const listClientsParamsSchema = z.strictObject({ orgId: z.string() }); @@ -413,44 +357,45 @@ export async function listClients( }; }); - const latestOlVersionPromise = getLatestOlmVersion(); + // REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW + // const latestOlmVersionPromise = getLatestOlmVersion(); - const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map( - (client) => { - const OlmWithUpdate: OlmWithUpdateAvailable = { ...client }; - // Initially set to false, will be updated if version check succeeds - OlmWithUpdate.olmUpdateAvailable = false; - return OlmWithUpdate; - } - ); + // const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map( + // (client) => { + // const OlmWithUpdate: OlmWithUpdateAvailable = { ...client }; + // // Initially set to false, will be updated if version check succeeds + // OlmWithUpdate.olmUpdateAvailable = false; + // return OlmWithUpdate; + // } + // ); // Try to get the latest version, but don't block if it fails - try { - const latestOlVersion = await latestOlVersionPromise; + // try { + // const latestOlmVersion = await latestOlVersionPromise; - if (latestOlVersion) { - olmsWithUpdates.forEach((client) => { - try { - client.olmUpdateAvailable = semver.lt( - client.olmVersion ? client.olmVersion : "", - latestOlVersion - ); - } catch (error) { - client.olmUpdateAvailable = false; - } - }); - } - } catch (error) { - // Log the error but don't let it block the response - logger.warn( - "Failed to check for OLM updates, continuing without update info:", - error - ); - } + // if (latestOlVersion) { + // olmsWithUpdates.forEach((client) => { + // try { + // client.olmUpdateAvailable = semver.lt( + // client.olmVersion ? client.olmVersion : "", + // latestOlVersion + // ); + // } catch (error) { + // client.olmUpdateAvailable = false; + // } + // }); + // } + // } catch (error) { + // // Log the error but don't let it block the response + // logger.warn( + // "Failed to check for OLM updates, continuing without update info:", + // error + // ); + // } return response(res, { data: { - clients: olmsWithUpdates, + clients: clientsWithSites, pagination: { total: totalCount, page, diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts index 0ae31165a..d793faf09 100644 --- a/server/routers/client/listUserDevices.ts +++ b/server/routers/client/listUserDevices.ts @@ -30,65 +30,10 @@ import { } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; -const olmVersionCache = new NodeCache({ stdTTL: 3600 }); - -async function getLatestOlmVersion(): Promise { - try { - const cachedVersion = olmVersionCache.get("latestOlmVersion"); - if (cachedVersion) { - return cachedVersion; - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 1500); - - const response = await fetch( - "https://api.github.com/repos/fosrl/olm/tags", - { - signal: controller.signal - } - ); - - clearTimeout(timeoutId); - - if (!response.ok) { - logger.warn( - `Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}` - ); - return null; - } - - let tags = await response.json(); - if (!Array.isArray(tags) || tags.length === 0) { - logger.warn("No tags found for Olm repository"); - return null; - } - tags = tags.filter((version) => !version.name.includes("rc")); - const latestVersion = tags[0].name; - - olmVersionCache.set("latestOlmVersion", latestVersion, 3600); - - return latestVersion; - } catch (error: any) { - if (error.name === "AbortError") { - logger.warn("Request to fetch latest Olm version timed out (1.5s)"); - } else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { - logger.warn("Connection timeout while fetching latest Olm version"); - } else { - logger.warn( - "Error fetching latest Olm version:", - error.message || error - ); - } - return null; - } -} - const listUserDevicesParamsSchema = z.strictObject({ orgId: z.string() }); @@ -453,29 +398,30 @@ export async function listUserDevices( } ); - // Try to get the latest version, but don't block if it fails - try { - const latestOlmVersion = await getLatestOlmVersion(); + // REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW + // // Try to get the latest version, but don't block if it fails + // try { + // const latestOlmVersion = await getLatestOlmVersion(); - if (latestOlmVersion) { - olmsWithUpdates.forEach((client) => { - try { - client.olmUpdateAvailable = semver.lt( - client.olmVersion ? client.olmVersion : "", - latestOlmVersion - ); - } catch (error) { - client.olmUpdateAvailable = false; - } - }); - } - } catch (error) { - // Log the error but don't let it block the response - logger.warn( - "Failed to check for OLM updates, continuing without update info:", - error - ); - } + // if (latestOlmVersion) { + // olmsWithUpdates.forEach((client) => { + // try { + // client.olmUpdateAvailable = semver.lt( + // client.olmVersion ? client.olmVersion : "", + // latestOlmVersion + // ); + // } catch (error) { + // client.olmUpdateAvailable = false; + // } + // }); + // } + // } catch (error) { + // // Log the error but don't let it block the response + // logger.warn( + // "Failed to check for OLM updates, continuing without update info:", + // error + // ); + // } return response(res, { data: { diff --git a/server/routers/external.ts b/server/routers/external.ts index 177626aa2..d7729bca5 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -440,6 +440,12 @@ authenticated.get( resource.getUserResources ); +authenticated.get( + "/org/:orgId/user-resource-aliases", + verifyOrgAccess, + resource.listUserResourceAliases +); + authenticated.get( "/org/:orgId/domains", verifyOrgAccess, @@ -796,6 +802,11 @@ unauthenticated.get( // ); unauthenticated.get("/user", verifySessionMiddleware, user.getUser); +unauthenticated.post( + "/user/locale", + verifySessionMiddleware, + user.updateUserLocale +); unauthenticated.get("/my-device", verifySessionMiddleware, user.myDevice); authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers); diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index 042c844aa..dcd897471 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { sql } from "drizzle-orm"; -import { db } from "@server/db"; +import { db, DB_TYPE } from "@server/db"; import logger from "@server/logger"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -96,6 +96,10 @@ async function dbQueryRows>( return (await anyDb.all(query)) as T[]; } +function isSQLite(): boolean { + return DB_TYPE == "sqlite"; +} + /** * Flush all accumulated site bandwidth data to the database. * @@ -141,19 +145,36 @@ export async function flushSiteBandwidthToDb(): Promise { const chunk = sortedEntries.slice(i, i + BATCH_CHUNK_SIZE); const chunkEnd = i + chunk.length - 1; - // Build a parameterised VALUES list: (pubKey, bytesIn, bytesOut), ... - // Both PostgreSQL and SQLite (≥ 3.33.0, which better-sqlite3 bundles) - // support UPDATE … FROM (VALUES …), letting us update the whole chunk - // in a single query instead of N individual round-trips. - const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) => - sql`(${publicKey}, ${bytesIn}, ${bytesOut})` - ); - const valuesClause = sql.join(valuesList, sql`, `); - let rows: { orgId: string; pubKey: string }[] = []; try { rows = await withDeadlockRetry(async () => { + if (isSQLite()) { + // SQLite: one UPDATE per row — no need for batch efficiency here. + const results: { orgId: string; pubKey: string }[] = []; + for (const [publicKey, { bytesIn, bytesOut }] of chunk) { + const result = await dbQueryRows<{ + orgId: string; + pubKey: string; + }>(sql` + UPDATE sites + SET + "bytesOut" = COALESCE("bytesOut", 0) + ${bytesIn}, + "bytesIn" = COALESCE("bytesIn", 0) + ${bytesOut}, + "lastBandwidthUpdate" = ${currentTime} + WHERE "pubKey" = ${publicKey} + RETURNING "orgId", "pubKey" + `); + results.push(...result); + } + return results; + } + + // PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk. + const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) => + sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)` + ); + const valuesClause = sql.join(valuesList, sql`, `); return dbQueryRows<{ orgId: string; pubKey: string }>(sql` UPDATE sites SET diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index 35d52816e..afb196152 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -168,13 +168,13 @@ export async function buildClientConfigurationForNewtClient( ) ); - const resourceTarget = generateSubnetProxyTargetV2( + const resourceTargets = generateSubnetProxyTargetV2( resource, resourceClients ); - if (resourceTarget) { - targetsToSend.push(resourceTarget); + if (resourceTargets) { + targetsToSend.push(...resourceTargets); } } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index c73098ce4..9c67f53ee 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -8,14 +8,7 @@ import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { buildClientConfigurationForNewtClient } from "./buildConfiguration"; import { convertTargetsIfNessicary } from "../client/targets"; import { canCompress } from "@server/lib/clientVersionChecks"; - -const inputSchema = z.object({ - publicKey: z.string(), - port: z.int().positive(), - chainId: z.string() -}); - -type Input = z.infer; +import config from "@server/lib/config"; export const handleGetConfigMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; @@ -35,16 +28,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { return; } - const parsed = inputSchema.safeParse(message.data); - if (!parsed.success) { - logger.error( - "handleGetConfigMessage: Invalid input: " + - fromError(parsed.error).toString() - ); - return; - } - - const { publicKey, port, chainId } = message.data as Input; + const { publicKey, port, chainId } = message.data; const siteId = newt.siteId; // Get the current site data @@ -72,7 +56,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) { logger.warn( - `handleGetConfigMessage: Site ${existingSite.siteId} last hole punch is too old, skipping` + `Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?` ); return; } diff --git a/server/routers/newt/handleNewtPingMessage.ts b/server/routers/newt/handleNewtPingMessage.ts index da25852a0..32f665758 100644 --- a/server/routers/newt/handleNewtPingMessage.ts +++ b/server/routers/newt/handleNewtPingMessage.ts @@ -1,8 +1,11 @@ -import { db, newts, sites } from "@server/db"; -import { hasActiveConnections, getClientConfigVersion } from "#dynamic/routers/ws"; +import { db, newts, sites, targetHealthCheck, targets } from "@server/db"; +import { + hasActiveConnections, + getClientConfigVersion +} from "#dynamic/routers/ws"; import { MessageHandler } from "@server/routers/ws"; import { Newt } from "@server/db"; -import { eq, lt, isNull, and, or } from "drizzle-orm"; +import { eq, lt, isNull, and, or, ne, not } from "drizzle-orm"; import logger from "@server/logger"; import { sendNewtSyncMessage } from "./sync"; import { recordPing } from "./pingAccumulator"; @@ -11,6 +14,7 @@ import { recordPing } from "./pingAccumulator"; let offlineCheckerInterval: NodeJS.Timeout | null = null; const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes +const OFFLINE_THRESHOLD_BANDWIDTH_MS = 8 * 60 * 1000; // 8 minutes /** * Starts the background interval that checks for newt sites that haven't @@ -56,7 +60,9 @@ export const startNewtOfflineChecker = (): void => { // Backward-compatibility check: if the newt still has an // active WebSocket connection (older clients that don't send // pings), keep the site online. - const isConnected = await hasActiveConnections(staleSite.newtId); + const isConnected = await hasActiveConnections( + staleSite.newtId + ); if (isConnected) { logger.debug( `Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online` @@ -72,6 +78,83 @@ export const startNewtOfflineChecker = (): void => { .update(sites) .set({ online: false }) .where(eq(sites.siteId, staleSite.siteId)); + + const healthChecksOnSite = await db + .select() + .from(targetHealthCheck) + .innerJoin( + targets, + eq(targets.targetId, targetHealthCheck.targetId) + ) + .innerJoin(sites, eq(sites.siteId, targets.siteId)) + .where(eq(sites.siteId, staleSite.siteId)); + + for (const healthCheck of healthChecksOnSite) { + logger.info( + `Marking health check ${healthCheck.targetHealthCheck.targetHealthCheckId} offline due to site ${staleSite.siteId} being marked offline` + ); + await db + .update(targetHealthCheck) + .set({ hcHealth: "unknown" }) + .where( + eq( + targetHealthCheck.targetHealthCheckId, + healthCheck.targetHealthCheck + .targetHealthCheckId + ) + ); + } + } + + // this part only effects self hosted. Its not efficient but we dont expect people to have very many wireguard sites + // select all of the wireguard sites to evaluate if they need to be offline due to the last bandwidth update + const allWireguardSites = await db + .select({ + siteId: sites.siteId, + online: sites.online, + lastBandwidthUpdate: sites.lastBandwidthUpdate + }) + .from(sites) + .where( + and( + eq(sites.type, "wireguard"), + not(isNull(sites.lastBandwidthUpdate)) + ) + ); + + const wireguardOfflineThreshold = Math.floor( + (Date.now() - OFFLINE_THRESHOLD_BANDWIDTH_MS) / 1000 + ); + + // loop over each one. If its offline and there is a new update then mark it online. If its online and there is no update then mark it offline + for (const site of allWireguardSites) { + const lastBandwidthUpdate = + new Date(site.lastBandwidthUpdate!).getTime() / 1000; + if ( + lastBandwidthUpdate < wireguardOfflineThreshold && + site.online + ) { + logger.info( + `Marking wireguard site ${site.siteId} offline: no bandwidth update in over ${OFFLINE_THRESHOLD_BANDWIDTH_MS / 60000} minutes` + ); + + await db + .update(sites) + .set({ online: false }) + .where(eq(sites.siteId, site.siteId)); + } else if ( + lastBandwidthUpdate >= wireguardOfflineThreshold && + !site.online + ) { + logger.info( + `Marking wireguard site ${site.siteId} online: recent bandwidth update` + ); + + await db + .update(sites) + .set({ online: true }) + .where(eq(sites.siteId, site.siteId)); + } } } catch (error) { logger.error("Error in newt offline checker interval", { error }); diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts index b75ddd5e4..8f6df4bec 100644 --- a/server/routers/newt/handleNewtPingRequestMessage.ts +++ b/server/routers/newt/handleNewtPingRequestMessage.ts @@ -33,7 +33,7 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { return; } - const { noCloud } = message.data; + const { noCloud, chainId } = message.data; const exitNodesList = await listExitNodes( site.orgId, @@ -98,7 +98,8 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => { message: { type: "newt/ping/exitNodes", data: { - exitNodes: filteredExitNodes + exitNodes: filteredExitNodes, + chainId: chainId } }, broadcast: false, // Send to all clients diff --git a/server/routers/newt/pingAccumulator.ts b/server/routers/newt/pingAccumulator.ts index 83afd613e..fe2cde216 100644 --- a/server/routers/newt/pingAccumulator.ts +++ b/server/routers/newt/pingAccumulator.ts @@ -1,6 +1,6 @@ import { db } from "@server/db"; import { sites, clients, olms } from "@server/db"; -import { eq, inArray } from "drizzle-orm"; +import { inArray } from "drizzle-orm"; import logger from "@server/logger"; /** @@ -21,7 +21,7 @@ import logger from "@server/logger"; */ const FLUSH_INTERVAL_MS = 10_000; // Flush every 10 seconds -const MAX_RETRIES = 2; +const MAX_RETRIES = 5; const BASE_DELAY_MS = 50; // ── Site (newt) pings ────────────────────────────────────────────────── @@ -36,6 +36,14 @@ const pendingOlmArchiveResets: Set = new Set(); let flushTimer: NodeJS.Timeout | null = null; +/** + * Guard that prevents two flush cycles from running concurrently. + * setInterval does not await async callbacks, so without this a slow flush + * (e.g. due to DB latency) would overlap with the next scheduled cycle and + * the two concurrent bulk UPDATEs would deadlock each other. + */ +let isFlushing = false; + // ── Public API ───────────────────────────────────────────────────────── /** @@ -72,6 +80,12 @@ export function recordClientPing( /** * Flush all accumulated site pings to the database. + * + * Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE + * statement. We use the maximum timestamp across the batch so that `lastPing` + * reflects the most recent ping seen for any site in the group. This avoids + * the multi-statement transaction that previously created additional + * row-lock ordering hazards. */ async function flushSitePingsToDb(): Promise { if (pendingSitePings.size === 0) { @@ -83,55 +97,35 @@ async function flushSitePingsToDb(): Promise { const pingsToFlush = new Map(pendingSitePings); pendingSitePings.clear(); - // Sort by siteId for consistent lock ordering (prevents deadlocks) - const sortedEntries = Array.from(pingsToFlush.entries()).sort( - ([a], [b]) => a - b - ); + const entries = Array.from(pingsToFlush.entries()); const BATCH_SIZE = 50; - for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) { - const batch = sortedEntries.slice(i, i + BATCH_SIZE); + for (let i = 0; i < entries.length; i += BATCH_SIZE) { + const batch = entries.slice(i, i + BATCH_SIZE); + + // Use the latest timestamp in the batch so that `lastPing` always + // moves forward. Using a single timestamp for the whole batch means + // we only ever need one UPDATE statement (no transaction). + const maxTimestamp = Math.max(...batch.map(([, ts]) => ts)); + const siteIds = batch.map(([id]) => id); try { await withRetry(async () => { - // Group by timestamp for efficient bulk updates - const byTimestamp = new Map(); - for (const [siteId, timestamp] of batch) { - const group = byTimestamp.get(timestamp) || []; - group.push(siteId); - byTimestamp.set(timestamp, group); - } - - if (byTimestamp.size === 1) { - const [timestamp, siteIds] = Array.from( - byTimestamp.entries() - )[0]; - await db - .update(sites) - .set({ - online: true, - lastPing: timestamp - }) - .where(inArray(sites.siteId, siteIds)); - } else { - await db.transaction(async (tx) => { - for (const [timestamp, siteIds] of byTimestamp) { - await tx - .update(sites) - .set({ - online: true, - lastPing: timestamp - }) - .where(inArray(sites.siteId, siteIds)); - } - }); - } + await db + .update(sites) + .set({ + online: true, + lastPing: maxTimestamp + }) + .where(inArray(sites.siteId, siteIds)); }, "flushSitePingsToDb"); } catch (error) { logger.error( `Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`, { error } ); + // Re-queue only if the preserved timestamp is newer than any + // update that may have landed since we snapshotted. for (const [siteId, timestamp] of batch) { const existing = pendingSitePings.get(siteId); if (!existing || existing < timestamp) { @@ -144,6 +138,8 @@ async function flushSitePingsToDb(): Promise { /** * Flush all accumulated client (OLM) pings to the database. + * + * Same single-UPDATE-per-batch approach as `flushSitePingsToDb`. */ async function flushClientPingsToDb(): Promise { if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) { @@ -159,51 +155,25 @@ async function flushClientPingsToDb(): Promise { // ── Flush client pings ───────────────────────────────────────────── if (pingsToFlush.size > 0) { - const sortedEntries = Array.from(pingsToFlush.entries()).sort( - ([a], [b]) => a - b - ); + const entries = Array.from(pingsToFlush.entries()); const BATCH_SIZE = 50; - for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) { - const batch = sortedEntries.slice(i, i + BATCH_SIZE); + for (let i = 0; i < entries.length; i += BATCH_SIZE) { + const batch = entries.slice(i, i + BATCH_SIZE); + + const maxTimestamp = Math.max(...batch.map(([, ts]) => ts)); + const clientIds = batch.map(([id]) => id); try { await withRetry(async () => { - const byTimestamp = new Map(); - for (const [clientId, timestamp] of batch) { - const group = byTimestamp.get(timestamp) || []; - group.push(clientId); - byTimestamp.set(timestamp, group); - } - - if (byTimestamp.size === 1) { - const [timestamp, clientIds] = Array.from( - byTimestamp.entries() - )[0]; - await db - .update(clients) - .set({ - lastPing: timestamp, - online: true, - archived: false - }) - .where(inArray(clients.clientId, clientIds)); - } else { - await db.transaction(async (tx) => { - for (const [timestamp, clientIds] of byTimestamp) { - await tx - .update(clients) - .set({ - lastPing: timestamp, - online: true, - archived: false - }) - .where( - inArray(clients.clientId, clientIds) - ); - } - }); - } + await db + .update(clients) + .set({ + lastPing: maxTimestamp, + online: true, + archived: false + }) + .where(inArray(clients.clientId, clientIds)); }, "flushClientPingsToDb"); } catch (error) { logger.error( @@ -260,7 +230,12 @@ export async function flushPingsToDb(): Promise { /** * Simple retry wrapper with exponential backoff for transient errors - * (connection timeouts, unexpected disconnects). + * (deadlocks, connection timeouts, unexpected disconnects). + * + * PostgreSQL deadlocks (40P01) are always safe to retry: the database + * guarantees exactly one winner per deadlock pair, so the loser just needs + * to try again. MAX_RETRIES is intentionally higher than typical connection + * retry budgets to give deadlock victims enough chances to succeed. */ async function withRetry( operation: () => Promise, @@ -277,7 +252,8 @@ async function withRetry( const jitter = Math.random() * baseDelay; const delay = baseDelay + jitter; logger.warn( - `Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms` + `Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`, + { code: error?.code ?? error?.cause?.code } ); await new Promise((resolve) => setTimeout(resolve, delay)); continue; @@ -288,14 +264,14 @@ async function withRetry( } /** - * Detect transient connection errors that are safe to retry. + * Detect transient errors that are safe to retry. */ function isTransientError(error: any): boolean { if (!error) return false; const message = (error.message || "").toLowerCase(); const causeMessage = (error.cause?.message || "").toLowerCase(); - const code = error.code || ""; + const code = error.code || error.cause?.code || ""; // Connection timeout / terminated if ( @@ -308,12 +284,17 @@ function isTransientError(error: any): boolean { return true; } - // PostgreSQL deadlock + // PostgreSQL deadlock detected — always safe to retry (one winner guaranteed) if (code === "40P01" || message.includes("deadlock")) { return true; } - // ECONNRESET, ECONNREFUSED, EPIPE + // PostgreSQL serialization failure + if (code === "40001") { + return true; + } + + // ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT if ( code === "ECONNRESET" || code === "ECONNREFUSED" || @@ -337,12 +318,26 @@ export function startPingAccumulator(): void { } flushTimer = setInterval(async () => { + // Skip this tick if the previous flush is still in progress. + // setInterval does not await async callbacks, so without this guard + // two flush cycles can run concurrently and deadlock each other on + // overlapping bulk UPDATE statements. + if (isFlushing) { + logger.debug( + "Ping accumulator: previous flush still in progress, skipping cycle" + ); + return; + } + + isFlushing = true; try { await flushPingsToDb(); } catch (error) { logger.error("Unhandled error in ping accumulator flush", { error }); + } finally { + isFlushing = false; } }, FLUSH_INTERVAL_MS); @@ -364,7 +359,22 @@ export async function stopPingAccumulator(): Promise { flushTimer = null; } - // Final flush to persist any remaining pings + // Final flush to persist any remaining pings. + // Wait for any in-progress flush to finish first so we don't race. + if (isFlushing) { + logger.debug( + "Ping accumulator: waiting for in-progress flush before stopping…" + ); + await new Promise((resolve) => { + const poll = setInterval(() => { + if (!isFlushing) { + clearInterval(poll); + resolve(); + } + }, 50); + }); + } + try { await flushPingsToDb(); } catch (error) { @@ -379,4 +389,4 @@ export async function stopPingAccumulator(): Promise { */ export function getPendingPingCount(): number { return pendingSitePings.size + pendingClientPings.size; -} \ No newline at end of file +} diff --git a/server/routers/newt/registerNewt.ts b/server/routers/newt/registerNewt.ts index 427ac173f..de68ab2de 100644 --- a/server/routers/newt/registerNewt.ts +++ b/server/routers/newt/registerNewt.ts @@ -27,10 +27,11 @@ import { build } from "@server/build"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { INSPECT_MAX_BYTES } from "buffer"; -import { v } from "@faker-js/faker/dist/airline-Dz1uGqgJ"; +import { getNextAvailableClientSubnet } from "@server/lib/ip"; const bodySchema = z.object({ - provisioningKey: z.string().nonempty() + provisioningKey: z.string().nonempty(), + name: z.string().optional() }); export type RegisterNewtBody = z.infer; @@ -56,7 +57,7 @@ export async function registerNewt( ); } - const { provisioningKey } = parsedBody.data; + const { provisioningKey, name } = parsedBody.data; // Keys are in the format "siteProvisioningKeyId.secret" const dotIndex = provisioningKey.indexOf("."); @@ -82,7 +83,8 @@ export async function registerNewt( orgId: siteProvisioningKeyOrg.orgId, maxBatchSize: siteProvisioningKeys.maxBatchSize, numUsed: siteProvisioningKeys.numUsed, - validUntil: siteProvisioningKeys.validUntil + validUntil: siteProvisioningKeys.validUntil, + approveNewSites: siteProvisioningKeys.approveNewSites, }) .from(siteProvisioningKeys) .innerJoin( @@ -150,6 +152,11 @@ export async function registerNewt( createHttpError(HttpCode.NOT_FOUND, "Organization not found") ); } + if (!org.subnet) { + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Organization subnet not found") + ); + } // SaaS billing check if (build == "saas") { @@ -188,15 +195,31 @@ export async function registerNewt( let newSiteId: number | undefined; await db.transaction(async (trx) => { + + const newClientAddress = await getNextAvailableClientSubnet(orgId); + if (!newClientAddress) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "No available subnet found" + ) + ); + } + + let clientAddress = newClientAddress.split("/")[0]; + clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org + // Create the site (type "newt", name = niceId) const [newSite] = await trx .insert(sites) .values({ orgId, - name: niceId, + name: name || niceId, niceId, + address: clientAddress, type: "newt", - dockerSocketEnabled: true + dockerSocketEnabled: true, + status: keyRecord.approveNewSites ? "approved" : "pending", }) .returning(); diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 26dbff1bd..01495de3b 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -20,6 +20,7 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils"; import { Alias } from "@server/lib/ip"; import { build } from "@server/build"; import { canCompress } from "@server/lib/clientVersionChecks"; +import config from "@server/lib/config"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -274,7 +275,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // TODO: I still think there is a better way to do this rather than locking it out here but ??? if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) { logger.warn( - "Client last hole punch is too old and we have sites to send; skipping this register" + `Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?` ); return; } diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 5049ac1fa..4eca9a9a6 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -34,6 +34,10 @@ const updateOrgBodySchema = z .min(build === "saas" ? 0 : -1) .optional(), settingsLogRetentionDaysAction: z + .number() + .min(build === "saas" ? 0 : -1) + .optional(), + settingsLogRetentionDaysConnection: z .number() .min(build === "saas" ? 0 : -1) .optional() @@ -164,6 +168,17 @@ export async function updateOrg( ) ); } + if ( + parsedBody.data.settingsLogRetentionDaysConnection !== undefined && + parsedBody.data.settingsLogRetentionDaysConnection > maxRetentionDays + ) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription` + ) + ); + } } } @@ -179,7 +194,9 @@ export async function updateOrg( settingsLogRetentionDaysAccess: parsedBody.data.settingsLogRetentionDaysAccess, settingsLogRetentionDaysAction: - parsedBody.data.settingsLogRetentionDaysAction + parsedBody.data.settingsLogRetentionDaysAction, + settingsLogRetentionDaysConnection: + parsedBody.data.settingsLogRetentionDaysConnection }) .where(eq(orgs.orgId, orgId)) .returning(); @@ -197,6 +214,7 @@ export async function updateOrg( await cache.del(`org_${orgId}_retentionDays`); await cache.del(`org_${orgId}_actionDays`); await cache.del(`org_${orgId}_accessDays`); + await cache.del(`org_${orgId}_connectionDays`); return response(res, { data: updatedOrg[0], diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 6cff4d23a..f026166a6 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, loginPage } from "@server/db"; +import { db, domainNamespaces, loginPage } from "@server/db"; import { domains, orgDomains, @@ -24,6 +24,8 @@ import { build } from "@server/build"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; +import { isSubscribed } from "#dynamic/lib/isSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const createResourceParamsSchema = z.strictObject({ orgId: z.string() @@ -112,7 +114,10 @@ export async function createResource( const { orgId } = parsedParams.data; - if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) { + if ( + req.user && + (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0) + ) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); @@ -193,6 +198,29 @@ async function createHttpResource( const subdomain = parsedBody.data.subdomain; const stickySession = parsedBody.data.stickySession; + if (build == "saas" && !isSubscribed(orgId!, tierMatrix.domainNamespaces)) { + // grandfather in existing users + const lastAllowedDate = new Date("2026-04-13"); + const userCreatedDate = new Date(req.user?.dateCreated || new Date()); + if (userCreatedDate > lastAllowedDate) { + // check if this domain id is a namespace domain and if so, reject + const domain = await db + .select() + .from(domainNamespaces) + .where(eq(domainNamespaces.domainId, domainId)) + .limit(1); + + if (domain.length > 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature." + ) + ); + } + } + } + // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( domainId, diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index b2ce2ee7c..200ee07d4 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -14,10 +14,11 @@ import { isValidUrlGlobPattern } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; +import { isValidRegionId } from "@server/db/regions"; const createResourceRuleSchema = z.strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]), value: z.string().min(1), priority: z.int(), enabled: z.boolean().optional() @@ -126,6 +127,15 @@ export async function createResourceRule( ) ); } + } else if (match === "REGION") { + if (!isValidRegionId(value)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid region ID provided" + ) + ); + } } // Create the new resource rule diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 9afd6b4f3..802fffb1b 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -142,6 +142,7 @@ export async function getUserResources( let siteResourcesData: Array<{ siteResourceId: number; name: string; + niceId: string; destination: string; mode: string; protocol: string | null; @@ -154,6 +155,7 @@ export async function getUserResources( .select({ siteResourceId: siteResources.siteResourceId, name: siteResources.name, + niceId: siteResources.niceId, destination: siteResources.destination, mode: siteResources.mode, protocol: siteResources.protocol, @@ -249,7 +251,7 @@ export async function getUserResources( }); return response(res, { - data: { + data: { resources: resourcesWithAuth, siteResources: siteResourcesFormatted }, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 3ada13d85..12e98a70d 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -22,6 +22,7 @@ export * from "./deleteResourceRule"; export * from "./listResourceRules"; export * from "./updateResourceRule"; export * from "./getUserResources"; +export * from "./listUserResourceAliases"; export * from "./setResourceHeaderAuth"; export * from "./addEmailToResourceWhitelist"; export * from "./removeEmailFromResourceWhitelist"; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index fa7ec8a48..d1accfc9d 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -6,6 +6,7 @@ import { resourcePincode, resources, roleResources, + sites, targetHealthCheck, targets, userResources @@ -138,6 +139,7 @@ export type ResourceWithTargets = { port: number; enabled: boolean; healthStatus: "healthy" | "unhealthy" | "unknown" | null; + siteName: string | null; }>; }; @@ -446,14 +448,16 @@ export async function listResources( port: targets.port, enabled: targets.enabled, healthStatus: targetHealthCheck.hcHealth, - hcEnabled: targetHealthCheck.hcEnabled + hcEnabled: targetHealthCheck.hcEnabled, + siteName: sites.name }) .from(targets) .where(inArray(targets.resourceId, resourceIdList)) .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) - ); + ) + .leftJoin(sites, eq(targets.siteId, sites.siteId)); // avoids TS issues with reduce/never[] const map = new Map(); diff --git a/server/routers/resource/listUserResourceAliases.ts b/server/routers/resource/listUserResourceAliases.ts new file mode 100644 index 000000000..663700e64 --- /dev/null +++ b/server/routers/resource/listUserResourceAliases.ts @@ -0,0 +1,262 @@ +import { Request, Response, NextFunction } from "express"; +import { + db, + siteResources, + userSiteResources, + roleSiteResources, + userOrgRoles, + userOrgs +} from "@server/db"; +import { and, eq, inArray, asc, isNotNull, ne } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import { z } from "zod"; +import { fromZodError } from "zod-validation-error"; +import type { PaginatedResponse } from "@server/types/Pagination"; +import { OpenAPITags, registry } from "@server/openApi"; +import { localCache } from "#dynamic/lib/cache"; + +const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60; + +function userResourceAliasesCacheKey( + orgId: string, + userId: string, + page: number, + pageSize: number +) { + return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}`; +} + +const listUserResourceAliasesParamsSchema = z.strictObject({ + orgId: z.string() +}); + +const listUserResourceAliasesQuerySchema = z.object({ + pageSize: z.coerce + .number() + .int() + .positive() + .optional() + .catch(20) + .default(20) + .openapi({ + type: "integer", + default: 20, + description: "Number of items per page" + }), + page: z.coerce + .number() + .int() + .min(0) + .optional() + .catch(1) + .default(1) + .openapi({ + type: "integer", + default: 1, + description: "Page number to retrieve" + }) +}); + +export type ListUserResourceAliasesResponse = PaginatedResponse<{ + aliases: string[]; +}>; + +// registry.registerPath({ +// method: "get", +// path: "/org/{orgId}/user-resource-aliases", +// description: +// "List private (host-mode) site resource aliases the authenticated user can access in the organization, paginated.", +// tags: [OpenAPITags.PrivateResource], +// request: { +// params: z.object({ +// orgId: z.string() +// }), +// query: listUserResourceAliasesQuerySchema +// }, +// responses: {} +// }); + +export async function listUserResourceAliases( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listUserResourceAliasesQuerySchema.safeParse( + req.query + ); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedQuery.error) + ) + ); + } + const { page, pageSize } = parsedQuery.data; + + const parsedParams = listUserResourceAliasesParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedParams.error) + ) + ); + } + + const { orgId } = parsedParams.data; + const userId = req.user?.userId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + const [userOrg] = await db + .select() + .from(userOrgs) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) + .limit(1); + + if (!userOrg) { + return next( + createHttpError(HttpCode.FORBIDDEN, "User not in organization") + ); + } + + const cacheKey = userResourceAliasesCacheKey( + orgId, + userId, + page, + pageSize + ); + const cachedData: ListUserResourceAliasesResponse | undefined = + localCache.get(cacheKey); + + if (cachedData) { + return response(res, { + data: cachedData, + success: true, + error: false, + message: "User resource aliases retrieved successfully", + status: HttpCode.OK + }); + } + + const userRoleIds = await db + .select({ roleId: userOrgRoles.roleId }) + .from(userOrgRoles) + .where( + and( + eq(userOrgRoles.userId, userId), + eq(userOrgRoles.orgId, orgId) + ) + ) + .then((rows) => rows.map((r) => r.roleId)); + + const directSiteResourcesQuery = db + .select({ siteResourceId: userSiteResources.siteResourceId }) + .from(userSiteResources) + .where(eq(userSiteResources.userId, userId)); + + const roleSiteResourcesQuery = + userRoleIds.length > 0 + ? db + .select({ + siteResourceId: roleSiteResources.siteResourceId + }) + .from(roleSiteResources) + .where(inArray(roleSiteResources.roleId, userRoleIds)) + : Promise.resolve([]); + + const [directSiteResourceResults, roleSiteResourceResults] = + await Promise.all([ + directSiteResourcesQuery, + roleSiteResourcesQuery + ]); + + const accessibleSiteResourceIds = [ + ...directSiteResourceResults.map((r) => r.siteResourceId), + ...roleSiteResourceResults.map((r) => r.siteResourceId) + ]; + + if (accessibleSiteResourceIds.length === 0) { + const data: ListUserResourceAliasesResponse = { + aliases: [], + pagination: { + total: 0, + pageSize, + page + } + }; + localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC); + return response(res, { + data, + success: true, + error: false, + message: "User resource aliases retrieved successfully", + status: HttpCode.OK + }); + } + + const whereClause = and( + eq(siteResources.orgId, orgId), + eq(siteResources.enabled, true), + eq(siteResources.mode, "host"), + isNotNull(siteResources.alias), + ne(siteResources.alias, ""), + inArray(siteResources.siteResourceId, accessibleSiteResourceIds) + ); + + const baseSelect = () => + db + .select({ alias: siteResources.alias }) + .from(siteResources) + .where(whereClause); + + const countQuery = db.$count(baseSelect().as("filtered_aliases")); + + const [rows, totalCount] = await Promise.all([ + baseSelect() + .orderBy(asc(siteResources.alias)) + .limit(pageSize) + .offset(pageSize * (page - 1)), + countQuery + ]); + + const aliases = rows.map((r) => r.alias as string); + + const data: ListUserResourceAliasesResponse = { + aliases, + pagination: { + total: totalCount, + pageSize, + page + } + }; + localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC); + + return response(res, { + data, + success: true, + error: false, + message: "User resource aliases retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Internal server error" + ) + ); + } +} diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 01f3e79ff..21a923704 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, loginPage } from "@server/db"; +import { db, domainNamespaces, loginPage } from "@server/db"; import { domains, Org, @@ -25,6 +25,7 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { isSubscribed } from "#dynamic/lib/isSubscribed"; const updateResourceParamsSchema = z.strictObject({ resourceId: z.string().transform(Number).pipe(z.int().positive()) @@ -120,7 +121,9 @@ const updateHttpResourceBodySchema = z if (data.headers) { // HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230) const validHeaderValue = /^[\t\x20-\x7E]*$/; - return data.headers.every((h) => validHeaderValue.test(h.value)); + return data.headers.every((h) => + validHeaderValue.test(h.value) + ); } return true; }, @@ -318,6 +321,34 @@ async function updateHttpResource( if (updateData.domainId) { const domainId = updateData.domainId; + if ( + build == "saas" && + !isSubscribed(resource.orgId, tierMatrix.domainNamespaces) + ) { + // grandfather in existing users + const lastAllowedDate = new Date("2026-04-13"); + const userCreatedDate = new Date( + req.user?.dateCreated || new Date() + ); + if (userCreatedDate > lastAllowedDate) { + // check if this domain id is a namespace domain and if so, reject + const domain = await db + .select() + .from(domainNamespaces) + .where(eq(domainNamespaces.domainId, domainId)) + .limit(1); + + if (domain.length > 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature." + ) + ); + } + } + } + // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( domainId, @@ -366,7 +397,7 @@ async function updateHttpResource( ); } } - + if (build != "oss") { const existingLoginPages = await db .select() diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index 3e8f395dd..4074fd93a 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -14,6 +14,7 @@ import { isValidUrlGlobPattern } from "@server/lib/validators"; import { OpenAPITags, registry } from "@server/openApi"; +import { isValidRegionId } from "@server/db/regions"; // Define Zod schema for request parameters validation const updateResourceRuleParamsSchema = z.strictObject({ @@ -25,7 +26,7 @@ const updateResourceRuleParamsSchema = z.strictObject({ const updateResourceRuleSchema = z .strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]).optional(), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]).optional(), value: z.string().min(1).optional(), priority: z.int(), enabled: z.boolean().optional() @@ -166,6 +167,15 @@ export async function updateResourceRule( ) ); } + } else if (match === "REGION") { + if (!isValidRegionId(value)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid region ID provided" + ) + ); + } } } diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 4edebb080..d397b2784 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -298,7 +298,8 @@ export async function createSite( niceId, address: updatedAddress || null, type, - dockerSocketEnabled: true + dockerSocketEnabled: true, + status: "approved" }) .returning(); } else if (type == "wireguard") { @@ -355,7 +356,8 @@ export async function createSite( niceId, subnet, type, - pubKey: pubKey || null + pubKey: pubKey || null, + status: "approved" }) .returning(); } else if (type == "local") { @@ -370,7 +372,8 @@ export async function createSite( type, dockerSocketEnabled: false, online: true, - subnet: "0.0.0.0/32" + subnet: "0.0.0.0/32", + status: "approved" }) .returning(); } else { diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index a244c650c..b65182908 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -21,6 +21,11 @@ import semver from "semver"; import { z } from "zod"; import { fromError } from "zod-validation-error"; +// Stale-while-revalidate: keeps the last successfully fetched version so that +// a transient network failure / timeout does not flip every site back to +// newtUpdateAvailable: false. +let staleNewtVersion: string | null = null; + async function getLatestNewtVersion(): Promise { try { const cachedVersion = await cache.get("latestNewtVersion"); @@ -29,7 +34,7 @@ async function getLatestNewtVersion(): Promise { } const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 1500); // Reduced timeout to 1.5 seconds + const timeoutId = setTimeout(() => controller.abort(), 1500); const response = await fetch( "https://api.github.com/repos/fosrl/newt/tags", @@ -44,18 +49,46 @@ async function getLatestNewtVersion(): Promise { logger.warn( `Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}` ); - return null; + return staleNewtVersion; } let tags = await response.json(); if (!Array.isArray(tags) || tags.length === 0) { logger.warn("No tags found for Newt repository"); - return null; + return staleNewtVersion; } - tags = tags.filter((version) => !version.name.includes("rc")); + + // Remove release-candidates, then sort descending by semver so that + // duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks + // from the GitHub API do not cause an older tag to be selected. + tags = tags.filter((tag: any) => !tag.name.includes("rc")); + tags.sort((a: any, b: any) => { + const va = semver.coerce(a.name); + const vb = semver.coerce(b.name); + if (!va && !vb) return 0; + if (!va) return 1; + if (!vb) return -1; + return semver.rcompare(va, vb); + }); + + // Deduplicate: keep only the first (highest) entry per normalised version + const seen = new Set(); + tags = tags.filter((tag: any) => { + const normalised = semver.coerce(tag.name)?.version; + if (!normalised || seen.has(normalised)) return false; + seen.add(normalised); + return true; + }); + + if (tags.length === 0) { + logger.warn("No valid semver tags found for Newt repository"); + return staleNewtVersion; + } + const latestVersion = tags[0].name; - await cache.set("latestNewtVersion", latestVersion, 3600); + staleNewtVersion = latestVersion; + await cache.set("cache:latestNewtVersion", latestVersion, 3600); return latestVersion; } catch (error: any) { @@ -73,7 +106,7 @@ async function getLatestNewtVersion(): Promise { error.message || error ); } - return null; + return staleNewtVersion; } } @@ -135,6 +168,15 @@ const listSitesSchema = z.object({ .openapi({ type: "boolean", description: "Filter by online status" + }), + status: z + .enum(["pending", "approved"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["pending", "approved"], + description: "Filter by site status" }) }); @@ -156,7 +198,8 @@ function querySitesBase() { exitNodeId: sites.exitNodeId, exitNodeName: exitNodes.name, exitNodeEndpoint: exitNodes.endpoint, - remoteExitNodeId: remoteExitNodes.remoteExitNodeId + remoteExitNodeId: remoteExitNodes.remoteExitNodeId, + status: sites.status }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) @@ -245,7 +288,7 @@ export async function listSites( .where(eq(sites.orgId, orgId)); } - const { pageSize, page, query, sort_by, order, online } = + const { pageSize, page, query, sort_by, order, online, status } = parsedQuery.data; const accessibleSiteIds = accessibleSites.map((site) => site.siteId); @@ -273,6 +316,9 @@ export async function listSites( if (typeof online !== "undefined") { conditions.push(eq(sites.online, online)); } + if (typeof status !== "undefined") { + conditions.push(eq(sites.status, status)); + } const baseQuery = querySitesBase().where(and(...conditions)); diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index ca0f76783..34d1341d7 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -19,7 +19,8 @@ const updateSiteBodySchema = z .strictObject({ name: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(), - dockerSocketEnabled: z.boolean().optional() + dockerSocketEnabled: z.boolean().optional(), + status: z.enum(["pending", "approved"]).optional(), // remoteSubnets: z.string().optional() // subdomain: z // .string() diff --git a/server/routers/siteProvisioning/types.ts b/server/routers/siteProvisioning/types.ts index d06c1fe26..785d9dfff 100644 --- a/server/routers/siteProvisioning/types.ts +++ b/server/routers/siteProvisioning/types.ts @@ -8,6 +8,7 @@ export type SiteProvisioningKeyListItem = { maxBatchSize: number | null; numUsed: number; validUntil: string | null; + approveNewSites: boolean; }; export type ListSiteProvisioningKeysResponse = { @@ -26,6 +27,7 @@ export type CreateSiteProvisioningKeyResponse = { maxBatchSize: number | null; numUsed: number; validUntil: string | null; + approveNewSites: boolean; }; export type UpdateSiteProvisioningKeyResponse = { @@ -38,4 +40,5 @@ export type UpdateSiteProvisioningKeyResponse = { maxBatchSize: number | null; numUsed: number; validUntil: string | null; + approveNewSites: boolean; }; diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 8f56ece0f..ab70d0fce 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -618,11 +618,11 @@ export async function handleMessagingForUpdatedSiteResource( // Only update targets on newt if destination changed if (destinationChanged || portRangesChanged) { - const oldTarget = generateSubnetProxyTargetV2( + const oldTargets = generateSubnetProxyTargetV2( existingSiteResource, mergedAllClients ); - const newTarget = generateSubnetProxyTargetV2( + const newTargets = generateSubnetProxyTargetV2( updatedSiteResource, mergedAllClients ); @@ -630,8 +630,8 @@ export async function handleMessagingForUpdatedSiteResource( await updateTargets( newt.newtId, { - oldTargets: oldTarget ? [oldTarget] : [], - newTargets: newTarget ? [newTarget] : [] + oldTargets: oldTargets ? oldTargets : [], + newTargets: newTargets ? newTargets : [] }, newt.version ); diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index 01cbdea81..7ea1730ce 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -77,7 +77,8 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( const [targetCheck] = await db .select({ targetId: targets.targetId, - siteId: targets.siteId + siteId: targets.siteId, + hcStatus: targetHealthCheck.hcHealth }) .from(targets) .innerJoin( @@ -85,6 +86,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( eq(targets.resourceId, resources.resourceId) ) .innerJoin(sites, eq(targets.siteId, sites.siteId)) + .innerJoin(targetHealthCheck, eq(targets.targetId, targetHealthCheck.targetId)) .where( and( eq(targets.targetId, targetIdNum), @@ -101,6 +103,14 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( continue; } + // check if the status has changed + if (targetCheck.hcStatus === healthStatus.status) { + logger.debug( + `Health status for target ${targetId} is already ${healthStatus.status}, skipping update` + ); + continue; + } + // Update the target's health status in the database await db .update(targetHealthCheck) diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index dd31f5f1b..1f9eff716 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -188,6 +188,8 @@ export async function updateTarget( ); } + const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null; + const [updatedTarget] = await db .update(targets) .set({ @@ -200,8 +202,8 @@ export async function updateTarget( path: parsedBody.data.path, pathMatchType: parsedBody.data.pathMatchType, priority: parsedBody.data.priority, - rewritePath: parsedBody.data.rewritePath, - rewritePathType: parsedBody.data.rewritePathType + rewritePath: pathMatchTypeRemoved ? null : parsedBody.data.rewritePath, + rewritePathType: pathMatchTypeRemoved ? null : parsedBody.data.rewritePathType }) .where(eq(targets.targetId, targetId)) .returning(); diff --git a/server/routers/user/getUser.ts b/server/routers/user/getUser.ts index e33daab60..9ff52fd2d 100644 --- a/server/routers/user/getUser.ts +++ b/server/routers/user/getUser.ts @@ -20,7 +20,9 @@ async function queryUser(userId: string) { emailVerified: users.emailVerified, serverAdmin: users.serverAdmin, idpName: idp.name, - idpId: users.idpId + idpId: users.idpId, + locale: users.locale, + dateCreated: users.dateCreated }) .from(users) .leftJoin(idp, eq(users.idpId, idp.idpId)) diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index e03676caa..690a013f6 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -17,4 +17,5 @@ export * from "./createOrgUser"; export * from "./adminUpdateUser2FA"; export * from "./adminGetUser"; export * from "./updateOrgUser"; +export * from "./updateUserLocale"; export * from "./myDevice"; diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 7ac1849b9..b11586e69 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -1,7 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { orgs, roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db"; +import { + orgs, + roles, + userInviteRoles, + userInvites, + userOrgs, + users +} from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -37,8 +44,7 @@ const inviteUserBodySchema = z regenerate: z.boolean().optional() }) .refine( - (d) => - (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null, + (d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null, { message: "roleIds or roleId is required", path: ["roleIds"] } ) .transform((data) => ({ @@ -265,7 +271,7 @@ export async function inviteUser( ) ); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; if (doEmail) { await sendEmail( @@ -314,12 +320,12 @@ export async function inviteUser( expiresAt, tokenHash }); - await trx.insert(userInviteRoles).values( - uniqueRoleIds.map((roleId) => ({ inviteId, roleId })) - ); + await trx + .insert(userInviteRoles) + .values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId }))); }); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; if (doEmail) { await sendEmail( diff --git a/server/routers/user/myDevice.ts b/server/routers/user/myDevice.ts index 3b991ca56..1a767d4db 100644 --- a/server/routers/user/myDevice.ts +++ b/server/routers/user/myDevice.ts @@ -63,7 +63,9 @@ export async function myDevice( emailVerified: users.emailVerified, serverAdmin: users.serverAdmin, idpName: idp.name, - idpId: users.idpId + idpId: users.idpId, + locale: users.locale, + dateCreated: users.dateCreated }) .from(users) .leftJoin(idp, eq(users.idpId, idp.idpId)) diff --git a/server/routers/user/updateUserLocale.ts b/server/routers/user/updateUserLocale.ts new file mode 100644 index 000000000..6c28ce067 --- /dev/null +++ b/server/routers/user/updateUserLocale.ts @@ -0,0 +1,57 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { users } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const bodySchema = z.strictObject({ + locale: z.string().min(2).max(10) +}); + +export async function updateUserLocale( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const userId = req.user?.userId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not found") + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { locale } = parsedBody.data; + + await db.update(users).set({ locale }).where(eq(users.userId, userId)); + + return response(res, { + data: null, + success: true, + error: false, + message: "User locale updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/setup/scriptsPg/1.17.0.ts b/server/setup/scriptsPg/1.17.0.ts index 81c42e1a9..c92152863 100644 --- a/server/setup/scriptsPg/1.17.0.ts +++ b/server/setup/scriptsPg/1.17.0.ts @@ -20,9 +20,82 @@ export default async function migration() { `Found ${existingUserOrgRoles.length} existing userOrgs role assignment(s) to migrate` ); + // Query existing roleId data from userInvites before the transaction destroys it + const existingInviteRolesQuery = await db.execute( + sql`SELECT "inviteId", "roleId" FROM "userInvites" WHERE "roleId" IS NOT NULL` + ); + const existingUserInviteRoles = existingInviteRolesQuery.rows as { + inviteId: string; + roleId: number; + }[]; + + console.log( + `Found ${existingUserInviteRoles.length} existing userInvites role assignment(s) to migrate` + ); + try { await db.execute(sql`BEGIN`); + await db.execute(sql` + CREATE TABLE "bannedEmails" ( + "email" varchar(255) PRIMARY KEY NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "bannedIps" ( + "ip" varchar(255) PRIMARY KEY NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "connectionAuditLog" ( + "id" serial PRIMARY KEY NOT NULL, + "sessionId" text NOT NULL, + "siteResourceId" integer, + "orgId" text, + "siteId" integer, + "clientId" integer, + "userId" text, + "sourceAddr" text NOT NULL, + "destAddr" text NOT NULL, + "protocol" text NOT NULL, + "startedAt" integer NOT NULL, + "endedAt" integer, + "bytesTx" integer, + "bytesRx" integer + ); + `); + + await db.execute(sql` + CREATE TABLE "siteProvisioningKeyOrg" ( + "siteProvisioningKeyId" varchar(255) NOT NULL, + "orgId" varchar(255) NOT NULL, + CONSTRAINT "siteProvisioningKeyOrg_siteProvisioningKeyId_orgId_pk" PRIMARY KEY("siteProvisioningKeyId","orgId") + ); + `); + await db.execute(sql` + CREATE TABLE "siteProvisioningKeys" ( + "siteProvisioningKeyId" varchar(255) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "siteProvisioningKeyHash" text NOT NULL, + "lastChars" varchar(4) NOT NULL, + "dateCreated" varchar(255) NOT NULL, + "lastUsed" varchar(255), + "maxBatchSize" integer, + "numUsed" integer DEFAULT 0 NOT NULL, + "validUntil" varchar(255) + ); + `); + + await db.execute(sql` + CREATE TABLE "userInviteRoles" ( + "inviteId" varchar NOT NULL, + "roleId" integer NOT NULL, + CONSTRAINT "userInviteRoles_inviteId_roleId_pk" PRIMARY KEY("inviteId","roleId") + ); + `); + await db.execute(sql` CREATE TABLE "userOrgRoles" ( "userId" varchar NOT NULL, @@ -31,12 +104,122 @@ export default async function migration() { CONSTRAINT "userOrgRoles_userId_orgId_roleId_unique" UNIQUE("userId","orgId","roleId") ); `); - await db.execute(sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";`); - await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`); - await db.execute(sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql` + CREATE TABLE "eventStreamingCursors" ( + "cursorId" serial PRIMARY KEY NOT NULL, + "destinationId" integer NOT NULL, + "logType" varchar(50) NOT NULL, + "lastSentId" bigint DEFAULT 0 NOT NULL, + "lastSentAt" bigint + ); + `); + + await db.execute(sql` + CREATE TABLE "eventStreamingDestinations" ( + "destinationId" serial PRIMARY KEY NOT NULL, + "orgId" varchar(255) NOT NULL, + "sendConnectionLogs" boolean DEFAULT false NOT NULL, + "sendRequestLogs" boolean DEFAULT false NOT NULL, + "sendActionLogs" boolean DEFAULT false NOT NULL, + "sendAccessLogs" boolean DEFAULT false NOT NULL, + "type" varchar(50) NOT NULL, + "config" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL + ); + `); + + await db.execute( + sql`ALTER TABLE "eventStreamingCursors" ADD CONSTRAINT "eventStreamingCursors_destinationId_eventStreamingDestinations_destinationId_fk" FOREIGN KEY ("destinationId") REFERENCES "public"."eventStreamingDestinations"("destinationId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "eventStreamingDestinations" ADD CONSTRAINT "eventStreamingDestinations_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`CREATE UNIQUE INDEX "idx_eventStreamingCursors_dest_type" ON "eventStreamingCursors" USING btree ("destinationId","logType");` + ); + await db.execute( + sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";` + ); + await db.execute( + sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "userOrgRoles" ADD CONSTRAINT "userOrgRoles_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;` + ); await db.execute(sql`ALTER TABLE "userOrgs" DROP COLUMN "roleId";`); + await db.execute( + sql`ALTER TABLE "userInvites" DROP CONSTRAINT "userInvites_roleId_roles_roleId_fk";` + ); + await db.execute( + sql`ALTER TABLE "accessAuditLog" ADD COLUMN "siteResourceId" integer;` + ); + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" ADD COLUMN "isJitMode" boolean DEFAULT false NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "domains" ADD COLUMN "errorMessage" text;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysConnection" integer DEFAULT 0 NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "sites" ADD COLUMN "lastPing" integer;` + ); + await db.execute( + sql`ALTER TABLE "user" ADD COLUMN "marketingEmailConsent" boolean DEFAULT false;` + ); + await db.execute(sql`ALTER TABLE "user" ADD COLUMN "locale" varchar;`); + await db.execute( + sql`ALTER TABLE "connectionAuditLog" ADD CONSTRAINT "connectionAuditLog_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "connectionAuditLog" ADD CONSTRAINT "connectionAuditLog_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "connectionAuditLog" ADD CONSTRAINT "connectionAuditLog_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "connectionAuditLog" ADD CONSTRAINT "connectionAuditLog_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "connectionAuditLog" ADD CONSTRAINT "connectionAuditLog_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "siteProvisioningKeyOrg" ADD CONSTRAINT "siteProvisioningKeyOrg_siteProvisioningKeyId_siteProvisioningKeys_siteProvisioningKeyId_fk" FOREIGN KEY ("siteProvisioningKeyId") REFERENCES "public"."siteProvisioningKeys"("siteProvisioningKeyId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "siteProvisioningKeyOrg" ADD CONSTRAINT "siteProvisioningKeyOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "userInviteRoles" ADD CONSTRAINT "userInviteRoles_inviteId_userInvites_inviteId_fk" FOREIGN KEY ("inviteId") REFERENCES "public"."userInvites"("inviteId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`ALTER TABLE "userInviteRoles" ADD CONSTRAINT "userInviteRoles_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;` + ); + await db.execute( + sql`CREATE INDEX "idx_accessAuditLog_startedAt" ON "connectionAuditLog" USING btree ("startedAt");` + ); + await db.execute( + sql`CREATE INDEX "idx_accessAuditLog_org_startedAt" ON "connectionAuditLog" USING btree ("orgId","startedAt");` + ); + await db.execute( + sql`CREATE INDEX "idx_accessAuditLog_siteResourceId" ON "connectionAuditLog" USING btree ("siteResourceId");` + ); + await db.execute(sql`ALTER TABLE "userInvites" DROP COLUMN "roleId";`); + await db.execute( + sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;` + ); + await db.execute( + sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';` + ); + await db.execute(sql`COMMIT`); console.log("Migrated database"); } catch (e) { @@ -46,13 +229,41 @@ export default async function migration() { throw e; } + // Re-insert the preserved invite role assignments into the new userInviteRoles table + if (existingUserInviteRoles.length > 0) { + try { + for (const row of existingUserInviteRoles) { + await db.execute(sql` + INSERT INTO "userInviteRoles" ("inviteId", "roleId") + SELECT ${row.inviteId}, ${row.roleId} + WHERE EXISTS (SELECT 1 FROM "userInvites" WHERE "inviteId" = ${row.inviteId}) + AND EXISTS (SELECT 1 FROM "roles" WHERE "roleId" = ${row.roleId}) + ON CONFLICT DO NOTHING + `); + } + + console.log( + `Migrated ${existingUserInviteRoles.length} role assignment(s) into userInviteRoles` + ); + } catch (e) { + console.error( + "Error while migrating role assignments into userInviteRoles:", + e + ); + throw e; + } + } + // Re-insert the preserved role assignments into the new userOrgRoles table if (existingUserOrgRoles.length > 0) { try { for (const row of existingUserOrgRoles) { await db.execute(sql` INSERT INTO "userOrgRoles" ("userId", "orgId", "roleId") - VALUES (${row.userId}, ${row.orgId}, ${row.roleId}) + SELECT ${row.userId}, ${row.orgId}, ${row.roleId} + WHERE EXISTS (SELECT 1 FROM "user" WHERE "id" = ${row.userId}) + AND EXISTS (SELECT 1 FROM "orgs" WHERE "orgId" = ${row.orgId}) + AND EXISTS (SELECT 1 FROM "roles" WHERE "roleId" = ${row.roleId}) ON CONFLICT DO NOTHING `); } @@ -70,4 +281,4 @@ export default async function migration() { } console.log(`${version} migration complete`); -} \ No newline at end of file +} diff --git a/server/setup/scriptsSqlite/1.17.0.ts b/server/setup/scriptsSqlite/1.17.0.ts index fe7d82de0..ecf40b7a8 100644 --- a/server/setup/scriptsSqlite/1.17.0.ts +++ b/server/setup/scriptsSqlite/1.17.0.ts @@ -24,7 +24,95 @@ export default async function migration() { `Found ${existingUserOrgRoles.length} existing userOrgs role assignment(s) to migrate` ); + // Query existing roleId data from userInvites before the transaction destroys it + const existingUserInviteRoles = db + .prepare( + `SELECT "inviteId", "roleId" FROM 'userInvites' WHERE "roleId" IS NOT NULL` + ) + .all() as { inviteId: string; roleId: number }[]; + + console.log( + `Found ${existingUserInviteRoles.length} existing userInvites role assignment(s) to migrate` + ); + db.transaction(() => { + db.prepare( + ` + CREATE TABLE 'bannedEmails' ( + 'email' text PRIMARY KEY NOT NULL + ); + ` + ).run(); + db.prepare( + ` + CREATE TABLE 'bannedIps' ( + 'ip' text PRIMARY KEY NOT NULL + ); + ` + ).run(); + db.prepare( + ` + CREATE TABLE 'connectionAuditLog' ( + 'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'sessionId' text NOT NULL, + 'siteResourceId' integer, + 'orgId' text, + 'siteId' integer, + 'clientId' integer, + 'userId' text, + 'sourceAddr' text NOT NULL, + 'destAddr' text NOT NULL, + 'protocol' text NOT NULL, + 'startedAt' integer NOT NULL, + 'endedAt' integer, + 'bytesTx' integer, + 'bytesRx' integer, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');` + ).run(); + db.prepare( + `CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');` + ).run(); + db.prepare( + `CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');` + ).run(); + + db.prepare( + ` + CREATE TABLE 'siteProvisioningKeyOrg' ( + 'siteProvisioningKeyId' text NOT NULL, + 'orgId' text NOT NULL, + PRIMARY KEY('siteProvisioningKeyId', 'orgId'), + FOREIGN KEY ('siteProvisioningKeyId') REFERENCES 'siteProvisioningKeys'('siteProvisioningKeyId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + db.prepare( + ` + CREATE TABLE 'siteProvisioningKeys' ( + 'siteProvisioningKeyId' text PRIMARY KEY NOT NULL, + 'name' text NOT NULL, + 'siteProvisioningKeyHash' text NOT NULL, + 'lastChars' text NOT NULL, + 'dateCreated' text NOT NULL, + 'lastUsed' text, + 'maxBatchSize' integer, + 'numUsed' integer DEFAULT 0 NOT NULL, + 'validUntil' text + ); + ` + ).run(); + db.prepare( ` CREATE TABLE 'userOrgRoles' ( @@ -57,25 +145,139 @@ export default async function migration() { ).run(); db.prepare( - `INSERT INTO '__new_userOrgs'("userId", "orgId", "isOwner", "autoProvisioned", "pamUsername") SELECT "userId", "orgId", "isOwner", "autoProvisioned", "pamUsername" FROM 'userOrgs';` + `INSERT INTO '__new_userOrgs'("userId", "orgId", "isOwner", "autoProvisioned", "pamUsername") SELECT "userId", "orgId", "isOwner", "autoProvisioned", "pamUsername" FROM 'userOrgs' WHERE EXISTS (SELECT 1 FROM 'user' WHERE id = userOrgs.userId) AND EXISTS (SELECT 1 FROM 'orgs' WHERE orgId = userOrgs.orgId);` ).run(); db.prepare(`DROP TABLE 'userOrgs';`).run(); db.prepare( `ALTER TABLE '__new_userOrgs' RENAME TO 'userOrgs';` ).run(); + db.prepare( + ` + CREATE TABLE 'userInviteRoles' ( + 'inviteId' text NOT NULL, + 'roleId' integer NOT NULL, + PRIMARY KEY('inviteId', 'roleId'), + FOREIGN KEY ('inviteId') REFERENCES 'userInvites'('inviteId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + db.prepare( + ` + CREATE TABLE '__new_userInvites' ( + 'inviteId' text PRIMARY KEY NOT NULL, + 'orgId' text NOT NULL, + 'email' text NOT NULL, + 'expiresAt' integer NOT NULL, + 'token' text NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'eventStreamingCursors' ( + 'cursorId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'destinationId' integer NOT NULL, + 'logType' text NOT NULL, + 'lastSentId' integer DEFAULT 0 NOT NULL, + 'lastSentAt' integer, + FOREIGN KEY ('destinationId') REFERENCES 'eventStreamingDestinations'('destinationId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + db.prepare( + ` + CREATE UNIQUE INDEX 'idx_eventStreamingCursors_dest_type' ON 'eventStreamingCursors' ('destinationId','logType');--> statement-breakpoint + ` + ).run(); + db.prepare( + ` + CREATE TABLE 'eventStreamingDestinations' ( + 'destinationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'orgId' text NOT NULL, + 'sendConnectionLogs' integer DEFAULT false NOT NULL, + 'sendRequestLogs' integer DEFAULT false NOT NULL, + 'sendActionLogs' integer DEFAULT false NOT NULL, + 'sendAccessLogs' integer DEFAULT false NOT NULL, + 'type' text NOT NULL, + 'config' text NOT NULL, + 'enabled' integer DEFAULT true NOT NULL, + 'createdAt' integer NOT NULL, + 'updatedAt' integer NOT NULL, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + db.prepare( + `INSERT INTO '__new_userInvites'("inviteId", "orgId", "email", "expiresAt", "token") SELECT "inviteId", "orgId", "email", "expiresAt", "token" FROM 'userInvites';` + ).run(); + db.prepare(`DROP TABLE 'userInvites';`).run(); + db.prepare( + `ALTER TABLE '__new_userInvites' RENAME TO 'userInvites';` + ).run(); + + db.prepare( + `ALTER TABLE 'accessAuditLog' ADD 'siteResourceId' integer;` + ).run(); + db.prepare( + `ALTER TABLE 'clientSitesAssociationsCache' ADD 'isJitMode' integer DEFAULT false NOT NULL;` + ).run(); + db.prepare(`ALTER TABLE 'domains' ADD 'errorMessage' text;`).run(); + db.prepare( + `ALTER TABLE 'orgs' ADD 'settingsLogRetentionDaysConnection' integer DEFAULT 0 NOT NULL;` + ).run(); + db.prepare(`ALTER TABLE 'sites' ADD 'lastPing' integer;`).run(); + db.prepare( + `ALTER TABLE 'user' ADD 'marketingEmailConsent' integer DEFAULT false;` + ).run(); + db.prepare(`ALTER TABLE 'user' ADD 'locale' text;`).run(); + db.prepare( + `ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;` + ).run(); + db.prepare( + `ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';` + ).run(); })(); db.pragma("foreign_keys = ON"); + // Re-insert the preserved invite role assignments into the new userInviteRoles table + if (existingUserInviteRoles.length > 0) { + const insertUserInviteRole = db.prepare( + `INSERT OR IGNORE INTO 'userInviteRoles' ("inviteId", "roleId") + SELECT ?, ? + WHERE EXISTS (SELECT 1 FROM 'userInvites' WHERE inviteId = ?) + AND EXISTS (SELECT 1 FROM 'roles' WHERE roleId = ?)` + ); + + const insertAll = db.transaction(() => { + for (const row of existingUserInviteRoles) { + insertUserInviteRole.run(row.inviteId, row.roleId, row.inviteId, row.roleId); + } + }); + + insertAll(); + + console.log( + `Migrated ${existingUserInviteRoles.length} role assignment(s) into userInviteRoles` + ); + } + // Re-insert the preserved role assignments into the new userOrgRoles table if (existingUserOrgRoles.length > 0) { const insertUserOrgRole = db.prepare( - `INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId") VALUES (?, ?, ?)` + `INSERT OR IGNORE INTO 'userOrgRoles' ("userId", "orgId", "roleId") + SELECT ?, ?, ? + WHERE EXISTS (SELECT 1 FROM 'user' WHERE id = ?) + AND EXISTS (SELECT 1 FROM 'orgs' WHERE orgId = ?) + AND EXISTS (SELECT 1 FROM 'roles' WHERE roleId = ?)` ); const insertAll = db.transaction(() => { for (const row of existingUserOrgRoles) { - insertUserOrgRole.run(row.userId, row.orgId, row.roleId); + insertUserOrgRole.run(row.userId, row.orgId, row.roleId, row.userId, row.orgId, row.roleId); } }); @@ -93,4 +295,4 @@ export default async function migration() { } console.log(`${version} migration complete`); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index ba08f6022..fdf0d252a 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -491,6 +491,10 @@ export default function BillingPage() { const currentPlanId = getCurrentPlanId(); + const visiblePlanOptions = planOptions.filter( + (plan) => plan.id !== "home" || currentPlanId === "home" + ); + // Check if subscription is in a problematic state that requires attention const hasProblematicSubscription = (): boolean => { if (!tierSubscription?.subscription) return false; @@ -803,8 +807,8 @@ export default function BillingPage() { {/* Plan Cards Grid */} -
- {planOptions.map((plan) => { +
+ {visiblePlanOptions.map((plan) => { const isCurrentPlan = plan.id === currentPlanId; const planAction = getPlanAction(plan); diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index 00cb0ffc8..ae37c3752 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -3,13 +3,12 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import InvitationsTable, { InvitationRow -} from "../../../../../components/InvitationsTable"; +} from "@app/components/InvitationsTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 812ac2b64..84685cc04 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -3,13 +3,12 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { ListUsersResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; -import UsersTable, { UserRow } from "../../../../../components/UsersTable"; +import UsersTable, { UserRow } from "@app/components/UsersTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx index 2973bb542..0ed9553af 100644 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -4,7 +4,7 @@ import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import OrgApiKeysTable, { OrgApiKeyRow -} from "../../../../components/OrgApiKeysTable"; +} from "@app/components/OrgApiKeysTable"; import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index cf23e81be..23a79737d 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -10,6 +10,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { GetDNSRecordsResponse } from "@server/routers/domain"; import DNSRecordsTable from "@app/components/DNSRecordTable"; import DomainCertForm from "@app/components/DomainCertForm"; +import { build } from "@server/build"; interface DomainSettingsPageProps { params: Promise<{ domainId: string; orgId: string }>; @@ -65,12 +66,14 @@ export default async function DomainSettingsPage({ )}
- + {build != "oss" && env.flags.usePangolinDns ? ( + + ) : null} diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx index 04db84b38..d1325d32b 100644 --- a/src/app/[orgId]/settings/domains/page.tsx +++ b/src/app/[orgId]/settings/domains/page.tsx @@ -2,7 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import DomainsTable, { DomainRow } from "../../../../components/DomainsTable"; +import DomainsTable, { DomainRow } from "@app/components/DomainsTable"; import { getTranslations } from "next-intl/server"; import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; diff --git a/src/app/[orgId]/settings/general/security/page.tsx b/src/app/[orgId]/settings/general/security/page.tsx index 2c51e9ecb..e7d0d85c8 100644 --- a/src/app/[orgId]/settings/general/security/page.tsx +++ b/src/app/[orgId]/settings/general/security/page.tsx @@ -79,7 +79,8 @@ const SecurityFormSchema = z.object({ passwordExpiryDays: z.number().nullable().optional(), settingsLogRetentionDaysRequest: z.number(), settingsLogRetentionDaysAccess: z.number(), - settingsLogRetentionDaysAction: z.number() + settingsLogRetentionDaysAction: z.number(), + settingsLogRetentionDaysConnection: z.number() }); const LOG_RETENTION_OPTIONS = [ @@ -120,7 +121,8 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { SecurityFormSchema.pick({ settingsLogRetentionDaysRequest: true, settingsLogRetentionDaysAccess: true, - settingsLogRetentionDaysAction: true + settingsLogRetentionDaysAction: true, + settingsLogRetentionDaysConnection: true }) ), defaultValues: { @@ -129,7 +131,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { settingsLogRetentionDaysAccess: org.settingsLogRetentionDaysAccess ?? 15, settingsLogRetentionDaysAction: - org.settingsLogRetentionDaysAction ?? 15 + org.settingsLogRetentionDaysAction ?? 15, + settingsLogRetentionDaysConnection: + org.settingsLogRetentionDaysConnection ?? 15 }, mode: "onChange" }); @@ -155,7 +159,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { settingsLogRetentionDaysAccess: data.settingsLogRetentionDaysAccess, settingsLogRetentionDaysAction: - data.settingsLogRetentionDaysAction + data.settingsLogRetentionDaysAction, + settingsLogRetentionDaysConnection: + data.settingsLogRetentionDaysConnection } as any; // Update organization @@ -473,6 +479,107 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { ); }} /> + { + const isDisabled = !isPaidUser( + tierMatrix.connectionLogs + ); + + return ( + + + {t( + "logRetentionConnectionLabel" + )} + + + + + + + ); + }} + /> )} diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index dbb7b6708..a0f1b5386 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -465,7 +465,11 @@ export default function GeneralPage() { cell: ({ row }) => { return ( + + + + + + onDelete(destination)} + > + {t("delete")} + + + +
+
+ ); +} + +// ── Add destination card ─────────────────────────────────────────────────────── + +function AddDestinationCard({ onClick }: { onClick: () => void }) { + const t = useTranslations(); + + return ( + + ); +} + +// ── Destination type picker ──────────────────────────────────────────────────── + +type DestinationType = "http" | "s3" | "datadog"; + +interface DestinationTypePickerProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSelect: (type: DestinationType) => void; + isPaywalled?: boolean; +} + +function DestinationTypePicker({ + open, + onOpenChange, + onSelect, + isPaywalled = false +}: DestinationTypePickerProps) { + const t = useTranslations(); + const [selected, setSelected] = useState("http"); + + const destinationTypeOptions: ReadonlyArray< + StrategyOption + > = [ + { + id: "http", + title: t("streamingHttpWebhookTitle"), + description: t("streamingHttpWebhookDescription"), + icon: + }, + { + id: "s3", + title: t("streamingS3Title"), + description: t("streamingS3Description"), + disabled: true, + icon: ( + {t("streamingS3Title")} + ) + }, + { + id: "datadog", + title: t("streamingDatadogTitle"), + description: t("streamingDatadogDescription"), + disabled: true, + icon: ( + {t("streamingDatadogTitle")} + ) + } + ]; + + useEffect(() => { + if (open) setSelected("http"); + }, [open]); + + return ( + + + + + {t("streamingAddDestination")} + + + {t("streamingTypePickerDescription")} + + + +
+ +
+
+ + + + + + +
+
+ ); +} + +// ── Main page ────────────────────────────────────────────────────────────────── + +export default function StreamingDestinationsPage() { + const { orgId } = useParams() as { orgId: string }; + const api = createApiClient(useEnvContext()); + const { isPaidUser } = usePaidStatus(); + const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]); + const t = useTranslations(); + + const [destinations, setDestinations] = useState([]); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [typePickerOpen, setTypePickerOpen] = useState(false); + const [editingDestination, setEditingDestination] = + useState(null); + const [togglingIds, setTogglingIds] = useState>(new Set()); + + // Delete state + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + + const loadDestinations = useCallback(async () => { + if (build == "oss") { + setDestinations([]); + setLoading(false); + return; + } + try { + const res = await api.get>( + `/org/${orgId}/event-streaming-destinations` + ); + setDestinations(res.data.data.destinations ?? []); + } catch (e) { + toast({ + variant: "destructive", + title: t("streamingFailedToLoad"), + description: formatAxiosError(e, t("streamingUnexpectedError")) + }); + } finally { + setLoading(false); + } + }, [orgId]); + + useEffect(() => { + loadDestinations(); + }, [loadDestinations]); + + const handleToggle = async (destinationId: number, enabled: boolean) => { + // Optimistic update + setDestinations((prev) => + prev.map((d) => + d.destinationId === destinationId ? { ...d, enabled } : d + ) + ); + setTogglingIds((prev) => new Set(prev).add(destinationId)); + + try { + await api.post( + `/org/${orgId}/event-streaming-destination/${destinationId}`, + { enabled } + ); + } catch (e) { + // Revert on failure + setDestinations((prev) => + prev.map((d) => + d.destinationId === destinationId + ? { ...d, enabled: !enabled } + : d + ) + ); + toast({ + variant: "destructive", + title: t("streamingFailedToUpdate"), + description: formatAxiosError(e, t("streamingUnexpectedError")) + }); + } finally { + setTogglingIds((prev) => { + const next = new Set(prev); + next.delete(destinationId); + return next; + }); + } + }; + + const handleDeleteCard = (destination: Destination) => { + setDeleteTarget(destination); + setDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (!deleteTarget) return; + setDeleting(true); + try { + await api.delete( + `/org/${orgId}/event-streaming-destination/${deleteTarget.destinationId}` + ); + toast({ title: t("streamingDeletedSuccess") }); + setDeleteDialogOpen(false); + setDeleteTarget(null); + loadDestinations(); + } catch (e) { + toast({ + variant: "destructive", + title: t("streamingFailedToDelete"), + description: formatAxiosError(e, t("streamingUnexpectedError")) + }); + } finally { + setDeleting(false); + } + }; + + const openCreate = () => { + setTypePickerOpen(true); + }; + + const handleTypePicked = (_type: DestinationType) => { + setTypePickerOpen(false); + setEditingDestination(null); + setModalOpen(true); + }; + + const openEdit = (destination: Destination) => { + setEditingDestination(destination); + setModalOpen(true); + }; + + return ( + <> + + + + + {loading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {destinations.map((dest) => ( + + ))} + {/* Add card is always clickable — paywall is enforced inside the picker */} + +
+ )} + + + + + + {deleteTarget && ( + { + setDeleteDialogOpen(v); + if (!v) setDeleteTarget(null); + }} + string={ + parseHttpConfig(deleteTarget.config).name || + t("streamingDeleteDialogThisDestination") + } + title={t("streamingDeleteTitle")} + dialog={ +

+ {t("streamingDeleteDialogAreYouSure")}{" "} + + {parseHttpConfig(deleteTarget.config).name || + t("streamingDeleteDialogThisDestination")} + + {t("streamingDeleteDialogPermanentlyRemoved")} +

+ } + buttonText={t("streamingDeleteButtonText")} + onConfirm={handleDeleteConfirm} + /> + )} + + ); +} diff --git a/src/app/[orgId]/settings/provisioning/keys/page.tsx b/src/app/[orgId]/settings/provisioning/keys/page.tsx new file mode 100644 index 000000000..32a06706d --- /dev/null +++ b/src/app/[orgId]/settings/provisioning/keys/page.tsx @@ -0,0 +1,84 @@ +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { AxiosResponse } from "axios"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; +import SiteProvisioningKeysTable, { + SiteProvisioningKeyRow +} from "@app/components/SiteProvisioningKeysTable"; +import { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types"; +import { getTranslations } from "next-intl/server"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; +import DismissableBanner from "@app/components/DismissableBanner"; +import Link from "next/link"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, Plug } from "lucide-react"; + +type ProvisioningKeysPageProps = { + params: Promise<{ orgId: string }>; +}; + +export const dynamic = "force-dynamic"; + +export default async function ProvisioningKeysPage( + props: ProvisioningKeysPageProps +) { + const params = await props.params; + const t = await getTranslations(); + + let siteProvisioningKeys: ListSiteProvisioningKeysResponse["siteProvisioningKeys"] = + []; + try { + const res = await internal.get< + AxiosResponse + >( + `/org/${params.orgId}/site-provisioning-keys`, + await authCookieHeader() + ); + siteProvisioningKeys = res.data.data.siteProvisioningKeys; + } catch (e) {} + + const rows: SiteProvisioningKeyRow[] = siteProvisioningKeys.map((k) => ({ + name: k.name, + id: k.siteProvisioningKeyId, + key: `${k.siteProvisioningKeyId}••••••••••••••••••••${k.lastChars}`, + createdAt: k.createdAt, + lastUsed: k.lastUsed, + maxBatchSize: k.maxBatchSize, + numUsed: k.numUsed, + validUntil: k.validUntil, + approveNewSites: k.approveNewSites + })); + + return ( + <> + } + description={t("provisioningKeysBannerDescription")} + > + + + + + + + + + + ); +} diff --git a/src/app/[orgId]/settings/provisioning/layout.tsx b/src/app/[orgId]/settings/provisioning/layout.tsx new file mode 100644 index 000000000..bd2da7812 --- /dev/null +++ b/src/app/[orgId]/settings/provisioning/layout.tsx @@ -0,0 +1,38 @@ +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { getTranslations } from "next-intl/server"; + +interface ProvisioningLayoutProps { + children: React.ReactNode; + params: Promise<{ orgId: string }>; +} + +export default async function ProvisioningLayout({ + children, + params +}: ProvisioningLayoutProps) { + const { orgId } = await params; + const t = await getTranslations(); + + const navItems = [ + { + title: t("provisioningKeys"), + href: `/${orgId}/settings/provisioning/keys` + }, + { + title: t("pendingSites"), + href: `/${orgId}/settings/provisioning/pending` + } + ]; + + return ( + <> + + + {children} + + ); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/provisioning/page.tsx b/src/app/[orgId]/settings/provisioning/page.tsx index e8b53104f..51db66c2d 100644 --- a/src/app/[orgId]/settings/provisioning/page.tsx +++ b/src/app/[orgId]/settings/provisioning/page.tsx @@ -1,60 +1,10 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { AxiosResponse } from "axios"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SiteProvisioningKeysTable, { - SiteProvisioningKeyRow -} from "../../../../components/SiteProvisioningKeysTable"; -import { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types"; -import { getTranslations } from "next-intl/server"; -import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; +import { redirect } from "next/navigation"; type ProvisioningPageProps = { params: Promise<{ orgId: string }>; }; -export const dynamic = "force-dynamic"; - export default async function ProvisioningPage(props: ProvisioningPageProps) { const params = await props.params; - const t = await getTranslations(); - - let siteProvisioningKeys: ListSiteProvisioningKeysResponse["siteProvisioningKeys"] = - []; - try { - const res = await internal.get< - AxiosResponse - >( - `/org/${params.orgId}/site-provisioning-keys`, - await authCookieHeader() - ); - siteProvisioningKeys = res.data.data.siteProvisioningKeys; - } catch (e) {} - - const rows: SiteProvisioningKeyRow[] = siteProvisioningKeys.map((k) => ({ - name: k.name, - id: k.siteProvisioningKeyId, - key: `${k.siteProvisioningKeyId}••••••••••••••••••••${k.lastChars}`, - createdAt: k.createdAt, - lastUsed: k.lastUsed, - maxBatchSize: k.maxBatchSize, - numUsed: k.numUsed, - validUntil: k.validUntil - })); - - return ( - <> - - - - - - - ); -} + redirect(`/${params.orgId}/settings/provisioning/keys`); +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/provisioning/pending/page.tsx b/src/app/[orgId]/settings/provisioning/pending/page.tsx new file mode 100644 index 000000000..4669f9160 --- /dev/null +++ b/src/app/[orgId]/settings/provisioning/pending/page.tsx @@ -0,0 +1,116 @@ +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { ListSitesResponse } from "@server/routers/site"; +import { AxiosResponse } from "axios"; +import { SiteRow } from "@app/components/SitesTable"; +import PendingSitesTable from "@app/components/PendingSitesTable"; +import { getTranslations } from "next-intl/server"; +import DismissableBanner from "@app/components/DismissableBanner"; +import Link from "next/link"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, Plug } from "lucide-react"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; + +type PendingSitesPageProps = { + params: Promise<{ orgId: string }>; + searchParams: Promise>; +}; + +export const dynamic = "force-dynamic"; + +export default async function PendingSitesPage(props: PendingSitesPageProps) { + const params = await props.params; + + const incomingSearchParams = new URLSearchParams(await props.searchParams); + incomingSearchParams.set("status", "pending"); + + let sites: ListSitesResponse["sites"] = []; + let pagination: ListSitesResponse["pagination"] = { + total: 0, + page: 1, + pageSize: 20 + }; + + try { + const res = await internal.get>( + `/org/${params.orgId}/sites?${incomingSearchParams.toString()}`, + await authCookieHeader() + ); + const responseData = res.data.data; + sites = responseData.sites; + pagination = responseData.pagination; + } catch (e) {} + + const t = await getTranslations(); + + function formatSize(mb: number, type: string): string { + if (type === "local") { + return "-"; + } + if (mb >= 1024 * 1024) { + return t("terabytes", { count: (mb / (1024 * 1024)).toFixed(2) }); + } else if (mb >= 1024) { + return t("gigabytes", { count: (mb / 1024).toFixed(2) }); + } else { + return t("megabytes", { count: mb.toFixed(2) }); + } + } + + const siteRows: SiteRow[] = sites.map((site) => ({ + name: site.name, + id: site.siteId, + nice: site.niceId.toString(), + address: site.address?.split("/")[0], + mbIn: formatSize(site.megabytesIn || 0, site.type), + mbOut: formatSize(site.megabytesOut || 0, site.type), + orgId: params.orgId, + type: site.type as any, + online: site.online, + newtVersion: site.newtVersion || undefined, + newtUpdateAvailable: site.newtUpdateAvailable || false, + exitNodeName: site.exitNodeName || undefined, + exitNodeEndpoint: site.exitNodeEndpoint || undefined, + remoteExitNodeId: (site as any).remoteExitNodeId || undefined + })); + + return ( + <> + } + description={t("pendingSitesBannerDescription")} + > + + + + + + + + + ); +} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 414a9b652..12f511078 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -133,8 +133,7 @@ export default function ResourceAuthenticationPage() { ...orgQueries.identityProviders({ orgId: org.org.orgId, useOrgOnlyIdp: env.app.identityProviderMode === "org" - }), - enabled: isPaidUser(tierMatrix.orgOidc) + }) }); const pageLoading = diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index aff10dc52..a9128b9d3 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -400,7 +400,11 @@ function ProxyResourceTargetsForm({ pathMatchType: row.original.pathMatchType }} onChange={(config) => - updateTarget(row.original.targetId, config) + updateTarget(row.original.targetId, + config.path === null && config.pathMatchType === null + ? { ...config, rewritePath: null, rewritePathType: null } + : config + ) } trigger={
+ ) : addRuleForm.watch( + "match" + ) === "REGION" ? ( + + + + + + + + + + {t( + "noRegionFound" + )} + + {REGIONS.map((continent) => ( + + { + field.onChange( + continent.id + ); + setOpenAddRuleRegionSelect( + false + ); + }} + > + + {t(continent.name)} ({continent.id}) + + {continent.includes.map((subregion) => ( + { + field.onChange( + subregion.id + ); + setOpenAddRuleRegionSelect( + false + ); + }} + > + + {t(subregion.name)} ({subregion.id}) + + ))} + + ))} + + + + ) : ( )} diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index b651a06bb..f5c20d8cc 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -776,7 +776,11 @@ export default function Page() { pathMatchType: row.original.pathMatchType }} onChange={(config) => - updateTarget(row.original.targetId, config) + updateTarget(row.original.targetId, + config.path === null && config.pathMatchType === null + ? { ...config, rewritePath: null, rewritePathType: null } + : config + ) } trigger={ + + + + + + )} + {/* */} {/* */} {/* */} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 7368cb253..2a000b34b 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { AdminListUsersResponse } from "@server/routers/user/adminListUsers"; -import UsersTable, { GlobalUserRow } from "../../../components/AdminUsersTable"; +import UsersTable, { GlobalUserRow } from "@app/components/AdminUsersTable"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 4d3fd027c..59a75472b 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -24,6 +24,7 @@ import { Settings, SquareMousePointer, TicketCheck, + Unplug, User, UserCog, Users, @@ -197,6 +198,11 @@ export const orgNavSections = ( title: "sidebarLogsConnection", href: "/{orgId}/settings/logs/connection", icon: + }, + { + title: "sidebarLogsStreaming", + href: "/{orgId}/settings/logs/streaming", + icon: } ] : []) diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 9a187f1ed..8840d2f93 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -154,7 +154,7 @@ export default function CreateDomainForm({ const punycodePreview = useMemo(() => { if (!baseDomain) return ""; - const punycode = toPunycode(baseDomain); + const punycode = toPunycode(baseDomain.toLowerCase()); return punycode !== baseDomain.toLowerCase() ? punycode : ""; }, [baseDomain]); @@ -239,21 +239,24 @@ export default function CreateDomainForm({ className="space-y-4" id="create-domain-form" > - ( - - - - - )} - /> + {build != "oss" && env.flags.usePangolinDns ? ( + ( + + + + + )} + /> + ) : null} + { const v = data.validUntil; @@ -103,7 +104,8 @@ export default function CreateSiteProvisioningKeyCredenza({ name: "", unlimitedBatchSize: false, maxBatchSize: 100, - validUntil: "" + validUntil: "", + approveNewSites: true } }); @@ -114,7 +116,8 @@ export default function CreateSiteProvisioningKeyCredenza({ name: "", unlimitedBatchSize: false, maxBatchSize: 100, - validUntil: "" + validUntil: "", + approveNewSites: true }); } }, [open, form]); @@ -123,18 +126,21 @@ export default function CreateSiteProvisioningKeyCredenza({ setLoading(true); try { const res = await api - .put< - AxiosResponse - >(`/org/${orgId}/site-provisioning-key`, { - name: data.name, - maxBatchSize: data.unlimitedBatchSize - ? null - : data.maxBatchSize, - validUntil: - data.validUntil == null || data.validUntil.trim() === "" - ? undefined - : data.validUntil - }) + .put>( + `/org/${orgId}/site-provisioning-key`, + { + name: data.name, + maxBatchSize: data.unlimitedBatchSize + ? null + : data.maxBatchSize, + validUntil: + data.validUntil == null || + data.validUntil.trim() === "" + ? undefined + : data.validUntil, + approveNewSites: data.approveNewSites + } + ) .catch((e) => { toast({ variant: "destructive", @@ -152,9 +158,7 @@ export default function CreateSiteProvisioningKeyCredenza({ } } - const credential = - created && - created.siteProvisioningKey; + const credential = created && created.siteProvisioningKey; const unlimitedBatchSize = form.watch("unlimitedBatchSize"); @@ -213,15 +217,12 @@ export default function CreateSiteProvisioningKeyCredenza({ min={1} max={1_000_000} autoComplete="off" - disabled={ - unlimitedBatchSize - } + disabled={unlimitedBatchSize} name={field.name} ref={field.ref} onBlur={field.onBlur} onChange={(e) => { - const v = - e.target.value; + const v = e.target.value; field.onChange( v === "" ? 100 @@ -269,9 +270,7 @@ export default function CreateSiteProvisioningKeyCredenza({ const dateTimeValue: DateTimeValue = (() => { if (!field.value) return {}; - const d = new Date( - field.value - ); + const d = new Date(field.value); if (isNaN(d.getTime())) return {}; const hours = d @@ -313,11 +312,7 @@ export default function CreateSiteProvisioningKeyCredenza({ value.date ); if (value.time) { - const [ - h, - m, - s - ] = + const [h, m, s] = value.time.split( ":" ); @@ -352,6 +347,40 @@ export default function CreateSiteProvisioningKeyCredenza({ ); }} /> + ( + + + + field.onChange( + c === true + ) + } + /> + +
+ + {t( + "provisioningKeysApproveNewSites" + )} + + + {t( + "provisioningKeysApproveNewSitesDescription" + )} + +
+
+ )} + /> )} @@ -395,4 +424,4 @@ export default function CreateSiteProvisioningKeyCredenza({ ); -} +} \ No newline at end of file diff --git a/src/components/DeviceLoginForm.tsx b/src/components/DeviceLoginForm.tsx index 16e7f2e1f..0a05e46d3 100644 --- a/src/components/DeviceLoginForm.tsx +++ b/src/components/DeviceLoginForm.tsx @@ -319,6 +319,7 @@ export default function DeviceLoginForm({
new Date("2026-04-13"); const { data = [], isLoading: loadingDomains } = useQuery( orgQueries.domains({ orgId }) @@ -509,9 +521,11 @@ export default function DomainPicker({ {selectedBaseDomain.domain} - {selectedBaseDomain.verified && ( - - )} + {selectedBaseDomain.verified && + selectedBaseDomain.domainType !== + "wildcard" && ( + + )}
) : ( t("domainPickerSelectBaseDomain") @@ -574,14 +588,23 @@ export default function DomainPicker({ } - {orgDomain.type.toUpperCase()}{" "} - •{" "} - {orgDomain.verified + {orgDomain.type === + "wildcard" ? t( - "domainPickerVerified" + "domainPickerManual" ) - : t( - "domainPickerUnverified" + : ( + <> + {orgDomain.type.toUpperCase()}{" "} + •{" "} + {orgDomain.verified + ? t( + "domainPickerVerified" + ) + : t( + "domainPickerUnverified" + )} + )} @@ -640,6 +663,7 @@ export default function DomainPicker({ }) } className="mx-2 rounded-md" + disabled={requiresPaywall} >
@@ -680,6 +704,19 @@ export default function DomainPicker({
+ {requiresPaywall && !hideFreeDomain && ( + + +
+ + + {t("domainPickerFreeDomainsPaidFeature")} + +
+
+
+ )} + {/*showProvidedDomainSearch && build === "saas" && ( diff --git a/src/components/EditSiteProvisioningKeyCredenza.tsx b/src/components/EditSiteProvisioningKeyCredenza.tsx index 138190edc..e0e9cdde0 100644 --- a/src/components/EditSiteProvisioningKeyCredenza.tsx +++ b/src/components/EditSiteProvisioningKeyCredenza.tsx @@ -45,6 +45,7 @@ export type EditableSiteProvisioningKey = { name: string; maxBatchSize: number | null; validUntil: string | null; + approveNewSites: boolean; }; type EditSiteProvisioningKeyCredenzaProps = { @@ -76,7 +77,8 @@ export default function EditSiteProvisioningKeyCredenza({ .max(1_000_000, { message: t("provisioningKeysMaxBatchSizeInvalid") }), - validUntil: z.string().optional() + validUntil: z.string().optional(), + approveNewSites: z.boolean() }) .superRefine((data, ctx) => { const v = data.validUntil; @@ -100,7 +102,8 @@ export default function EditSiteProvisioningKeyCredenza({ name: "", unlimitedBatchSize: false, maxBatchSize: 100, - validUntil: "" + validUntil: "", + approveNewSites: true } }); @@ -112,7 +115,8 @@ export default function EditSiteProvisioningKeyCredenza({ name: provisioningKey.name, unlimitedBatchSize: provisioningKey.maxBatchSize == null, maxBatchSize: provisioningKey.maxBatchSize ?? 100, - validUntil: provisioningKey.validUntil ?? "" + validUntil: provisioningKey.validUntil ?? "", + approveNewSites: provisioningKey.approveNewSites }); }, [open, provisioningKey, form]); @@ -135,7 +139,8 @@ export default function EditSiteProvisioningKeyCredenza({ data.validUntil == null || data.validUntil.trim() === "" ? "" - : data.validUntil + : data.validUntil, + approveNewSites: data.approveNewSites } ) .catch((e) => { @@ -255,6 +260,38 @@ export default function EditSiteProvisioningKeyCredenza({ )} /> + ( + + + + field.onChange(c === true) + } + /> + +
+ + {t( + "provisioningKeysApproveNewSites" + )} + + + {t( + "provisioningKeysApproveNewSitesDescription" + )} + +
+
+ )} + /> ; + format: PayloadFormat; + useBodyTemplate: boolean; + bodyTemplate?: string; +} + +export interface Destination { + destinationId: number; + orgId: string; + type: string; + config: string; + enabled: boolean; + sendAccessLogs: boolean; + sendActionLogs: boolean; + sendConnectionLogs: boolean; + sendRequestLogs: boolean; + createdAt: number; + updatedAt: number; +} + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +export const defaultHttpConfig = (): HttpConfig => ({ + name: "", + url: "", + authType: "none", + bearerToken: "", + basicCredentials: "", + customHeaderName: "", + customHeaderValue: "", + headers: [], + format: "json_array", + useBodyTemplate: false, + bodyTemplate: "" +}); + +export function parseHttpConfig(raw: string): HttpConfig { + try { + return { ...defaultHttpConfig(), ...JSON.parse(raw) }; + } catch { + return defaultHttpConfig(); + } +} + +// ── Headers editor ───────────────────────────────────────────────────────────── + +interface HeadersEditorProps { + headers: Array<{ key: string; value: string }>; + onChange: (headers: Array<{ key: string; value: string }>) => void; +} + +function HeadersEditor({ headers, onChange }: HeadersEditorProps) { + const t = useTranslations(); + + const addRow = () => onChange([...headers, { key: "", value: "" }]); + + const removeRow = (i: number) => + onChange(headers.filter((_, idx) => idx !== i)); + + const updateRow = (i: number, field: "key" | "value", val: string) => { + const next = [...headers]; + next[i] = { ...next[i], [field]: val }; + onChange(next); + }; + + return ( +
+ {headers.length === 0 && ( +

+ {t("httpDestNoHeadersConfigured")} +

+ )} + {headers.map((h, i) => ( +
+ updateRow(i, "key", e.target.value)} + placeholder={t("httpDestHeaderNamePlaceholder")} + className="flex-1" + /> + + updateRow(i, "value", e.target.value) + } + placeholder={t("httpDestHeaderValuePlaceholder")} + className="flex-1" + /> + +
+ ))} + +
+ ); +} + +// ── Component ────────────────────────────────────────────────────────────────── + +export interface HttpDestinationCredenzaProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editing: Destination | null; + orgId: string; + onSaved: () => void; +} + +export function HttpDestinationCredenza({ + open, + onOpenChange, + editing, + orgId, + onSaved +}: HttpDestinationCredenzaProps) { + const api = createApiClient(useEnvContext()); + const t = useTranslations(); + + const [saving, setSaving] = useState(false); + const [cfg, setCfg] = useState(defaultHttpConfig()); + const [sendAccessLogs, setSendAccessLogs] = useState(false); + const [sendActionLogs, setSendActionLogs] = useState(false); + const [sendConnectionLogs, setSendConnectionLogs] = useState(false); + const [sendRequestLogs, setSendRequestLogs] = useState(false); + + useEffect(() => { + if (open) { + setCfg( + editing ? parseHttpConfig(editing.config) : defaultHttpConfig() + ); + setSendAccessLogs(editing?.sendAccessLogs ?? false); + setSendActionLogs(editing?.sendActionLogs ?? false); + setSendConnectionLogs(editing?.sendConnectionLogs ?? false); + setSendRequestLogs(editing?.sendRequestLogs ?? false); + } + }, [open, editing]); + + const update = (patch: Partial) => + setCfg((prev) => ({ ...prev, ...patch })); + + const urlError: string | null = (() => { + const raw = cfg.url.trim(); + if (!raw) return null; + try { + const parsed = new URL(raw); + if ( + parsed.protocol !== "http:" && + parsed.protocol !== "https:" + ) { + return t("httpDestUrlErrorHttpRequired"); + } + if (build === "saas" && parsed.protocol !== "https:") { + return t("httpDestUrlErrorHttpsRequired"); + } + return null; + } catch { + return t("httpDestUrlErrorInvalid"); + } + })(); + + const isValid = + cfg.name.trim() !== "" && + cfg.url.trim() !== "" && + urlError === null; + + async function handleSave() { + if (!isValid) return; + setSaving(true); + try { + const payload = { + type: "http", + config: JSON.stringify(cfg), + sendAccessLogs, + sendActionLogs, + sendConnectionLogs, + sendRequestLogs + }; + if (editing) { + await api.post( + `/org/${orgId}/event-streaming-destination/${editing.destinationId}`, + payload + ); + toast({ title: t("httpDestUpdatedSuccess") }); + } else { + await api.put( + `/org/${orgId}/event-streaming-destination`, + payload + ); + toast({ title: t("httpDestCreatedSuccess") }); + } + onSaved(); + onOpenChange(false); + } catch (e) { + toast({ + variant: "destructive", + title: editing + ? t("httpDestUpdateFailed") + : t("httpDestCreateFailed"), + description: formatAxiosError( + e, + t("streamingUnexpectedError") + ) + }); + } finally { + setSaving(false); + } + } + + return ( + + + + + {editing + ? t("httpDestEditTitle") + : t("httpDestAddTitle")} + + + {editing + ? t("httpDestEditDescription") + : t("httpDestAddDescription")} + + + + + + {/* ── Settings tab ────────────────────────────── */} +
+ {/* Name */} +
+ + + update({ name: e.target.value }) + } + /> +
+ + {/* URL */} +
+ + + update({ url: e.target.value }) + } + /> + {urlError && ( +

+ {urlError} +

+ )} +
+ + {/* Authentication */} +
+
+ +

+ {t("httpDestAuthDescription")} +

+
+ + + update({ authType: v as AuthType }) + } + className="gap-2" + > + {/* None */} +
+ +
+ +

+ {t("httpDestAuthNoneDescription")} +

+
+
+ + {/* Bearer */} +
+ +
+
+ +

+ {t("httpDestAuthBearerDescription")} +

+
+ {cfg.authType === "bearer" && ( + + update({ + bearerToken: + e.target.value + }) + } + /> + )} +
+
+ + {/* Basic */} +
+ +
+
+ +

+ {t("httpDestAuthBasicDescription")} +

+
+ {cfg.authType === "basic" && ( + + update({ + basicCredentials: + e.target.value + }) + } + /> + )} +
+
+ + {/* Custom */} +
+ +
+
+ +

+ {t("httpDestAuthCustomDescription")} +

+
+ {cfg.authType === "custom" && ( +
+ + update({ + customHeaderName: + e.target + .value + }) + } + className="flex-1" + /> + + update({ + customHeaderValue: + e.target + .value + }) + } + className="flex-1" + /> +
+ )} +
+
+
+
+
+ + {/* ── Headers tab ──────────────────────────────── */} +
+
+ +

+ {t("httpDestCustomHeadersDescription")} +

+
+ update({ headers })} + /> +
+ + {/* ── Body tab ─────────────────────────── */} +
+
+ +

+ {t("httpDestBodyTemplateDescription")} +

+
+ +
+ + update({ useBodyTemplate: v }) + } + /> + +
+ + {cfg.useBodyTemplate && ( +
+ +