diff --git a/.cursor/rules/Migrations.mdc b/.cursor/rules/Migrations.mdc
deleted file mode 100644
index d9562b4e1..000000000
--- a/.cursor/rules/Migrations.mdc
+++ /dev/null
@@ -1,5 +0,0 @@
----
-alwaysApply: true
----
-
-Don't write or edit migrations in `server/setup` unless specificall instructed to do so.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b3dc38d09..685be384c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,42 +1,52 @@
version: 2
-
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 1
groups:
- npm-dependencies:
- patterns:
- - "*"
-
+ dev-patch-updates:
+ dependency-type: "development"
+ update-types:
+ - "patch"
+ dev-minor-updates:
+ dependency-type: "development"
+ update-types:
+ - "minor"
+ prod-patch-updates:
+ dependency-type: "production"
+ update-types:
+ - "patch"
+ prod-minor-updates:
+ dependency-type: "production"
+ update-types:
+ - "minor"
+
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
- open-pull-requests-limit: 1
groups:
- docker-dependencies:
- patterns:
- - "*"
+ patch-updates:
+ update-types:
+ - "patch"
+ minor-updates:
+ update-types:
+ - "minor"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- open-pull-requests-limit: 1
- groups:
- github-actions-dependencies:
- patterns:
- - "*"
- package-ecosystem: "gomod"
directory: "/install"
schedule:
interval: "daily"
- open-pull-requests-limit: 1
groups:
- go-install-dependencies:
- patterns:
- - "*"
+ patch-updates:
+ update-types:
+ - "patch"
+ minor-updates:
+ update-types:
+ - "minor"
diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 09d2986e9..2b8b5a5e2 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Monitor storage space
run: |
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Monitor storage space
run: |
@@ -201,7 +201,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout code
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Extract tag name
id: get-tag
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index 09b5270b0..ba60e6337 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3f64d913f..970cd0dc9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Docker image sqlite
run: make dev-build-sqlite
@@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Docker image pg
run: make dev-build-pg
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a7da2a1cc..5092cb6c1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,8 +18,5 @@
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
- "editor.formatOnSave": true,
- "cSpell.words": [
- "nessicary"
- ]
+ "editor.formatOnSave": true
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 1827f2570..ab857350e 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@
-Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with 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
@@ -63,26 +63,11 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
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.
-* Lightweight user-space connector runs anywhere
-* Punches through any firewall
-* Doesn't require open ports or a public IP
-* Strict network segmentation
-* WireGuard-based
-* Get alerts when a device or network resource goes down
-
### Browser-based reverse proxy access
-Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the browser through identity and context-aware tunneled reverse proxies. Users access resources 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.
-
-* Expose a web panel anywhere
-* Access via any web browser
-* Single sign-on across all resources
-* HTTPS resources
-* Remote desktop in the browser with VNC and RDP
-* In-browser SSH terminal with privileged access management (PAM)
-* PIN codes, passcodes, email OTP, geoblocking, allow-lists, and more
+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.
@@ -90,35 +75,14 @@ Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the b
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.
-* Peer-to-peer with intelligent NAT traversal
-* Hosts/IPs and port ranges
-* Network ranges/CIDRs
-* Friendly DNS aliases for network addresses
-* Privileged access management (PAM) with SSH resources
-* Private HTTPS resources only accessible on the private network
-
### 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.
-
-* Bring your existing identity provider (IdP) or use Pangolin identities
-* Sync users and roles from your IdP
-* User- and role-based access control
-* Full network audit and access logs
+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.
-### Find and launch resources from a personalized home page
-
-Give users a landing page to quickly find and open the resources they can access. Resources are grouped by site or label, searchable, and filterable, with grid or list views. Saved views capture filters, grouping, and layout as personal or organization-wide defaults.
-
-* Single place for admins and non-admins to see accessible resources
-* Create reusable views for common access patterns
-
-
-
## Download Clients
Download the Pangolin client for your platform:
diff --git a/compose.drizzle.yaml b/docker-compose.drizzle.yml
similarity index 100%
rename from compose.drizzle.yaml
rename to docker-compose.drizzle.yml
diff --git a/compose.example.yaml b/docker-compose.example.yml
similarity index 98%
rename from compose.example.yaml
rename to docker-compose.example.yml
index 5a35e20cf..50cb1bcc1 100644
--- a/compose.example.yaml
+++ b/docker-compose.example.yml
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode
traefik:
- image: traefik:v3.7
+ image: traefik:v3.6
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
diff --git a/compose.mailpit.yaml b/docker-compose.mailpit.yml
similarity index 100%
rename from compose.mailpit.yaml
rename to docker-compose.mailpit.yml
diff --git a/compose.pgr.yaml b/docker-compose.pgr.yml
similarity index 100%
rename from compose.pgr.yaml
rename to docker-compose.pgr.yml
diff --git a/compose.yaml b/docker-compose.yml
similarity index 100%
rename from compose.yaml
rename to docker-compose.yml
diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml
index 05b2fbe8c..96b15ce47 100644
--- a/install/config/docker-compose.yml
+++ b/install/config/docker-compose.yml
@@ -50,7 +50,7 @@ services:
- 80:80{{end}}
traefik:
- image: docker.io/traefik:v3.7
+ image: docker.io/traefik:v3.6
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
diff --git a/install/go.mod b/install/go.mod
index 69dc9e5fc..7f50d4e9e 100644
--- a/install/go.mod
+++ b/install/go.mod
@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
- golang.org/x/term v0.44.0
+ golang.org/x/term v0.43.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect
- golang.org/x/sys v0.46.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
diff --git a/install/go.sum b/install/go.sum
index 704142a64..7ed97929f 100644
--- a/install/go.sum
+++ b/install/go.sum
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
-golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
+golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 2d455afa8..af2c2eca3 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -66,15 +66,9 @@
"local": "Локална",
"edit": "Редактиране",
"siteConfirmDelete": "Потвърждение на изтриване на сайта",
- "siteConfirmDeleteAndResources": "Потвърдете изтриването на сайта и ресурсите",
"siteDelete": "Изтриване на сайта",
- "siteDeleteAndResources": "Изтриване на сайта и ресурсите",
"siteMessageRemove": "След премахване, сайтът вече няма да бъде достъпен. Всички цели, свързани със сайта, също ще бъдат премахнати.",
- "siteMessageRemoveAndResources": "Това ще изтрие окончателно всички публични и частни ресурси, свързани с този сайт, дори ако ресурсът е асоцииран и с други сайтове.",
"siteQuestionRemove": "Сигурни ли сте, че искате да премахнете сайта от организацията?",
- "siteQuestionRemoveAndResources": "Наистина ли желаете да изтриете този сайт и всички свързани ресурси?",
- "sitesTableDeleteSite": "Изтриване на сайта",
- "sitesTableDeleteSiteAndResources": "Изтриване на сайта и ресурсите",
"siteManageSites": "Управление на сайтове",
"siteDescription": "Създайте и управлявайте сайтове, за да осигурите свързаност със частни мрежи",
"sitesBannerTitle": "Свържете се с мрежа.",
@@ -123,16 +117,6 @@
"siteUpdated": "Сайтът е обновен",
"siteUpdatedDescription": "Сайтът е актуализиран.",
"siteGeneralDescription": "Конфигурирайте общи настройки за този сайт",
- "siteRestartTitle": "Рестартирайте Сайта",
- "siteRestartDescription": "Рестартирайте WireGuard тунела за този сайт. Това ще прекъсне връзката за кратко.",
- "siteRestartBody": "Използвайте това, ако тунелът на сайта не функционира правилно и искате да принудите повторно свързване без да рестартирате хоста.",
- "siteRestartButton": "Рестартирайте Сайта",
- "siteRestartDialogMessage": "Сигурни ли сте, че искате да рестартирате WireGuard тунела за {name}? Сайтът ще изгуби връзка за кратко.",
- "siteRestartWarning": "Сайтът ще се изключи за кратко, докато тунелът се рестартира.",
- "siteRestarted": "Сайтът е рестартиран",
- "siteRestartedDescription": "WireGuard тунелът е рестартиран.",
- "siteErrorRestart": "Неуспешно рестартиране на сайта",
- "siteErrorRestartDescription": "Възникна грешка при рестартирането на сайта.",
"siteSettingDescription": "Конфигурирайте настройките на сайта",
"siteResourcesTab": "Ресурси",
"siteResourcesNoneOnSite": "Този сайт все още няма публични или частни ресурси.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Получавайте заявки чрез суров TCP/UDP с използване на портен номер. Изисква се сайтовете да се свързват към отдалечен възел.",
"resourceCreate": "Създайте ресурс",
"resourceCreateDescription": "Следвайте стъпките по-долу, за да създадете нов ресурс",
- "resourcePublicCreate": "Създаване на публичен ресурс",
- "resourcePublicCreateDescription": "Следвайте стъпките по-долу, за да създадете нов обществен ресурс, достъпен чрез уеб браузър",
"resourceCreateGeneralDescription": "Конфигуриране на основните настройки на ресурса, включително име и тип",
"resourceSeeAll": "Вижте всички ресурси",
"resourceCreateGeneral": "Основни параметри",
@@ -615,8 +597,7 @@
"idpNameInternal": "Вътрешен",
"emailInvalid": "Невалиден имейл адрес",
"inviteValidityDuration": "Моля, изберете продължителност",
- "accessRoleSelectPlease": "Потребителят трябва да принадлежи към поне една роля.",
- "accessRoleRequired": "Ролята е задължителна",
+ "accessRoleSelectPlease": "Моля, изберете роля",
"removeOwnAdminRoleConfirmTitle": "Премахване на административния ви достъп?",
"removeOwnAdminRoleConfirmDescription": "След записване няма да имате повече администраторски права в тази организация. Друг администратор може да възстанови достъпа, ако е необходимо.",
"removeOwnAdminRoleConfirmButton": "Премахнете административния ми достъп",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Базово удостоверяване чрез заглавие",
"policyAuthHeaderAuthDescription": "Валидирайте собствено HTTP заглавие и стойност при всяка заявка",
"policyAuthHeaderAuthSummary": "Конфигурирано заглавие",
- "policyAuthHeaderName": "Потребителско име",
- "policyAuthHeaderValue": "Парола",
+ "policyAuthHeaderName": "Име на заглавието",
+ "policyAuthHeaderValue": "Очаквана стойност",
"policyAuthSetPasscode": "Задайте парола",
"policyAuthSetPincode": "Задайте ПИН код",
"policyAuthSetEmailWhitelist": "Задайте списък с имейли",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Приложи Чернова",
"actionListBlueprints": "Списък с планове.",
"actionGetBlueprint": "Вземи план.",
- "actionCreateOrgWideLauncherView": "Създайте Изглед на Стартирача за Цялата Организация",
"setupToken": "Конфигурация на токен",
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Задайте ПИН код на ресурса",
"actionSetResourceEmailWhitelist": "Задайте списък на одобрените имейл адреси за ресурса",
"actionGetResourceEmailWhitelist": "Вземете списък на одобрените имейл адреси за ресурса",
- "actionGetResourcePolicy": "Получете политика за ресурси",
- "actionUpdateResourcePolicy": "Актуализирайте политика за ресурси",
- "actionSetResourcePolicyUsers": "Задайте потребители на ресурсна политика",
- "actionSetResourcePolicyRoles": "Задайте роли на ресурсна политика",
- "actionSetResourcePolicyPassword": "Задайте парола на ресурсна политика",
- "actionSetResourcePolicyPincode": "Задайте ПИН код на ресурсна политика",
- "actionSetResourcePolicyHeaderAuth": "Задайте автентикация на ресурсна политика",
- "actionSetResourcePolicyWhitelist": "Задайте бял списък на имейли за ресурси",
- "actionSetResourcePolicyRules": "Задайте правила за ресурси",
"actionCreateTarget": "Създайте цел",
"actionDeleteTarget": "Изтрийте цел",
"actionGetTarget": "Вземете цел",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Генериране на токен за достъп",
"actionDeleteAccessToken": "Изтриване на токен за достъп",
"actionListAccessTokens": "Изброяване на токени за достъп",
- "actionCreateResourceSessionToken": "Създаване на токен за сесия на ресурс",
"actionCreateResourceRule": "Създаване на правило за ресурс",
"actionDeleteResourceRule": "Изтрийте правило за ресурс",
"actionListResourceRules": "Изброяване на правила за ресурс",
@@ -1520,30 +1490,6 @@
"navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението",
"navbarDocsLink": "Документация",
- "commandPaletteTitle": "Командна палитра",
- "commandPaletteDescription": "Търсене на страници, организации, ресурси и действия",
- "commandPaletteSearchPlaceholder": "Търсете страници, ресурси, действия...",
- "commandPaletteNoResults": "Няма намерени резултати.",
- "commandPaletteSearching": "Търсене...",
- "commandPaletteNavigation": "Навигация",
- "commandPaletteOrganizations": "Организации",
- "commandPaletteSites": "Сайтове",
- "commandPaletteResources": "Ресурси",
- "commandPaletteUsers": "Потребители",
- "commandPaletteClients": "Машинни клиенти",
- "commandPaletteActions": "Действия",
- "commandPaletteCreateSite": "Създаване на сайт",
- "commandPaletteCreateProxyResource": "Създаване на обществен ресурс",
- "commandPaletteCreatePrivateResource": "Създаване на частен ресурс",
- "commandPaletteCreateUser": "Създаване на потребител",
- "commandPaletteCreateApiKey": "Създаване на API ключ",
- "commandPaletteCreateMachineClient": "Създаване на машинен клиент",
- "commandPaletteCreateAlertRule": "Създаване на правила за известия",
- "commandPaletteCreateIdentityProvider": "Създаване на доставчик на идентичност",
- "commandPaletteToggleTheme": "Смяна на темата",
- "commandPaletteChooseOrganization": "Изберете организация",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Не може да се активира 2FA",
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Управление",
"sidebarBillingAndLicenses": "Фактуриране & Лицензи",
"sidebarLogsAnalytics": "Анализи",
- "commandSites": "Сайтове",
- "commandActionModeInfo": "Напишете \">\" за да отворите режим на действия",
- "commandResources": "Ресурси",
- "commandProxyResources": "Обществени ресурси",
- "commandClientResources": "Частни ресурси",
- "commandClients": "Клиенти",
- "commandUserDevices": "Потребителски устройства",
- "commandMachineClients": "Машинни клиенти",
- "commandDomains": "Домейни",
- "commandRemoteExitNodes": "Отдалечени възли",
- "commandTeam": "Екип",
- "commandUsers": "Потребители",
- "commandRoles": "Роли",
- "commandInvitations": "Покани",
- "commandPolicies": "Споделени политики",
- "commandResourcePolicies": "Политики за обществени ресурси",
- "commandIdentityProviders": "Доставчици на идентичност",
- "commandApprovals": "Заявки за потвърждение",
- "commandShareableLinks": "Споделени връзки",
- "commandOrganization": "Организация",
- "commandLogsAndAnalytics": "Логове и Анализи",
- "commandLogsAnalytics": "Анализи",
- "commandLogsRequest": "HTTP заявки за логове",
- "commandLogsAccess": "Логове за достъп",
- "commandLogsAction": "Логове на администратори",
- "commandLogsConnection": "Логове на връзките",
- "commandLogsStreaming": "Течения на събития",
- "commandManagement": "Управление",
- "commandAlerting": "Предупреждение",
- "commandProvisioning": "Осигуряване",
- "commandBluePrints": "Планове",
- "commandApiKeys": "API ключове",
- "commandBillingAndLicenses": "Фактуриране и лицензи",
- "commandBilling": "Фактуриране",
- "commandEnterpriseLicenses": "Лицензи",
- "commandSettings": "Настройки",
- "commandLauncher": "Стартиращо устройство",
- "commandResourceLauncher": "Стартер на ресурси",
- "commandSearchResults": "Резултати от търсенето",
"alertingTitle": "Извеждане на предупреждения",
"alertingDescription": "Определете източници, тригери и действия за уведомления",
"alertingRules": "Правила за предупреждение",
@@ -1991,9 +1898,6 @@
"billingDomains": "Домейни",
"billingOrganizations": "Организации",
"billingRemoteExitNodes": "Дистанционни възли",
- "billingPublicResources": "Обществени ресурси",
- "billingPrivateResources": "Частни ресурси",
- "billingMachineClients": "Машинни клиенти",
"billingNoLimitConfigured": "Няма конфигуриран лимит",
"billingEstimatedPeriod": "Очакван период на фактуриране",
"billingIncludedUsage": "Включено използване",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Колко потребители можете да използвате",
"billingDomainInfo": "Колко домейни можете да използвате",
"billingRemoteExitNodesInfo": "Колко дистанционни възли можете да използвате",
- "billingPublicResourcesInfo": "Колко публични ресурси можете да използвате",
- "billingPrivateResourcesInfo": "Колко частни ресурси можете да използвате",
- "billingMachineClientsInfo": "Колко машинни клиенти можете да използвате",
"billingLicenseKeys": "Лицензионни ключове",
"billingLicenseKeysDescription": "Управлявайте вашите абонаменти за лицензионни ключове",
"billingLicenseSubscription": "Абонамент за лиценз",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Мрежа",
"addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.",
"selectSites": "Избор на сайтове",
- "selectLabels": "Изберете етикети",
"sitesDescription": "Клиентът ще има връзка с избраните сайтове",
"clientInstallOlm": "Инсталиране на Olm",
"clientInstallOlmDescription": "Конфигурирайте Olm да работи на вашата система",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Затвори",
"createInternalResourceDialogCreateClientResource": "Създаване на частен ресурс",
"createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен само за клиенти, свързани към организацията",
- "privateResourceGeneralDescription": "Конфигурирайте името, идентификатора и другите общи настройки на ресурса.",
- "privateResourceCreatePageSeeAll": "Вижте всички частни ресурси",
- "privateResourceAllowIcmpPing": "Разрешете ICMP Ping",
- "privateResourceNetworkAccess": "Достъп до мрежата",
- "privateResourceNetworkAccessDescription": "Контролирайте достъпа до TCP/UDP портовете и дали ICMP пинг е разрешен за този ресурс.",
- "hostSettings": "Настройки на хоста",
- "cidrSettings": "Настройки на CIDR",
"createInternalResourceDialogResourceProperties": "Свойства на ресурса",
"createInternalResourceDialogName": "Име",
"createInternalResourceDialogSite": "Сайт",
"selectSite": "Изберете сайт...",
"multiSitesSelectorSitesCount": "{count, plural, one {# сайт} other {# сайтове}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# етикет} other {# етикета}}",
"noSitesFound": "Не са намерени сайтове.",
"createInternalResourceDialogProtocol": "Протокол",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.",
"createInternalResourceDialogAlias": "Псевдоним",
"createInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
- "internalResourceAliasLocalWarning": "Синоними с окончание .local могат да причинят проблеми с резолюцията поради mDNS в някои мрежи.",
"internalResourceDownstreamSchemeRequired": "Методът е задължителен за HTTP ресурси",
"internalResourceHttpPortRequired": "Портът към целта е задължителен за HTTP ресурси",
"siteConfiguration": "Конфигурация",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Отдалечени възли",
"remoteExitNodeId": "ID.",
"remoteExitNodeSecretKey": "Секретен ключ.",
- "remoteExitNodeNetworkingTitle": "Настройки на Мрежата",
- "remoteExitNodeNetworkingDescription": "Настройте как този отдалечен край маршрутизира трафика и кои сайтове предпочитат да се свържат чрез него. Усъвършенствани функции за използване при конфигурации на бекаул мрежи.",
- "remoteExitNodeNetworkingSave": "Запазване на Настройките",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Настройките на мрежата са успешно запазени",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Настройките на мрежата бяха успешно обновени.",
- "remoteExitNodeNetworkingSaveError": "Неуспешно запазване на мрежовите настройки",
- "remoteExitNodeNetworkingSubnetsTitle": "Отдалечени Подмрежи",
- "remoteExitNodeNetworkingSubnetsDescription": "Определете CIDR диапазоните, които този отдалечен край ще маршрутизира трафика към. Въведете валиден CIDR (e.g. 10.0.0.0/8) и натиснете Enter, за да добавите.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Добавете CIDR диапазон (напр. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Неуспешно зареждане на подмрежи",
- "remoteExitNodeNetworkingLabelsTitle": "Етикети за Предпочитания",
- "remoteExitNodeNetworkingLabelsDescription": "Сайтове с тези етикети ще бъдат принудени да се свържат чрез този отдалечен край.",
- "remoteExitNodeNetworkingLabelsButtonText": "Изберете етикети...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Търсене на етикети...",
- "remoteExitNodeNetworkingLabelsLoadError": "Неуспешно зареждане на етикети",
"remoteExitNodeCreate": {
"title": "Създаване на отдалечен възел.",
"description": "Създайте нов самохостнал отдалечен ретранслатор и прокси сървърен възел.",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Няма налични възли за тази организация. Първо създайте възел, за да използвате местни сайтове.",
"exitNode": "Изходен възел",
"country": "Държава",
- "countryIsNot": "Държавата не е",
"rulesMatchCountry": "Понастоящем на базата на изходния IP",
"region": "Регион",
"selectRegion": "Изберете регион",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC доставчик",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC доставчик",
"subnet": "Подмрежа",
- "utilitySubnet": "Мрежа на Помощни Подмрежи",
"subnetDescription": "Подмрежата за конфигурацията на мрежата на тази организация.",
"customDomain": "Персонализиран домейн.",
"authPage": "Страници за автентификация.",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Необходима е настройка на двуфакторно удостоверяване. Моля, влезте отново чрез {dashboardUrl}/auth/login, за да завършите тази стъпка. След това, върнете се тук.",
"additionalSecurityRequired": "Необходима е допълнителна сигурност",
"organizationRequiresAdditionalSteps": "Тази организация изисква допълнителни стъпки за сигурност, за да получите достъп до ресурсите.",
- "sessionExpired": "Сесията е изтекла",
- "sessionExpiredReauthRequired": "Вашата сесия изтече според политиката за сигурност на вашата организация. Моля, повторете автентификацията, за да продължите.",
- "reauthenticate": "Повторно автентифициране",
"completeTheseSteps": "Завършете тези стъпки",
"enableTwoFactorAuthentication": "Активирайте двуфакторното удостоверяване",
"completeSecuritySteps": "Завършете стъпките за сигурност",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Липсва идентификатор на организация или домейн",
"loadingDNSRecords": "Зареждане на DNS записи...",
"olmUpdateAvailableInfo": "Налична е актуализирана версия на Olm. Моля, актуализирайте до най-новата версия за най-добро преживяване.",
- "updateAvailableInfo": "На разположение е обновена версия. Моля, обновете до най-новата версия за най-добър опит.",
"client": "Клиент",
"proxyProtocol": "Настройки на прокси протокол",
"proxyProtocolDescription": "Конфигурирайте Proxy Protocol, за да запазите IP адресите на клиентите за TCP услуги.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Добавяне на потребители.",
"editInternalResourceDialogAddClients": "Добавяне на клиенти.",
"editInternalResourceDialogDestinationLabel": "Дестинация.",
- "editInternalResourceDialogDestinationDescription": "Конфигурирайте как клиентите да достигнат до този ресурс.",
+ "editInternalResourceDialogDestinationDescription": "Посочете адреса дестинация за вътрешния ресурс. Това може да бъде име на хост, IP адрес или CIDR обхват в зависимост от избрания режим. По избор настройте вътрешен DNS алиас за по-лесно идентифициране.",
"internalResourceFormMultiSiteRoutingHelp": "Избирайки няколко сайта, се осигурява сигурен път и пренасочване при висока достъпност.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Научете повече",
"editInternalResourceDialogPortRestrictionsDescription": "Ограничете достъпа до конкретни TCP/UDP портове или позволете/блокирайте всички портове.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Бял списък на имейли",
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
- "resourceLauncherTitle": "Стартер за Ресурси",
- "resourceSidebarLauncherTitle": "Стартирано устройство",
- "resourceLauncherDescription": "Вижте всички налични ресурси и ги стартирайте от един централен център",
- "resourceLauncherSearchPlaceholder": "Търсете вашите ресурси...",
- "resourceLauncherDefaultView": "По Подразбиране",
- "resourceLauncherSaveView": "Запазете Изгледа",
- "resourceLauncherSaveToCurrentView": "Запазете в Текущ Изглед",
- "resourceLauncherSaveDefaultPersonal": "Запази за мен",
- "resourceLauncherResetView": "Нулирайте Изгледа",
- "resourceLauncherResetSystemDefault": "Възстанови към системните настройки",
- "resourceLauncherSystemDefaultRestored": "Системните настройки са възстановени",
- "resourceLauncherSystemDefaultRestoredDescription": "По подразбиране изгледът е възстановен към оригиналните настройки.",
- "resourceLauncherSaveAsNewView": "Запазете като Нов Изглед",
- "resourceLauncherSaveAsNewViewDescription": "Дайте име на този изглед, за да запазите текущите си филтри и оформление.",
- "resourceLauncherSaveForEveryone": "Запазете за Всеки",
- "resourceLauncherSaveForEveryoneDescription": "Споделете този изглед с всички членове на организацията. Когато е изключено, изгледът е видим само за вас.",
- "resourceLauncherMakePersonal": "Направи Личен",
- "resourceLauncherFilter": "Филтър",
- "resourceLauncherFilterWithCount": "Филтър, {count} приложени",
- "resourceLauncherSort": "Сортиране",
- "resourceLauncherSortAscending": "Сортиране възходящо",
- "resourceLauncherSortDescending": "Сортиране низходящо",
- "resourceLauncherSettings": "Настройки",
- "resourceLauncherGroupBy": "Групирай По",
- "resourceLauncherGroupBySite": "Сайт",
- "resourceLauncherGroupByLabel": "Етикет",
- "resourceLauncherGroupByNone": "Няма",
- "resourceLauncherLayout": "Оформление",
- "resourceLauncherLayoutGrid": "Мрежа",
- "resourceLauncherLayoutList": "Списък",
- "resourceLauncherShowLabels": "Показване на Етикети",
- "resourceLauncherShowSiteTags": "Показване на Тагове на Сайт",
- "resourceLauncherShowRecents": "Показване на Последни",
- "resourceLauncherDeleteView": "Изтриване на Изглед",
- "resourceLauncherDeleteViewTitle": "Изтриване на изглед",
- "resourceLauncherDeleteViewQuestion": "Сигурни ли сте, че искате да изтриете този изглед на стартиращото устройство?",
- "resourceLauncherDeleteViewConfirm": "Изтриване на изглед",
- "resourceLauncherViewAsAdmin": "Вижте като Админ",
- "resourceLauncherResourceDetailsDescription": "Информация и статус на връзката за този ресурс.",
- "resourceLauncherResourceDetails": "Детайли за ресурса",
- "resourceLauncherAuthMethodsDescription": "Методи на автентикация, активирани за този ресурс.",
- "resourceLauncherPrivateClientRequired": "Свържете се с клиент на вашето устройство, за да получите частен достъп до този ресурс.",
- "resourceLauncherPrivateClientRequiredTitle": "Изисква се връзка с клиента",
- "resourceLauncherDownloadClient": "Изтеглете клиент",
- "resourceLauncherFailedToLoadDetails": "Не може да се зареди информацията за ресурса. Може вече да нямате достъп до този ресурс.",
- "resourceLauncherNoPortRestrictions": "Няма ограничения за порт",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Без Етикет",
- "resourceLauncherNoSite": "Няма Сайт",
- "resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
- "resourceLauncherEmptyStateTitle": "Няма Налични Ресурси",
- "resourceLauncherEmptyStateDescription": "Все още нямате достъп до никакви ресурси. Свържете се с вашия администратор, за да поискате достъп.",
- "resourceLauncherEmptyStateNoResultsTitle": "Няма Намерени Ресурси",
- "resourceLauncherEmptyStateNoResultsDescription": "Никакви ресурси не съвпадат с текущото ви търсене или филтри. Опитайте да ги коригирате, за да намерите това, което търсите.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Никакви ресурси не съвпадат с \"{query}\". Опитайте да коригирате търсенето или да изтриете филтри, за да видите всички ресурси.",
- "resourceLauncherSearchFirstTitle": "Търсете или филтрирайте за да разгледате",
- "resourceLauncherSearchFirstDescription": "Имате достъп до много ресурси. Използвайте търсене или филтър по сайт или етикет, за да намерите това, от което се нуждаете.",
- "resourceLauncherSiteGroupingDisabled": "Групирането по сайт не е налично при този мащаб. Филтрирайте по сайт, за да групирате по-малък набор.",
- "resourceLauncherLabelGroupingDisabled": "Групирането по етикет не е налично при този мащаб.",
- "resourceLauncherCompactModeHint": "Показване на опростен списък за по-бързо разглеждане. Използвайте търсене или филтри за да стесните резултатите.",
- "resourceLauncherCompactGroupingHint": "Прилагайте филтри за сайт или етикет, за да активирате групирането.",
- "resourceLauncherCopiedToClipboard": "Копирано в клипборда",
- "resourceLauncherCopiedAccessDescription": "Достъпът до ресурса е копиран на вашия клипборд.",
- "resourceLauncherViewNamePlaceholder": "Име на Изгледа",
- "resourceLauncherViewNameLabel": "Име на Изгледа",
- "resourceLauncherViewSaved": "Изгледът е запазен",
- "resourceLauncherViewSavedDescription": "Вашият изглед на стартер е запазен.",
- "resourceLauncherViewSaveFailed": "Неуспешно запазване на изгледа",
- "resourceLauncherViewSaveFailedDescription": "Не можеше да се запази изгледът на стартер. Моля, опитайте отново.",
- "resourceLauncherViewDeleted": "Изгледът е изтрит",
- "resourceLauncherViewDeletedDescription": "Изгледът на стартер е изтрит.",
- "resourceLauncherViewDeleteFailed": "Неуспешно изтриване на изгледа",
- "resourceLauncherViewDeleteFailedDescription": "Не можахте да изтриете изгледа на стартер. Моля, опитайте отново.",
"memberPortalPrevious": "Предишен",
"memberPortalNext": "Следващ",
"httpSettings": "HTTP настройки",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----НАЧАЛО НА OPENSSH ЧАСТЕН КЛЮЧ-----",
"sshPrivateKeyRequired": "Изисква се частен ключ",
"vncTitle": "VNC",
- "vncSignInDescription": "Въведете VNC данните си за връзка",
- "vncUsernameOptional": "Потребителско име (по избор)",
+ "vncSignInDescription": "Въведете вашата VNC парола за свързване",
"vncPasswordOptional": "Парола (по избор)",
"vncNoResourceTarget": "Не е налична цел за ресурса",
"vncFailedToLoadNovnc": "Неуспешно зареждане на noVNC",
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index f456ee3b9..581f37762 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -66,15 +66,9 @@
"local": "Místní",
"edit": "Upravit",
"siteConfirmDelete": "Potvrdit odstranění lokality",
- "siteConfirmDeleteAndResources": "Potvrdit odstranění lokality a zdrojů",
"siteDelete": "Odstranění lokality",
- "siteDeleteAndResources": "Odstranit lokalitu a zdroje",
"siteMessageRemove": "Po odstranění webu již nebude přístupný. Všechny cíle spojené s webem budou také odstraněny.",
- "siteMessageRemoveAndResources": "Toto trvale odstraní všechny veřejné a soukromé zdroje spojené s touto lokalitou, i když je zdroj také přiřazen k jiným lokalitám.",
"siteQuestionRemove": "Jste si jisti, že chcete odstranit tuto stránku z organizace?",
- "siteQuestionRemoveAndResources": "Opravdu chcete odstranit tuto lokalitu a všechny přidružené zdroje?",
- "sitesTableDeleteSite": "Odstranění lokality",
- "sitesTableDeleteSiteAndResources": "Odstranit lokalitu a zdroje",
"siteManageSites": "Správa lokalit",
"siteDescription": "Vytvořte a spravujte stránky pro povolení připojení k soukromým sítím",
"sitesBannerTitle": "Připojit jakoukoli síť",
@@ -123,16 +117,6 @@
"siteUpdated": "Lokalita upravena",
"siteUpdatedDescription": "Lokalita byla upravena.",
"siteGeneralDescription": "Upravte obecná nastavení pro tuto lokalitu",
- "siteRestartTitle": "Restartovat lokalitu",
- "siteRestartDescription": "Restartujte tunel WireGuard pro tuto lokalitu. To krátce přeruší konektivitu.",
- "siteRestartBody": "Použijte to, pokud tunel lokality nefunguje správně a chcete vynutit opětovné připojení bez restartování hostitele.",
- "siteRestartButton": "Restartovat lokalitu",
- "siteRestartDialogMessage": "Opravdu chcete restartovat WireGuard tunel pro {name}? Lokalita krátce ztratí konektivitu.",
- "siteRestartWarning": "Lokalita bude krátce odpojena, zatímco se tunel restartuje.",
- "siteRestarted": "Lokalita restartována",
- "siteRestartedDescription": "Tunel WireGuard byl restartován.",
- "siteErrorRestart": "Nepodařilo se restartovat lokalitu",
- "siteErrorRestartDescription": "Při restartování lokality došlo k chybě.",
"siteSettingDescription": "Konfigurace nastavení na webu",
"siteResourcesTab": "Zdroje",
"siteResourcesNoneOnSite": "Tento web zatím nemá veřejné ani soukromé zdroje.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy požadavky na syrové TCP/UDP pomocí čísla portu. Vyžaduje připojení stránek ke vzdálenému uzlu.",
"resourceCreate": "Vytvořit zdroj",
"resourceCreateDescription": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili nový zdroj",
- "resourcePublicCreate": "Vytvořit veřejný zdroj",
- "resourcePublicCreateDescription": "Postupujte podle kroků níže pro vytvoření nového veřejného zdroje přístupného přes webový prohlížeč",
"resourceCreateGeneralDescription": "Konfigurace základních nastavení zdroje včetně názvu a typu",
"resourceSeeAll": "Zobrazit všechny zdroje",
"resourceCreateGeneral": "Obecné",
@@ -615,8 +597,7 @@
"idpNameInternal": "Interní",
"emailInvalid": "Neplatná e-mailová adresa",
"inviteValidityDuration": "Zvolte prosím dobu trvání",
- "accessRoleSelectPlease": "Uživatel musí patřit minimálně do jedné role.",
- "accessRoleRequired": "Role je vyžadována",
+ "accessRoleSelectPlease": "Vyberte prosím roli",
"removeOwnAdminRoleConfirmTitle": "Odebrat přístup správce?",
"removeOwnAdminRoleConfirmDescription": "Po uložení již nebudete mít oprávnění správce v této organizaci. Další administrátor vám může přístup obnovit, pokud bude potřeba.",
"removeOwnAdminRoleConfirmButton": "Odebrat mé administrátorské oprávnění",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Základní Ověření Záhlaví",
"policyAuthHeaderAuthDescription": "Ověřit vlastní HTTP hlavičku názvu a hodnoty při každém požadavku",
"policyAuthHeaderAuthSummary": "Nastaveno hlavička",
- "policyAuthHeaderName": "Uživatelské jméno",
- "policyAuthHeaderValue": "Heslo",
+ "policyAuthHeaderName": "Název hlavičky",
+ "policyAuthHeaderValue": "Očekávaná hodnota",
"policyAuthSetPasscode": "Nastavit přístupový kód",
"policyAuthSetPincode": "Nastavit PIN kód",
"policyAuthSetEmailWhitelist": "Nastavit e-mailový whitelist",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Použít plán",
"actionListBlueprints": "Seznam šablon",
"actionGetBlueprint": "Získat šablonu",
- "actionCreateOrgWideLauncherView": "Vytvořit organizační pohled",
"setupToken": "Nastavit token",
"setupTokenDescription": "Zadejte nastavovací token z konzole serveru.",
"setupTokenRequired": "Je vyžadován token nastavení",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Nastavit zdrojový kód",
"actionSetResourceEmailWhitelist": "Nastavit seznam povolených dokumentů",
"actionGetResourceEmailWhitelist": "Získat seznam povolených dokumentů",
- "actionGetResourcePolicy": "Získat zásady zdroje",
- "actionUpdateResourcePolicy": "Aktualizovat zásady zdroje",
- "actionSetResourcePolicyUsers": "Nastavte uživatele pro zásady zdroje",
- "actionSetResourcePolicyRoles": "Nastavte role pro zásady zdroje",
- "actionSetResourcePolicyPassword": "Nastavte heslo pro zásady zdroje",
- "actionSetResourcePolicyPincode": "Nastavit PIN kód Zásady zdroje",
- "actionSetResourcePolicyHeaderAuth": "Nastavit autentizaci hlavičky zásady zdroje",
- "actionSetResourcePolicyWhitelist": "Nastavit seznam povolených e-mailů v zásadě zdroje",
- "actionSetResourcePolicyRules": "Nastavit pravidla zásady zdroje",
"actionCreateTarget": "Create Target",
"actionDeleteTarget": "Odstranit cíl",
"actionGetTarget": "Získat cíl",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Generovat přístupový token",
"actionDeleteAccessToken": "Odstranit přístupový token",
"actionListAccessTokens": "Seznam přístupových tokenů",
- "actionCreateResourceSessionToken": "Vytvořit token relace prostředku",
"actionCreateResourceRule": "Vytvořit pravidlo pro zdroj",
"actionDeleteResourceRule": "Odstranit pravidlo pro dokument",
"actionListResourceRules": "Seznam pravidel zdrojů",
@@ -1520,30 +1490,6 @@
"navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace",
"navbarDocsLink": "Dokumentace",
- "commandPaletteTitle": "Paleta příkazů",
- "commandPaletteDescription": "Vyhledávejte stránky, organizace, zdroje a akce",
- "commandPaletteSearchPlaceholder": "Hledat stránky, zdroje, akce...",
- "commandPaletteNoResults": "Nebyly nalezeny žádné výsledky.",
- "commandPaletteSearching": "Hledání...",
- "commandPaletteNavigation": "Navigace",
- "commandPaletteOrganizations": "Organizace",
- "commandPaletteSites": "Lokality",
- "commandPaletteResources": "Zdroje",
- "commandPaletteUsers": "Uživatelé",
- "commandPaletteClients": "Strojoví klienti",
- "commandPaletteActions": "Akce",
- "commandPaletteCreateSite": "Vytvořit lokalitu",
- "commandPaletteCreateProxyResource": "Vytvořit veřejný zdroj",
- "commandPaletteCreatePrivateResource": "Vytvořit privátní zdroj",
- "commandPaletteCreateUser": "Vytvořit uživatele",
- "commandPaletteCreateApiKey": "Vytvořit API klíč",
- "commandPaletteCreateMachineClient": "Vytvořit strojového klienta",
- "commandPaletteCreateAlertRule": "Vytvořit pravidlo upozornění",
- "commandPaletteCreateIdentityProvider": "Vytvořit poskytovatele identity",
- "commandPaletteToggleTheme": "Přepnout téma",
- "commandPaletteChooseOrganization": "Vybrat organizaci",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "2FA nelze povolit",
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
"otpSetupCheckCode": "Zadejte 6místný kód",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Správa",
"sidebarBillingAndLicenses": "Fakturace a licence",
"sidebarLogsAnalytics": "Analytici",
- "commandSites": "Lokality",
- "commandActionModeInfo": "Napište \">\" pro otevření režimu akcí",
- "commandResources": "Zdroje",
- "commandProxyResources": "Veřejné zdroje",
- "commandClientResources": "Privátní zdroje",
- "commandClients": "Klienti",
- "commandUserDevices": "Uživatelská zařízení",
- "commandMachineClients": "Strojoví klienti",
- "commandDomains": "Domény",
- "commandRemoteExitNodes": "Vzdálené uzly",
- "commandTeam": "Tým",
- "commandUsers": "Uživatelé",
- "commandRoles": "Role",
- "commandInvitations": "Pozvánky",
- "commandPolicies": "Sdílené zásady",
- "commandResourcePolicies": "Zásady veřejných zdrojů",
- "commandIdentityProviders": "Poskytovatelé identity",
- "commandApprovals": "Žádosti o schválení",
- "commandShareableLinks": "Sdílené odkazy",
- "commandOrganization": "Organizace",
- "commandLogsAndAnalytics": "Protokoly a analytika",
- "commandLogsAnalytics": "Analytika",
- "commandLogsRequest": "Protokoly požadavků na HTTP",
- "commandLogsAccess": "Protokoly autentizace",
- "commandLogsAction": "Protokoly akcí administrátora",
- "commandLogsConnection": "Protokoly připojení",
- "commandLogsStreaming": "Streamování událostí",
- "commandManagement": "Řízení",
- "commandAlerting": "Upozorňování",
- "commandProvisioning": "Zajištění",
- "commandBluePrints": "Blueprinty",
- "commandApiKeys": "API klíče",
- "commandBillingAndLicenses": "Účty a licence",
- "commandBilling": "Účty",
- "commandEnterpriseLicenses": "Licence",
- "commandSettings": "Nastavení",
- "commandLauncher": "Spouštěč",
- "commandResourceLauncher": "Spouštěč zdrojů",
- "commandSearchResults": "Výsledky hledání",
"alertingTitle": "Upozornění",
"alertingDescription": "Definujte zdroje, spouštěče a akce pro oznámení",
"alertingRules": "Pravidla upozornění",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domény",
"billingOrganizations": "Tělo",
"billingRemoteExitNodes": "Vzdálené uzly",
- "billingPublicResources": "Veřejné zdroje",
- "billingPrivateResources": "Soukromé zdroje",
- "billingMachineClients": "Strojní klienti",
"billingNoLimitConfigured": "Žádný limit nenastaven",
"billingEstimatedPeriod": "Odhadované období fakturace",
"billingIncludedUsage": "Zahrnuto využití",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Kolik uživatelů můžete použít",
"billingDomainInfo": "Kolik domén můžete použít",
"billingRemoteExitNodesInfo": "Kolik vzdálených uzlů můžete použít",
- "billingPublicResourcesInfo": "Kolik veřejných zdrojů můžete využít",
- "billingPrivateResourcesInfo": "Kolik soukromých zdrojů můžete využít",
- "billingMachineClientsInfo": "Kolik strojních klientů můžete využít",
"billingLicenseKeys": "Licenční klíče",
"billingLicenseKeysDescription": "Spravovat předplatné licenčního klíče",
"billingLicenseSubscription": "Předplatné licence",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Podsíť",
"addressDescription": "Interní adresa klienta. Musí spadat do podsítě organizace.",
"selectSites": "Vyberte stránky",
- "selectLabels": "Vyberte názvy",
"sitesDescription": "Klient bude mít připojení k vybraným webům",
"clientInstallOlm": "Nainstalovat Olm",
"clientInstallOlmDescription": "Stáhněte si Olm běžící ve vašem systému",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Zavřít",
"createInternalResourceDialogCreateClientResource": "Vytvořit soukromý zdroj",
"createInternalResourceDialogCreateClientResourceDescription": "Vytvořte nový zdroj, který bude přístupný pouze klientům připojeným k organizaci",
- "privateResourceGeneralDescription": "Nastavte název, identifikátor a další obecná nastavení zdroje.",
- "privateResourceCreatePageSeeAll": "Zobrazit všechny privátní zdroje",
- "privateResourceAllowIcmpPing": "Povolit ICMP ping",
- "privateResourceNetworkAccess": "Přístup k síti",
- "privateResourceNetworkAccessDescription": "Ovládejte přístup k TCP/UDP portům a zda je pro tento zdroj povolen ICMP ping.",
- "hostSettings": "Nastavení hostitele",
- "cidrSettings": "Nastavení CIDR",
"createInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
"createInternalResourceDialogName": "Jméno",
"createInternalResourceDialogSite": "Lokalita",
"selectSite": "Vybrat lokalitu...",
"multiSitesSelectorSitesCount": "{count, plural, one {# web} few {# weby} many {# webů} other {# weby}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# název} few {# názvy} many {# názvů} other {# názvů}}",
"noSitesFound": "Nebyly nalezeny žádné lokality.",
"createInternalResourceDialogProtocol": "Protokol",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
- "internalResourceAliasLocalWarning": "Aliasy končící na .local mohou způsobit problémy s vyřešením díky mDNS v některých sítích.",
"internalResourceDownstreamSchemeRequired": "HTTP metoda je vyžadována pro HTTP zdroje",
"internalResourceHttpPortRequired": "Přípoječný port je nutný pro HTTP zdroj",
"siteConfiguration": "Konfigurace",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Vzdálené uzly",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Tajný klíč",
- "remoteExitNodeNetworkingTitle": "Nastavení sítě",
- "remoteExitNodeNetworkingDescription": "Nastavte, jak tento vzdálený výstupní uzel směruje provoz a které lokality se mají připojit přes něj. Pokročilé funkce pro použití s konfiguracemi zpětné sítě.",
- "remoteExitNodeNetworkingSave": "Uložit nastavení",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Nastavení sítě bylo úspěšně uloženo",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Nastavení sítě bylo úspěšně aktualizováno.",
- "remoteExitNodeNetworkingSaveError": "Nepodařilo se uložit nastavení sítě",
- "remoteExitNodeNetworkingSubnetsTitle": "Dálkové podsítě",
- "remoteExitNodeNetworkingSubnetsDescription": "Definujte rozsahy CIDR, ke kterým tento vzdálený výstupní uzel bude směrovat provoz. Zadejte platné CIDR (např. 10.0.0.0/8) a stiskněte Enter pro přidání.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Přidejte rozsah CIDR (např. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Nepodařilo se načíst podsítě",
- "remoteExitNodeNetworkingLabelsTitle": "Názvy preferencí",
- "remoteExitNodeNetworkingLabelsDescription": "Weby s těmito názvy budou nuceny připojit se tímto vzdáleným výstupním uzlem.",
- "remoteExitNodeNetworkingLabelsButtonText": "Vyberte názvy...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Hledat názvy...",
- "remoteExitNodeNetworkingLabelsLoadError": "Nepodařilo se načíst názvy",
"remoteExitNodeCreate": {
"title": "Vytvořit vzdálený uzel",
"description": "Vytvořte nový vlastní hostovaný vzdálený relační a proxy server uzel",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Pro tuto organizaci nejsou k dispozici žádné uzly. Nejprve vytvořte uzel pro použití lokálních stránek.",
"exitNode": "Ukončit uzel",
"country": "L 343, 22.12.2009, s. 1).",
- "countryIsNot": "Země není",
"rulesMatchCountry": "Aktuálně založené na zdrojové IP adrese",
"region": "Oblasti",
"selectRegion": "Vyberte region",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Poskytovatel Google OAuth2/OIDC",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Podsíť",
- "utilitySubnet": "Nástrojová podsíť",
"subnetDescription": "Podsíť pro konfiguraci sítě této organizace.",
"customDomain": "Vlastní doména",
"authPage": "Autentizační stránky",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Je vyžadováno nastavení dvoufaktorového ověřování. Přihlaste se znovu pomocí {dashboardUrl}/autentizace/přihlášení dokončí tento krok. Poté se vraťte zde.",
"additionalSecurityRequired": "Vyžadováno další zabezpečení",
"organizationRequiresAdditionalSteps": "Tato organizace vyžaduje další bezpečnostní kroky, než budete moci přistupovat ke zdrojům.",
- "sessionExpired": "Session vypršela",
- "sessionExpiredReauthRequired": "Vaše session vypršela podle bezpečnostní politiky organizace. Prosím, znovu se přihlaste, abyste mohli pokračovat.",
- "reauthenticate": "Znovu autentizovat",
"completeTheseSteps": "Dokončete tyto kroky",
"enableTwoFactorAuthentication": "Povolit dvoufaktorové ověření",
"completeSecuritySteps": "Dokončit bezpečnostní kroky",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Chybí ID organizace nebo domény",
"loadingDNSRecords": "Načítání DNS záznamů...",
"olmUpdateAvailableInfo": "Je k dispozici aktualizovaná verze Olm. Pro nejlepší zážitek prosím aktualizujte na nejnovější verzi.",
- "updateAvailableInfo": "Je k dispozici aktualizovaná verze. Aktualizujte prosím na nejnovější verzi pro nejlepší zážitek.",
"client": "Zákazník",
"proxyProtocol": "Nastavení proxy protokolu",
"proxyProtocolDescription": "Konfigurace Proxy protokolu pro zachování klientských IP adres pro služby TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Přidat uživatele",
"editInternalResourceDialogAddClients": "Přidat klienty",
"editInternalResourceDialogDestinationLabel": "Cíl",
- "editInternalResourceDialogDestinationDescription": "Nakonfigurujte, jak klienti dosáhnou tohoto zdroje.",
+ "editInternalResourceDialogDestinationDescription": "Určete cílovou adresu pro interní prostředek. Může se jednat o hostname, IP adresu, nebo rozsah CIDR v závislosti na vybraném režimu. Volitelně nastavte interní DNS alias pro snazší identifikaci.",
"internalResourceFormMultiSiteRoutingHelp": "Výběrem více webů se povolí odolné směrování a přepojení pro vysokou dostupnost.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Zjistit více",
"editInternalResourceDialogPortRestrictionsDescription": "Omezte přístup na specifické TCP/UDP porty nebo povolte/blokujte všechny porty.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Seznam povolených emailů",
"memberPortalResourceDisabled": "Zdroj je zakázán",
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
- "resourceLauncherTitle": "Spouštěč zdrojů",
- "resourceSidebarLauncherTitle": "Spouštěč",
- "resourceLauncherDescription": "Prohlédněte si všechny dostupné zdroje a spusťte je z jednoho centrálního hubu",
- "resourceLauncherSearchPlaceholder": "Prohledejte vaše zdroje...",
- "resourceLauncherDefaultView": "Výchozí",
- "resourceLauncherSaveView": "Uložit pohled",
- "resourceLauncherSaveToCurrentView": "Uložit do aktuálního pohledu",
- "resourceLauncherSaveDefaultPersonal": "Uložit pro mě",
- "resourceLauncherResetView": "Obnovit pohled",
- "resourceLauncherResetSystemDefault": "Obnovit na systémové nastavení",
- "resourceLauncherSystemDefaultRestored": "Systémové nastavení bylo obnoveno",
- "resourceLauncherSystemDefaultRestoredDescription": "Výchozí zobrazení bylo resetováno na původní nastavení.",
- "resourceLauncherSaveAsNewView": "Uložit jako nový pohled",
- "resourceLauncherSaveAsNewViewDescription": "Uložte tento pohled k uloženému filtrování a rozvržení.",
- "resourceLauncherSaveForEveryone": "Uložit pro všechny",
- "resourceLauncherSaveForEveryoneDescription": "Sdílejte tento pohled se všemi členy organizace. Pokud není zaškrtnuto, pohled je viditelný pouze vám.",
- "resourceLauncherMakePersonal": "Udělat osobní",
- "resourceLauncherFilter": "Filtr",
- "resourceLauncherFilterWithCount": "Filtr, {count} aplikováno",
- "resourceLauncherSort": "Řadit",
- "resourceLauncherSortAscending": "Řadit vzestupně",
- "resourceLauncherSortDescending": "Řadit sestupně",
- "resourceLauncherSettings": "Nastavení",
- "resourceLauncherGroupBy": "Seskupit podle",
- "resourceLauncherGroupBySite": "Lokalita",
- "resourceLauncherGroupByLabel": "Název",
- "resourceLauncherGroupByNone": "Žádný",
- "resourceLauncherLayout": "Rozvržení",
- "resourceLauncherLayoutGrid": "Mřížka",
- "resourceLauncherLayoutList": "Seznam",
- "resourceLauncherShowLabels": "Zobrazit název",
- "resourceLauncherShowSiteTags": "Zobrazit značky lokality",
- "resourceLauncherShowRecents": "Zobrazit nedávné",
- "resourceLauncherDeleteView": "Smazat pohled",
- "resourceLauncherDeleteViewTitle": "Odstranění zobrazení",
- "resourceLauncherDeleteViewQuestion": "Opravdu chcete smazat toto zobrazení launcheru?",
- "resourceLauncherDeleteViewConfirm": "Odstranění zobrazení",
- "resourceLauncherViewAsAdmin": "Zobrazit jako administrátor",
- "resourceLauncherResourceDetailsDescription": "Informace o připojení a stavu pro tento zdroj.",
- "resourceLauncherResourceDetails": "Podrobnosti o zdroji",
- "resourceLauncherAuthMethodsDescription": "Povolené metody autentizace pro tento zdroj.",
- "resourceLauncherPrivateClientRequired": "Připojte se s klientem na vašem zařízení, abyste měli k tomuto zdroji soukromý přístup.",
- "resourceLauncherPrivateClientRequiredTitle": "K připojení klienta vyžadováno",
- "resourceLauncherDownloadClient": "Stáhnout klienta",
- "resourceLauncherFailedToLoadDetails": "Nemohu načíst podrobnosti o zdroji. K tomuto zdroji možná již nemáte přístup.",
- "resourceLauncherNoPortRestrictions": "Žádná omezení portu",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Bez nálepky",
- "resourceLauncherNoSite": "Žádná lokalita",
- "resourceLauncherNoResourcesInGroup": "V této skupině nejsou žádné zdroje",
- "resourceLauncherEmptyStateTitle": "Žádné dostupné zdroje",
- "resourceLauncherEmptyStateDescription": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého administrátora, abyste požádali o přístup.",
- "resourceLauncherEmptyStateNoResultsTitle": "Nebyl nalezen žádný zdroj",
- "resourceLauncherEmptyStateNoResultsDescription": "Žádný zdroj neodpovídá vašemu aktuálnímu vyhledávání nebo filtrům. Zkuste je upravit, abyste našli to, co hledáte.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Žádné zdroje neodpovídají \"{query}\". Zkuste upravit vyhledávání nebo vymazat filtry, abyste viděli všechny zdroje.",
- "resourceLauncherSearchFirstTitle": "Hledat nebo filtrovat k procházení",
- "resourceLauncherSearchFirstDescription": "Máte přístup k mnoha zdrojům. Použijte vyhledávání nebo filtr podle místa či štítku k nalezení toho, co potřebujete.",
- "resourceLauncherSiteGroupingDisabled": "Na této úrovni není možné seskupení podle místa. Pro skupinování menší sady se použije filtr podle místa.",
- "resourceLauncherLabelGroupingDisabled": "Na této úrovni není možné seskupení podle štítku.",
- "resourceLauncherCompactModeHint": "Zobrazování zjednodušeného seznamu pro rychlejší procházení. K upřesnění výsledků použijte vyhledávání nebo filtry.",
- "resourceLauncherCompactGroupingHint": "Aplikujte filtry místa nebo štítku, abyste povolili seskupování.",
- "resourceLauncherCopiedToClipboard": "Zkopírováno do schránky",
- "resourceLauncherCopiedAccessDescription": "Přístup ke zdroji byl zkopírován do vaší schránky.",
- "resourceLauncherViewNamePlaceholder": "Název pohledu",
- "resourceLauncherViewNameLabel": "Název pohledu",
- "resourceLauncherViewSaved": "Pohled uložen",
- "resourceLauncherViewSavedDescription": "Váš spouštěcí pohled byl uložen.",
- "resourceLauncherViewSaveFailed": "Nepodařilo se uložit pohled",
- "resourceLauncherViewSaveFailedDescription": "Nepodařilo se uložit spouštěcí pohled. Prosím zkuste to znovu.",
- "resourceLauncherViewDeleted": "Pohled smazán",
- "resourceLauncherViewDeletedDescription": "Spouštěcí pohled byl smazán.",
- "resourceLauncherViewDeleteFailed": "Nepodařilo se smazat pohled",
- "resourceLauncherViewDeleteFailedDescription": "Nepodařilo se smazat spouštěcí pohled. Prosím zkuste to znovu.",
"memberPortalPrevious": "Předchozí",
"memberPortalNext": "Následující",
"httpSettings": "Nastavení HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----ZAČÁTEK SOUKROMÉHO KLÍČE OPENSSH-----",
"sshPrivateKeyRequired": "Je vyžadován soukromý klíč",
"vncTitle": "VNC",
- "vncSignInDescription": "Zadejte své VNC přihlašovací údaje pro připojení",
- "vncUsernameOptional": "Uživatelské jméno (nepovinné)",
+ "vncSignInDescription": "Zadejte své heslo VNC pro připojení",
"vncPasswordOptional": "Heslo (nepovinné)",
"vncNoResourceTarget": "Není k dispozici žádný cíl zdroje",
"vncFailedToLoadNovnc": "Nepodařilo se načíst noVNC",
diff --git a/messages/da-DK.json b/messages/da-DK.json
deleted file mode 100644
index ce08bff23..000000000
--- a/messages/da-DK.json
+++ /dev/null
@@ -1,3805 +0,0 @@
-{
- "contactSalesEnable": "Kontakt salgsafdelingen for at aktivere denne funktion.",
- "contactSalesBookDemo": "Book en demo",
- "contactSalesOr": "eller",
- "contactSalesContactUs": "kontakt os",
- "setupCreate": "Opret organisationen, sitet og ressourcerne",
- "headerAuthCompatibilityInfo": "Aktivér dette for at fremtvinge et 401 Uautoriseret-svar, når der mangler et autentificeringstoken. Dette kræves for browsere eller specifikke HTTP-biblioteker, der ikke sender legitimationsoplysninger uden en serverchallenge.",
- "headerAuthCompatibility": "Udvidet kompatibilitet",
- "setupNewOrg": "Ny organisation",
- "setupCreateOrg": "Opret organisation",
- "setupCreateResources": "Opret ressourcer",
- "setupOrgName": "Organisationsnavn",
- "orgDisplayName": "Dette er organisationens visningsnavn.",
- "orgId": "Organisations-ID",
- "setupIdentifierMessage": "Dette er den unikke identifikator for organisationen.",
- "setupErrorIdentifier": "Organisations-ID'et er allerede taget. Vælg venligst en anden.",
- "componentsErrorNoMemberCreate": "Du er i øjeblikket ikke medlem af nogen organisationer. Opret en organisation for at komme i gang.",
- "componentsErrorNoMember": "Du er i øjeblikket ikke medlem af nogen organisationer.",
- "welcome": "Velkommen!",
- "welcomeTo": "Velkommen til",
- "componentsCreateOrg": "Opret en organisation",
- "componentsMember": "Du er {count, plural, =0 {ikke medlem af nogen organisationer} one {medlem af en organisation} other {medlem af # organisationer}}.",
- "componentsInvalidKey": "Ugyldig eller udløbet licensnøgle registreret. Følg licensvilkårene for fortsat at kunne bruge alle funktionerne.",
- "dismiss": "Afvis",
- "subscriptionViolationMessage": "Du er uden for grænsen for din nuværende plan. Ret problemet ved at fjerne sites, brugere eller andre ressourcer, så du igen er inden for din plan.",
- "trialBannerMessage": "Din prøveperiode udløber om {countdown}. Opgrader for at bevare adgangen.",
- "trialBannerExpired": "Din prøveperiode er udløbet. Opgrader nu for at gendanne adgangen.",
- "billingTrialBannerTitle": "Prøveversion aktiv",
- "billingTrialBannerDescription": "Du har i øjeblikket en gratis prøveversion på Business-niveauet. Når prøveperioden slutter, vil din konto automatisk gå tilbage til funktioner og begrænsninger på Basis-niveauet. Opgrader når som helst for at beholde adgang til de den nuværende plans funktioner.",
- "billingTrialBannerUpgrade": "Opgrader nu",
- "billingTrialBadge": "Prøveversion",
- "trialActive": "Gratis prøveversion aktiv",
- "trialExpired": "Prøveperioden er udløbet",
- "trialHasEnded": "Din prøveperiode har afsluttet.",
- "trialDaysRemaining": "{count, plural, one {# dag tilbage} other {# dage tilbage}}",
- "trialDaysLeftShort": "{days}d tilbage af prøveperioden",
- "trialGoToBilling": "Gå til faktureringssiden",
- "subscriptionViolationViewBilling": "Vis fakturering",
- "componentsLicenseViolation": "Licensbrud: Denne server bruger {usedSites} sites, hvilket overskrider den licenserede grænse på {maxSites} sites. Følg licensvilkårene for fortsat at kunne bruge alle funktionerne.",
- "componentsSupporterMessage": "Tak fordi du støtter Pangolin som {tier}!",
- "inviteErrorNotValid": "Beklager, men det ser ud som invitationen du prøver at bruge ikke er blevet accepteret eller ikke er gyldig længere.",
- "inviteErrorUser": "Vi beklager, men det ser ud til, at invitationen, du prøver at få adgang til, ikke er til denne bruger.",
- "inviteLoginUser": "Venligst tjek at du er loget ind som korrekt bruger.",
- "inviteErrorNoUser": "Beklager, men det ser ud til, at invitationen du prøver at få adgang til, ikke er til en eksisterende bruger.",
- "inviteCreateUser": "Opret venligst en konto først.",
- "goHome": "Gå hjem",
- "inviteLogInOtherUser": "Log ind som en anden bruger",
- "createAnAccount": "Opret konto",
- "inviteNotAccepted": "Invitationen ikke accepteret",
- "authCreateAccount": "Opret en konto for at komme i gang",
- "authNoAccount": "Har du ikke en konto?",
- "email": "E-mail",
- "password": "Adgangskode",
- "confirmPassword": "Bekræft adgangskode",
- "createAccount": "Opret konto",
- "viewSettings": "Vis indstillinger",
- "delete": "Slet",
- "name": "Navn",
- "online": "Online",
- "offline": "Offline",
- "site": "Websted",
- "dataIn": "Data ind",
- "dataOut": "Data ud",
- "connectionType": "Forbindelsestype",
- "tunnelType": "Tunneltype",
- "local": "Lokal",
- "edit": "Rediger",
- "siteConfirmDelete": "Bekræft Sletning af Site",
- "siteConfirmDeleteAndResources": "Bekræft sletning af sted og ressourcer",
- "siteDelete": "Slet Site",
- "siteDeleteAndResources": "Slet Sted og Ressourcer",
- "siteMessageRemove": "Når sitet er fjernet, vil det ikke længere være tilgængeligt. Alle mål for sitet vil også blive fjernet.",
- "siteMessageRemoveAndResources": "Dette vil permanent slette alle offentlige og private ressourcer knyttet til dette sted, selv hvis en ressource også er knyttet til andre steder.",
- "siteQuestionRemove": "Er du sikker på at du vil fjerne sitet fra organisationen?",
- "siteQuestionRemoveAndResources": "Er du sikker på, at du vil slette dette sted og alle tilknyttede ressourcer?",
- "sitesTableDeleteSite": "Slet sted",
- "sitesTableDeleteSiteAndResources": "Slet sted og ressourcer",
- "siteManageSites": "Administrer Sites",
- "siteDescription": "Oprette og administrer sites for at aktivere forbindelse til private netværk",
- "sitesBannerTitle": "Opret forbindelse til alle netværk",
- "sitesBannerDescription": "Et netværk er en forbindelse til et eksternt netværk som tillader Pangolin at give adgang til ressourcer, enten offentlige eller private, til brugere hvor som helst. Installer netværksconnectoren (Newt) hvor som helst du kan køre en binærfil eller container for at oprette forbindelsen.",
- "sitesBannerButtonText": "Installer site",
- "approvalsBannerTitle": "Godkend eller afvis adgang til enhed",
- "approvalsBannerDescription": "Gennemgå og godkend eller afvis anmodninger om adgang fra brugere. Når enhedsgodkendelser er påkrævet, skal brugere have administratorgodkendelse, før deres enheder kan oprette forbindelse til organisationens ressourcer.",
- "approvalsBannerButtonText": "Læs mere",
- "siteCreate": "Opret site",
- "siteCreateDescription2": "Følg trinene nedenfor for at oprette og opret forbindelse til et nyt site",
- "siteCreateDescription": "Opret et nyt site for at forbinde til ressourcer",
- "close": "Luk",
- "siteErrorCreate": "Fejl ved oprettelse af site",
- "siteErrorCreateKeyPair": "Nøglepar eller standardindstillinger for site ikke fundet",
- "siteErrorCreateDefaults": "Standardindstillinger for site ikke fundet",
- "method": "Metode",
- "siteMethodDescription": "Sådan eksponerer du forbindelser.",
- "siteLearnNewt": "Lær hvordan du installerer Newt på dit system",
- "siteSeeConfigOnce": "Du kan kun se konfigurationen én gang.",
- "siteLoadWGConfig": "Indlæser WireGuard-konfiguration...",
- "siteDocker": "Udvid for detaljer om Docker-deployment",
- "toggle": "Skift",
- "dockerCompose": "Docker Compose",
- "dockerRun": "Docker Run",
- "siteLearnLocal": "Lokale sites tunnelerer ikke, læs mere",
- "siteConfirmCopy": "Jeg har kopieret konfigurationen",
- "searchSitesProgress": "Søger i sites...",
- "siteAdd": "Tilføj site",
- "sitesTableViewPublicResources": "Vis offentlige ressourcer",
- "sitesTableViewPrivateResources": "Vis private ressourcer",
- "siteInstallNewt": "Installer Newt",
- "siteInstallNewtDescription": "Få Newt til at køre på dit system",
- "siteInstallKubernetesDocsDescription": "For mere og opdateret information om Kubernetes-installation, se docs.pangolin.net/manage/sites/install-kubernetes.",
- "siteInstallAdvantechDocsDescription": "For installationsvejledning til Advantech-modemmer, se docs.pangolin.net/manage/sites/install-advantech.",
- "WgConfiguration": "WireGuard Konfiguration",
- "WgConfigurationDescription": "Brug følgende konfiguration til at oprette forbindelse til netværket.",
- "operatingSystem": "Operativsystem",
- "commands": "Kommandoer",
- "recommended": "Anbefalet",
- "siteNewtDescription": "For den beste brugeroplevelsen, brug Newt. Den bruger WireGuard i bakgrunnen og lader dig adressere dine private ressourcer med deres LAN-adresse på dit private netværk fra Pangolin-dashbordet.",
- "siteRunsInDocker": "Kører i Docker",
- "siteRunsInShell": "Kører i shell på macOS, Linux og Windows",
- "siteErrorDelete": "Fejl ved sletning af sitet",
- "siteErrorUpdate": "Kunne ikke opdatere sitet",
- "siteErrorUpdateDescription": "En fejl opstod under opdatering af sitet.",
- "siteUpdated": "Site opdateret",
- "siteUpdatedDescription": "Sitet er blevet opdateret.",
- "siteGeneralDescription": "Konfigurer de generelle indstillinger for dette site",
- "siteRestartTitle": "Genstart Site",
- "siteRestartDescription": "Genstart WireGuard-tunnelen for dette site. Dette vil kortvarigt afbryde forbindelsen.",
- "siteRestartBody": "Brug dette, hvis site-tunnelen ikke fungerer korrekt, og du vil tvinge en genforbindelse uden at genstarte værten.",
- "siteRestartButton": "Genstart Site",
- "siteRestartDialogMessage": "Er du sikker på, at du vil genstarte WireGuard-tunnelen for {name}? Sitet vil kortvarigt miste forbindelsen.",
- "siteRestartWarning": "Sitet vil kortvarigt afbryde forbindelse, mens tunnelen genstarter.",
- "siteRestarted": "Site genstartet",
- "siteRestartedDescription": "WireGuard-tunnelen er blevet genstartet.",
- "siteErrorRestart": "Kunne ikke genstarte site",
- "siteErrorRestartDescription": "En fejl opstod, mens sitet blev genstartet.",
- "siteSettingDescription": "Konfigurer indstillingerne for sitet",
- "siteResourcesTab": "Ressourcer",
- "siteResourcesNoneOnSite": "Dette site har endnu ingen offentlige eller private ressourcer.",
- "siteResourcesSectionPublic": "Offentlige ressourcer",
- "siteResourcesSectionPrivate": "Private ressourcer",
- "siteResourcesSectionPublicDescription": "Ressourcer eksponert eksternt gennem domæner eller porte.",
- "siteResourcesSectionPrivateDescription": "Ressourcer, der er tilgængelige på dit private netværk gennem sitet.",
- "siteResourcesViewAllPublic": "Vis alle ressourcer",
- "siteResourcesViewAllPrivate": "Vis alle ressourcer",
- "siteResourcesDialogDescription": "Oversigt over offentlige og private ressourcer tilknyttet dette site.",
- "siteResourcesShowMore": "Vis mere",
- "siteResourcesPermissionDenied": "Du har ikke tilladelse til at liste disse ressourcer.",
- "siteResourcesEmptyPublic": "Ingen offentlige ressourcer peger på dette site endnu.",
- "siteResourcesEmptyPrivate": "Ingen private ressourcer er tilknyttet dette site endnu.",
- "siteResourcesHowToAccess": "Hvordan få adgang",
- "siteResourcesTargetsOnSite": "Mål på dette site",
- "siteSetting": "{siteName} Indstillinger",
- "siteNewtTunnel": "Newt-site (anbefalet)",
- "siteNewtTunnelDescription": "Lekkeste måte at oprette et indgangspunkt til ethvert netværk. Ingen ekstra opsætning på.",
- "siteWg": "Grunnleggende WireGuard",
- "siteWgDescription": "Brug en hvilken som helst WireGuard-klient til at etablere en tunnel. Manuel NAT-opsætning er påkrævet.",
- "siteWgDescriptionSaas": "Brug en hvilken som helst WireGuard-klient til at etablere en tunnel. Manuel NAT-opsætning er påkrævet. VIRKER KUN PÅ SELVHOSTEDE NODER",
- "siteLocalDescription": "Kun lokale ressourcer. Ingen tunnelering.",
- "siteLocalDescriptionSaas": "Lokale ressourcer. Ingen tunnelering. Kun tilgængelig på eksterne noder.",
- "siteSeeAll": "Se alle sites",
- "siteTunnelDescription": "Afgør, hvordan du vil oprette forbindelse til sitet",
- "siteNewtCredentials": "Legitimationsoplysninger",
- "siteNewtCredentialsDescription": "Sådan godkender sitet sig mod serveren",
- "remoteNodeCredentialsDescription": "Sådan godkender den eksterne node sig mod serveren",
- "siteCredentialsSave": "Gem brugeroplysninger",
- "siteCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for at kopiere det til et sikkert sted.",
- "siteInfo": "Områdeinformation",
- "status": "Status",
- "shareTitle": "Administrer delbare links",
- "shareDescription": "Opret delbare links for at give midlertidig eller permanent adgang til proxyressourcer",
- "shareSearch": "Søg delbare links...",
- "shareCreate": "Opret delbar link",
- "shareErrorDelete": "Kunne ikke slette linket",
- "shareErrorDeleteMessage": "En fejl opstod ved sletning af link",
- "shareDeleted": "Link slettet",
- "shareDeletedDescription": "Linket er blevet slettet",
- "shareDelete": "Slet delbar link",
- "shareDeleteConfirm": "Bekræft sletning af delbar link",
- "shareQuestionRemove": "Er du sikker på, at du vil slette dette delingslink?",
- "shareMessageRemove": "Når linket er slettet, vil det ikke længere fungere, og alle, der bruger det, mister adgang til ressourcen.",
- "shareTokenDescription": "Adgangstoken kan sendes på to måter: som en queryparameter eller i request-headers. Disse skal sendes fra klienten på hver forespørgsel om autentificeret adgang.",
- "accessToken": "Adgangstoken",
- "usageExamples": "Brukseksempler",
- "tokenId": "Token-ID",
- "requestHeades": "Request headers",
- "queryParameter": "Forespørgselsparametre",
- "importantNote": "Vigtig bemærkning",
- "shareImportantDescription": "Af sikkerhedshensyn anbefales det at bruge headers frem for queryparametre der det er muligt, da queryparametre kan loges i serverlogs eller browserhistorik.",
- "token": "Token",
- "shareTokenSecurety": "Hold adgangstoken sikker. Ikke del den i offentlig tilgængelige sites eller klientsidekode.",
- "shareErrorFetchResource": "Kunne ikke hente ressourcer",
- "shareErrorFetchResourceDescription": "En fejl opstod under hentning af ressourcerne",
- "shareErrorCreate": "Mislykkedes med at oprette delingslenke",
- "shareErrorCreateDescription": "Det opstod en fejl ved oprettelse af delingslinket",
- "shareCreateDescription": "Alle med denne linket får adgang til ressourcen",
- "shareTitleOptional": "Titel (valgfrit)",
- "sharePathOptional": "Sti (valgfrit)",
- "sharePathDescription": "Linket vil videresende brugere til denne stien efter autentificering.",
- "expireIn": "Udløber om",
- "neverExpire": "Udløber aldrig",
- "shareExpireDescription": "Udløbstid er hvor længe linket vil være brukbar og give adgang til ressourcen. Efter denne tiden vil linket ikke længere fungere, og brugere som brugte denne linket vil miste adgangen til ressourcen.",
- "shareSeeOnce": "Du vil kun kunne se dette link én gang. Sørg for at kopiere det.",
- "shareAccessHint": "Alle med denne linket kan få adgang til ressourcen. Del forsiktig.",
- "shareTokenUsage": "Se brug af adgangstoken",
- "createLink": "Opret link",
- "resourcesNotFound": "Ingen ressourcer fundet",
- "resourceSearch": "Søg i ressourcer",
- "machineSearch": "Søg efter maskiner",
- "machinesSearch": "Søg efter maskinklienter...",
- "machineNotFound": "Ingen maskiner fundet",
- "userDeviceSearch": "Søg efter brugerenheder",
- "userDevicesSearch": "Søg efter brugerenheder...",
- "openMenu": "Åbn menu",
- "resource": "Ressource",
- "title": "Titel",
- "created": "Oprettet",
- "expires": "Udløber",
- "never": "Aldrig",
- "shareErrorSelectResource": "Vælg venligst en ressource",
- "proxyResourceTitle": "Administrer offentlige ressourcer",
- "proxyResourceDescription": "Opret og administrer ressourcer som er offentlig tilgængelige via en browser",
- "publicResourcesBannerTitle": "Web-baseret offentlig adgang",
- "publicResourcesBannerDescription": "Offentlige ressourcer er HTTPS-proxyer som er tilgængelige for alle på internettet via en browser. I modsætning til private ressourcer, kræver de ikke klientsoftware og kan inkludere identitets- og kontekstsensitive adgangspolitikker.",
- "clientResourceTitle": "Administrer private ressourcer",
- "clientResourceDescription": "Oprette og administrer ressourcer som kun er tilgængelige via en tilsluttet klient",
- "privateResourcesBannerTitle": "Zero-Trust privat adgang",
- "privateResourcesBannerDescription": "Private ressourcer bruger Zero-Trust-sikkerhed og sikrer, at brugere og maskiner kun kan få adgang til ressourcer, du eksplicit giver tilladelse til. Tilslut brugerenheder eller maskinklienter for at få adgang til disse ressourcer via et sikkert virtuelt privat netværk.",
- "resourcesSearch": "Søg i ressourcer...",
- "resourceAdd": "Tilføj ressource",
- "resourceErrorDelte": "Fejl ved sletning af ressource",
- "resourcePoliciesBannerTitle": "Genbrug autentificering og adgangsregler",
- "resourcePoliciesBannerDescription": "Delte ressourcepolitikker lader dig definere autentificeringsmetoder og adgangsregler én gang og derefter knytte dem til flere offentlige ressourcer. Når du opdaterer en politik, arver alle tilknyttede ressourcer automatisk ændringen.",
- "resourcePoliciesBannerButtonText": "Læs mere",
- "resourcePoliciesTitle": "Administrer offentlige ressourcepolitikker",
- "resourcePoliciesAttachedResourcesColumnTitle": "Ressourcer",
- "resourcePoliciesAttachedResources": "{count} ressource(er)",
- "resourcePoliciesAttachedResourcesCount": "{count, plural, one {# ressource} other {# ressourcer}}",
- "resourcePoliciesAttachedResourcesEmpty": "ingen ressourcer",
- "resourcePoliciesDescription": "Opret og administrer autentiseringsretningslinjer for at kontrollere adgang til dine offentlige ressourcer",
- "resourcePoliciesSearch": "Søg efter regler...",
- "resourcePoliciesAdd": "Tilføj politik",
- "resourcePoliciesDefaultBadgeText": "Standardpolitik",
- "resourcePoliciesCreate": "Opret offentlig ressourcepolitik",
- "resourcePoliciesCreateDescription": "Følg trinene nedenfor for at oprette en ny politik",
- "resourcePolicyName": "Politiknavn",
- "resourcePolicyNameDescription": "Give denne politiknavnet for at identifisere den på tværs af dine ressourcer",
- "resourcePolicyNamePlaceholder": "f.eks. Intern Adgangspolitik",
- "resourcePoliciesSeeAll": "Se Alle Politikker",
- "resourcePolicyAuthMethodAdd": "Tilføj Autentificeringsmetode",
- "resourcePolicyOtpEmailAdd": "Tilføj OTP-e-mails",
- "resourcePolicyRulesAdd": "Tilføj Regler",
- "resourcePolicyAuthMethodsDescription": "Tillad adgang til ressourcer via yderligere autentificeringsmetoder",
- "resourcePolicyUsersRolesDescription": "Konfigurer hvilke brugere og roller, der kan besøge tilknyttede ressourcer",
- "rulesResourcePolicyDescription": "Konfigurer regler for at kontrollere adgangen til ressourcer som er knyttet til denne politikken",
- "authentication": "Autentificering",
- "protected": "Beskyttet",
- "notProtected": "Ikke beskyttet",
- "resourceMessageRemove": "Når den er fjernet, vil ressourcen ikke længere være tilgængeligt. Alle mål knyttet til ressourcen vil også blive fjernet.",
- "resourceQuestionRemove": "Er du sikker på at du vil fjerne ressourcen fra organisationen?",
- "resourcePolicyMessageRemove": "Når ressourcepolitikken er fjernet, vil den ikke længere være tilgængelig. Alle tilknyttede ressourcer vil blive frakoblet og stå uden autentificering.",
- "resourcePolicyQuestionRemove": "Er du sikker på at du vil fjerne ressourcepolitikken fra organisationen?",
- "resourceHTTP": "HTTPS-ressource",
- "resourceHTTPDescription": "Proxyforespørgsler over HTTPS ved at bruge et fuldt kvalificeret domænenavn.",
- "resourceRaw": "Rå TCP/UDP-ressource",
- "resourceRawDescription": "Proxyforespørgsler over rå TCP/UDP ved at bruge et portnummer.",
- "resourceRawDescriptionCloud": "Proxyforespørgsler over rå TCP/UDP ved hjælp af et portnummer. Kræver, at sites opretter forbindelse til en ekstern node.",
- "resourceCreate": "Opret ressource",
- "resourceCreateDescription": "Følg trinene nedenfor for at oprette en ny ressource",
- "resourcePublicCreate": "Opret offentlig ressource",
- "resourcePublicCreateDescription": "Følg nedenstående trin for at oprette en ny offentlig ressource, der er tilgængelig via en webbrowser",
- "resourceCreateGeneralDescription": "Konfigurer de grunnleggende ressourceindstillingerne inklusive navnet og typen",
- "resourceSeeAll": "Se alle ressourcer",
- "resourceCreateGeneral": "Generelt",
- "resourceNameDescription": "Dette er visningsnavnet for ressourcen.",
- "siteSelect": "Vælg site",
- "siteSearch": "Søg i site",
- "siteNotFound": "Ingen sites fundet.",
- "selectCountry": "Vælg land",
- "searchCountries": "Søg land...",
- "noCountryFound": "Ingen land fundet.",
- "siteSelectionDescription": "Dette site vil give forbindelse til målet.",
- "resourceType": "Ressourcetype",
- "resourceTypeDescription": "Dette styrer ressourceprotokolen, og hvordan den vises i browseren. Dette kan ikke ændres senere.",
- "resourceDomainDescription": "Ressourcen vil blive serveret på dette fuldstændigt kvalificerede domænenavnet.",
- "resourceHTTPSSettings": "HTTPS-indstillinger",
- "resourceHTTPSSettingsDescription": "Konfigurer hvordan ressourcen skal nås over HTTPS",
- "resourcePortDescription": "Den eksterne port på Pangolin-instansen eller noden, hvor ressourcen vil være tilgængelig.",
- "domainType": "Domænetype",
- "subdomain": "Underdomæne",
- "baseDomain": "Basisdomæne",
- "configure": "Konfigurer",
- "subdomnainDescription": "Underdomænet hvor ressourcen vil være tilgængeligt.",
- "resourceRawSettings": "TCP/UDP-indstillinger",
- "resourceRawSettingsDescription": "Konfigurer hvordan ressourcen vil blive tilgængelig over TCP/UDP",
- "protocol": "Protokol",
- "protocolSelect": "Vælg en protokol",
- "resourcePortNumber": "Portnummer",
- "resourcePortNumberDescription": "Det eksterne portnummeret for proxyforespørgsler.",
- "back": "Tilbage",
- "cancel": "Annuller",
- "resourceConfig": "Konfigurationsuddrag",
- "resourceConfigDescription": "Kopiér og indsæt disse konfigurationsuddrag for at opsætte TCP/UDP-ressourcen.",
- "resourceAddEntrypoints": "Traefik: Tilføj indgangspunkter",
- "resourceExposePorts": "Gerbil: Eksponer Porte i Docker Compose",
- "resourceLearnRaw": "Lær hvordan at konfigurere TCP/UDP-ressourcer",
- "resourceBack": "Tilbage til ressourcer",
- "resourceGoTo": "Gå til ressource",
- "resourcePolicyDelete": "Slet Ressourcepolitik",
- "resourcePolicyDeleteConfirm": "Bekræft sletning af ressourcepolitik",
- "resourceDelete": "Slet ressource",
- "resourceDeleteConfirm": "Bekræft sletning af ressource",
- "labelDelete": "Slet etikett",
- "labelAdd": "Tilføj etikett",
- "labelCreateSuccessMessage": "Etikett oprettet med succes",
- "labelDuplicateError": "Dupliker etikett",
- "labelDuplicateErrorDescription": "En etikett med dette navnet findes allerede.",
- "labelEditSuccessMessage": "Etikett ændret med succes",
- "labelNameField": "Etikettnavn",
- "labelColorField": "Etikettfarge",
- "labelPlaceholder": "Eksempel: homelab",
- "labelCreate": "Opret etikett",
- "createLabelDialogTitle": "Opret etikett",
- "createLabelDialogDescription": "Opret en ny etikett, der kan knyttes til denne organisation",
- "labelEdit": "Rediger etikett",
- "editLabelDialogTitle": "Opdater etikett",
- "editLabelDialogDescription": "Rediger en ny etikett, der kan knyttes til denne organisation",
- "labelDeleteConfirm": "Bekræft sletning af etikett",
- "labelErrorDelete": "Kunne ikke slette etiket",
- "labelMessageRemove": "Denne handling er permanent. Alle sites, ressourcer og klienter, der er taget med denne etiket, får fjernet etiketten.",
- "labelQuestionRemove": "Er du sikker på at du vil fjerne etiketten fra organisationen?",
- "visibility": "Synlighed",
- "enabled": "Aktiveret",
- "disabled": "Deaktiveret",
- "general": "Generelt",
- "generalSettings": "Generelle indstillinger",
- "proxy": "Proxy",
- "internal": "Intern",
- "rules": "Regler",
- "resourceSettingDescription": "Konfigurere indstillingerne på ressourcen",
- "resourceSetting": "{resourceName} Indstillinger",
- "resourcePolicySettingDescription": "Konfigurer indstillingerne for denne offentlige ressourcepolitik",
- "resourcePolicySetting": "{policyName} Indstillinger",
- "alwaysAllow": "Omgå autentificering",
- "alwaysDeny": "Bloker adgang",
- "passToAuth": "Send til autentificering",
- "orgSettingsDescription": "Konfigurere organisationens indstillinger",
- "orgGeneralSettings": "Organisationsindstillinger",
- "orgGeneralSettingsDescription": "Administrer organisationens detaljer og konfiguration",
- "saveGeneralSettings": "Gem generelle indstillinger",
- "saveSettings": "Gem indstillinger",
- "orgDangerZone": "Farezone",
- "orgDangerZoneDescription": "Når du sletter denne organisation, er der ingen vej tilbage. Vær helt sikker.",
- "orgDelete": "Slet organisation",
- "orgDeleteConfirm": "Bekræft Sletning af Organisation",
- "orgMessageRemove": "Denne handling er irreversibel og vil slette alle tilknyttede data.",
- "orgMessageConfirm": "For at bekræfte, indtast venligst navnet på organisationen nedenfor.",
- "orgQuestionRemove": "Er du sikker på at du vil fjerne organisationen?",
- "orgUpdated": "Organisation opdateret",
- "orgUpdatedDescription": "Organisationen er blevet opdateret.",
- "orgErrorUpdate": "Kunne ikke opdatere organisationen",
- "orgErrorUpdateMessage": "En fejl opstod under opdatering af organisationen.",
- "orgErrorFetch": "Kunne ikke hente organisationer",
- "orgErrorFetchMessage": "Det opstod en fejl under visning af organisationerne dine",
- "orgErrorDelete": "Kunne ikke slette organisation",
- "orgErrorDeleteMessage": "Det opstod en fejl under sletning af organisationen.",
- "orgDeleted": "Organisation slettet",
- "orgDeletedMessage": "Organisationen og tilhørende data er slettet.",
- "deleteAccount": "Slet konto",
- "deleteAccountDescription": "Slet din konto permanent, alle organisationer du ejer, og alle data i disse organisationer. Dette kan ikke fortrydes.",
- "deleteAccountButton": "Slet konto",
- "deleteAccountConfirmTitle": "Slet konto",
- "deleteAccountConfirmMessage": "Dette vil slette din konto, alle organisationer du ejer og alle data i disse organisationer. Dette kan ikke fortrydes.",
- "deleteAccountConfirmString": "Slet konto",
- "deleteAccountSuccess": "Kontoen er slettet",
- "deleteAccountSuccessMessage": "Din konto er slettet.",
- "deleteAccountError": "Kunne ikke slette konto",
- "deleteAccountPreviewAccount": "Din konto",
- "deleteAccountPreviewOrgs": "Organisationer du ejer (og alle deres data)",
- "orgMissing": "Organisations-ID Mangler",
- "orgMissingMessage": "Kan ikke regenerere invitation uden en organisations-ID.",
- "accessUsersManage": "Administrer brugere",
- "accessUserManage": "Administrer brugere",
- "accessUsersDescription": "Inviter og administrer brugere med adgang til denne organisation",
- "accessUsersSearch": "Søg efter brugere...",
- "accessUsersRoleFilterCount": "{count, plural, one {# rolle} other {# roller}}",
- "accessUsersRoleFilterClear": "Fjern rollesøgefiltre",
- "accessUserCreate": "Opret bruger",
- "accessUserRemove": "Fjern bruger",
- "username": "Brugernavn",
- "identityProvider": "Identitetsudbyder",
- "role": "Rolle",
- "nameRequired": "Navn er påkrævet",
- "accessRolesManage": "Administrer Roller",
- "accessRolesDescription": "Opret og administrer roller for brugere i organisationen",
- "accessRolesSearch": "Søg efter roller...",
- "accessRolesAdd": "Tilføj rolle",
- "accessRoleDelete": "Slet rolle",
- "accessApprovalsManage": "Administrer godkendelser",
- "accessApprovalsDescription": "Se og administrer ventende godkendelser for adgang til denne organisation",
- "description": "Beskrivelse",
- "inviteTitle": "Åbne invitationer",
- "inviteDescription": "Administrer invitationer til andre brugere for at blive med i organisationen",
- "inviteSearch": "Søg i invitationer...",
- "minutes": "Minutter",
- "hours": "Timer",
- "days": "Dage",
- "weeks": "Uger",
- "months": "Måneder",
- "years": "År",
- "day": "{count, plural, one {én dag} other {# dage}}",
- "apiKeysTitle": "API-nøgleinformation",
- "apiKeysConfirmCopy2": "Du skal bekræfte at du har kopieret API-nøglen.",
- "apiKeysErrorCreate": "Fejl ved oprettelse af API-nøgle",
- "apiKeysErrorSetPermission": "Fejl ved indstilling af tilladelser",
- "apiKeysCreate": "Generer API-nøgle",
- "apiKeysCreateDescription": "Generer en ny API-nøgle for organisationen",
- "apiKeysGeneralSettings": "Tilladelser",
- "apiKeysGeneralSettingsDescription": "Find ud af, hvad denne API-nøgle kan gøre",
- "apiKeysList": "Ny API-nøgle",
- "apiKeysSave": "Gem API-nøgle",
- "apiKeysSaveDescription": "Du vil kun kunne se dette én gang. Sørg for at kopiere det til et sikkert sted.",
- "apiKeysInfo": "API-nøglen er:",
- "apiKeysConfirmCopy": "Jeg har kopieret API-nøglen",
- "generate": "Generér",
- "done": "Færdig",
- "apiKeysSeeAll": "Se alle API-nøgler",
- "apiKeysPermissionsErrorLoadingActions": "Fejl ved indlæsning af API-nøglehandlinger",
- "apiKeysPermissionsErrorUpdate": "Fejl ved indstilling af tilladelser",
- "apiKeysPermissionsUpdated": "Tilladelser opdateret",
- "apiKeysPermissionsUpdatedDescription": "Tilladelserne er blevet opdateret.",
- "apiKeysPermissionsGeneralSettings": "Tilladelser",
- "apiKeysPermissionsGeneralSettingsDescription": "Bestem, hvad denne API-nøgle kan gøre",
- "apiKeysPermissionsSave": "Gem tilladelser",
- "apiKeysPermissionsTitle": "Tilladelser",
- "apiKeys": "API-nøgler",
- "searchApiKeys": "Søg API-nøgler",
- "apiKeysAdd": "Generer API-nøgle",
- "apiKeysErrorDelete": "Fejl under sletning af API-nøgle",
- "apiKeysErrorDeleteMessage": "Fejl ved sletning af API-nøgle",
- "apiKeysQuestionRemove": "Er du sikker på at du vil fjerne API-nøglen fra organisationen?",
- "apiKeysMessageRemove": "Når den er fjernet, vil API-nøglen ikke længere kunne bruges.",
- "apiKeysDeleteConfirm": "Bekræft sletning af API-nøgle",
- "apiKeysDelete": "Slet API-nøgle",
- "apiKeysManage": "Administrer API-nøgler",
- "apiKeysDescription": "API-nøgler bruges for at autentificere med integrasjons-API",
- "provisioningKeysTitle": "Provisioneringsnøgle",
- "provisioningKeysManage": "Administrer provisioneringsnøgler",
- "provisioningKeysDescription": "Provisioneringsnøgler bruges til at godkende automatiseret site-provisionering for din organisation.",
- "provisioningManage": "Provisionering",
- "provisioningDescription": "Administrer provisioneringsnøgler og gennemgå ventende sites som venter på godkendelse.",
- "pendingSites": "Ventende sites",
- "siteApproveSuccess": "Med succes godkendelse af site",
- "siteApproveError": "Fejl ved godkendelse af side",
- "provisioningKeys": "Provisioneringsnøgler",
- "searchProvisioningKeys": "Søg varer i provisioneringsnøgler...",
- "provisioningKeysAdd": "Generer provisioneringsnøgle",
- "provisioningKeysErrorDelete": "Fejl under sletning af provisioneringsnøgle",
- "provisioningKeysErrorDeleteMessage": "Fejl under sletning af provisioneringsnøgle",
- "provisioningKeysQuestionRemove": "Er du sikker på at du vil fjerne denne midlertidig nøglen fra organisationen?",
- "provisioningKeysMessageRemove": "Når nøglen er fjernet, kan den ikke længere bruges til site-provisionering.",
- "provisioningKeysDeleteConfirm": "Bekræft sletning af provisioneringsnøgle",
- "provisioningKeysDelete": "Slet provisioneringsnøgle",
- "provisioningKeysCreate": "Generer provisioneringsnøgle",
- "provisioningKeysCreateDescription": "Generer en ny provisioneringsnøgle til organisationen",
- "provisioningKeysSeeAll": "Se alle provisioneringsnøgler",
- "provisioningKeysSave": "Gem den midlertidig nøglen",
- "provisioningKeysSaveDescription": "Du kan kun se denne én gang. Kopiér det til et sikkert sted.",
- "provisioningKeysErrorCreate": "Fejl under oprettelse af provisioneringsnøgle",
- "provisioningKeysList": "Ny provisioneringsnøgle",
- "provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
- "provisioningKeysUnlimitedBatchSize": "Ubegrænset mengde bunt (ingen begrænsning)",
- "provisioningKeysMaxBatchUnlimited": "Ubegrænset",
- "provisioningKeysMaxBatchSizeInvalid": "Angiv en gyldig sjakkstørrelse (1–1 000.000).",
- "provisioningKeysValidUntil": "Gyldig til",
- "provisioningKeysValidUntilHint": "Lad stå tomt for ingen udløb.",
- "provisioningKeysValidUntilInvalid": "Angiv en gyldig dato og et gyldigt klokkeslæt.",
- "provisioningKeysNumUsed": "Antal ganger brugt",
- "provisioningKeysLastUsed": "Sidst brugt",
- "provisioningKeysNoExpiry": "Ingen udløbsdato",
- "provisioningKeysNeverUsed": "Aldrig",
- "provisioningKeysEdit": "Rediger provisioneringsnøgle",
- "provisioningKeysEditDescription": "Opdater maksimal størrelse for bunt og udløbstid for denne nøglen.",
- "provisioningKeysApproveNewSites": "Godkend nye sites",
- "provisioningKeysApproveNewSitesDescription": "Godkend automatisk sites som registrerer dig med denne nøglen.",
- "provisioningKeysUpdateError": "Fejl under opdatering af provisioneringsnøgle",
- "provisioningKeysUpdated": "Foreslå nøgle opdateret",
- "provisioningKeysUpdatedDescription": "Dine ændringer er gemt.",
- "provisioningKeysBannerTitle": "Provisioneringsnøgler til sites",
- "provisioningKeysBannerDescription": "Generér en provisioneringsnøgle, og brug den med Newt-connectoren til automatisk oprettelse af sites ved første opstart — uden behov for separate legitimationsoplysninger til hvert site.",
- "provisioningKeysBannerButtonText": "Læs mere",
- "pendingSitesBannerTitle": "Ventende sites",
- "pendingSitesBannerDescription": "Sites som opretter forbindelse ved brug af en provisioneringsnøgle vises her for vurdering.",
- "pendingSitesBannerButtonText": "Læs mere",
- "apiKeysSettings": "{apiKeyName} Indstillinger",
- "userTitle": "Administrer alle brugere",
- "userDescription": "Vis og administrer alle brugere i systemet",
- "userAbount": "Om brugeradministration",
- "userAbountDescription": "Denne tabel viser alle basisbrugerobjekter i systemet. Hver bruger kan tilhøre flere organisationer. Når du fjerner en bruger fra en organisation, slettes brugerens basisbrugerobjekt ikke – brugeren forbliver i systemet. For at fjerne en bruger helt fra systemet skal du slette basisbrugerobjektet med slet-handlingen i denne tabel.",
- "userServer": "Serverbrugere",
- "userSearch": "Søg serverbrugere...",
- "userErrorDelete": "Fejl ved sletning af bruger",
- "userDeleteConfirm": "Bekræft sletning af bruger",
- "userDeleteServer": "Slet bruger fra server",
- "userMessageRemove": "Brugeren vil blive fjernet fra alle organisationer og vil blive fuldstændigt fjernet fra serveren.",
- "userQuestionRemove": "Er du sikker på at du vil slette brugeren permanent fra serveren?",
- "licenseKey": "Licensnøgle",
- "valid": "Gyldig",
- "numberOfSites": "Antal sites",
- "licenseKeySearch": "Søg licensnøgler...",
- "licenseKeyAdd": "Tilføj licensnøgle",
- "type": "Type",
- "licenseKeyRequired": "Licensnøgle er påkrævet",
- "licenseTermsAgree": "Du skal acceptere licensvilkårene",
- "licenseErrorKeyLoad": "Fejl ved indlæsning af licensnøgler",
- "licenseErrorKeyLoadDescription": "Det opstod en fejl ved indlæsning af licensnøgler.",
- "licenseErrorKeyDelete": "Kunne ikke slette licensnøgle",
- "licenseErrorKeyDeleteDescription": "Det opstod en fejl ved sletning af licensnøgle.",
- "licenseKeyDeleted": "Licensnøgle slettet",
- "licenseKeyDeletedDescription": "Licensnøglen er blevet slettet.",
- "licenseErrorKeyActivate": "Aktivering af licensnøgle mislykkedes",
- "licenseErrorKeyActivateDescription": "Det opstod en fejl under aktivering af licensnøglen.",
- "licenseAbout": "Om Licensering",
- "licenseBannerTitle": "Aktivér din enterprise-licens",
- "licenseBannerDescription": "Lås op for virksomhedsfunktioner på din selvhostede Pangolin-instans. Køb en licensnøgle for at aktivere premium-funktioner, og indtast den nedenfor.",
- "licenseBannerGetLicense": "Få en licens",
- "licenseBannerViewDocs": "Vis dokumentation",
- "communityEdition": "Community-udgave",
- "licenseAboutDescription": "Dette er for virksomheds- og enterprise-brugere som bruger Pangolin i et kommercielt miljø. Hvis du bruger Pangolin til personlig brug, kan du ignorere denne sektionen.",
- "licenseKeyActivated": "Licensnøgle aktiveret",
- "licenseKeyActivatedDescription": "Licensnøglen er blevet aktiveret.",
- "licenseErrorKeyRecheck": "En fejl opstod under bekræftelse af licensnøgler",
- "licenseErrorKeyRecheckDescription": "Det opstod en fejl under bekræftelse af licensnøgler.",
- "licenseErrorKeyRechecked": "Licensnøgler verificeret",
- "licenseErrorKeyRecheckedDescription": "Alle licensnøgler er verificeret",
- "licenseActivateKey": "Aktivér licensnøgle",
- "licenseActivateKeyDescription": "Indtast en licensnøgle for at aktivere den.",
- "licenseActivate": "Aktivér licens",
- "licenseAgreement": "Ved at markere dette felt bekræfter du, at du har læst og accepterer de licensvilkår, der svarer til niveauet knyttet til din licensnøgle.",
- "fossorialLicense": "Vis Fossorial kommerciel licens og abonnementsvilkår",
- "licenseMessageRemove": "Dette vil fjerne licensnøglen og alle tilknyttede tilladelser givet af den.",
- "licenseMessageConfirm": "For at bekræfte, indtast venligst licensnøglen nedenfor.",
- "licenseQuestionRemove": "Er du sikker på at du vil slette licensnøglen?",
- "licenseKeyDelete": "Slet Licensnøgle",
- "licenseKeyDeleteConfirm": "Bekræft sletning af licensnøgle",
- "licenseTitle": "Administrer licensstatus",
- "licenseTitleDescription": "Se og administrer licensnøgler i systemet",
- "licenseHost": "Hostlicens",
- "licenseHostDescription": "Administrer hovedlicensnøglen for verten.",
- "licensedNot": "Ikke licenseret",
- "hostId": "Host-ID",
- "licenseReckeckAll": "Bekræft alle nøgler",
- "licenseSiteUsage": "Site Brug",
- "licenseSiteUsageDecsription": "Vis antal sites som bruger denne licensen.",
- "licenseNoSiteLimit": "Der er ingen grænse for antal sites, der bruger en ulicenseret host.",
- "licensePurchase": "Køb licens",
- "licensePurchaseSites": "Køb flere sites",
- "licenseSitesUsedMax": "{usedSites} af {maxSites} sites brugt",
- "licenseSitesUsed": "{count, plural, =0 {ingen sites} one {ét site} other {# sites}} i systemet.",
- "licensePurchaseDescription": "Vælg hvor mange sites du vil {selectedMode, select, license {købe en licens for. Du kan altid tilføje flere sites senere.} other {tilføje din eksisterende licens.}}",
- "licenseFee": "Licensafgift",
- "licensePriceSite": "Pris per site",
- "total": "I alt",
- "licenseContinuePayment": "Fortsæt til betaling",
- "pricingPage": "Prisoversigt",
- "pricingPortal": "Se Købsportal",
- "licensePricingPage": "For de mest opdaterede priser og rabatterne, venligst besøg",
- "invite": "Invitationer",
- "inviteRegenerate": "Regenerer invitationen",
- "inviteRegenerateDescription": "Tilbagekald tidligere invitation og oprette en ny",
- "inviteRemove": "Fjern invitation",
- "inviteRemoveError": "Mislykkedes at fjerne invitation",
- "inviteRemoveErrorDescription": "Det opstod en fejl under fjernelse af invitationen.",
- "inviteRemoved": "Invitation fjernet",
- "inviteRemovedDescription": "Invitationen for {email} er fjernet.",
- "inviteQuestionRemove": "Er du sikker på at du vil fjerne invitationen?",
- "inviteMessageRemove": "Når fjernet, vil denne invitationen ikke længere være gyldig. Du kan altid invitere brugeren igen senere.",
- "inviteMessageConfirm": "For at bekræfte, indtast venligst invitationens e-mailadresse nedenfor.",
- "inviteQuestionRegenerate": "Er du sikker på at du vil generere invitationen igen for {email}? Dette vil ugyldiggøre den forrige invitation.",
- "inviteRemoveConfirm": "Bekræft fjernelse af invitation",
- "inviteRegenerated": "Invitation fornyet",
- "inviteSent": "En ny invitation er sendt til {email}.",
- "inviteSentEmail": "Send e-mailnotifikation til brugeren",
- "inviteGenerate": "En ny invitation er generert for {email}.",
- "inviteDuplicateError": "Dupliker invitation",
- "inviteDuplicateErrorDescription": "En invitation for denne bruger findes allerede.",
- "inviteRateLimitError": "Anmodningsgrænse overskredet",
- "inviteRateLimitErrorDescription": "Du har overskredet grænsen på 3 regenereringer pr. time. Prøv igen senere.",
- "inviteRegenerateError": "Kunne ikke regenerere invitation",
- "inviteRegenerateErrorDescription": "Det opstod en fejl under regenerering af invitationen.",
- "inviteValidityPeriod": "Gyldighedsperiode",
- "inviteValidityPeriodSelect": "Vælg gyldighetsperiode",
- "inviteRegenerateMessage": "Invitationen er generert igen. Brugeren skal gå til linket nedenfor for at acceptere invitationen.",
- "inviteRegenerateButton": "Regenerer",
- "expiresAt": "Udløbstidspunkt",
- "accessRoleUnknown": "Ukendt rolle",
- "placeholder": "Pladsholder",
- "userErrorOrgRemove": "En fejl opstod under fjernelse af bruger",
- "userErrorOrgRemoveDescription": "Det opstod en fejl under fjernelse af brugeren.",
- "userOrgRemoved": "Bruger fjernet",
- "userOrgRemovedDescription": "Brugeren {email} er fjernet fra organisationen.",
- "userQuestionOrgRemove": "Er du sikker på at du vil fjerne denne bruger fra organisationen?",
- "userMessageOrgRemove": "Når denne bruger er fjernet, vil de ikke længere have adgang til organisationen. Du kan altid invitere dem igen senere, men de vil skulle acceptere invitationen igen.",
- "userRemoveOrgConfirm": "Bekræft fjernelse af bruger",
- "userRemoveOrg": "Fjern bruger fra organisation",
- "userQuestionOrgRemoveSelf": "Er du sikker på at du vil fjerne dig selv fra denne organisation?",
- "userMessageOrgRemoveSelf": "Du vil miste adgang umiddelbart. En administrator kan invitere dig tilbage senere, men du skal acceptere en ny invitation.",
- "userRemoveOrgConfirmSelf": "Bekræft fjernelse af mig selv",
- "userRemoveOrgSelf": "Fjern dig selv fra organisationen",
- "userRemoveOrgSelfWarning": "Du vil miste adgangen til denne organisation umiddelbart.",
- "userRemoveOrgConfirmPhraseSelf": "FJERN MIG SELV FRA ORG",
- "users": "Brugere",
- "accessRoleMember": "Medlem",
- "accessRoleOwner": "Ejer",
- "userConfirmed": "Bekræftet",
- "idpNameInternal": "Intern",
- "emailInvalid": "Ugyldig e-mailadresse",
- "inviteValidityDuration": "Vælg venligst en varighed",
- "accessRoleSelectPlease": "En bruger skal tilhøre mindst én rolle.",
- "accessRoleRequired": "Rolle påkrævet",
- "removeOwnAdminRoleConfirmTitle": "Fjern din administratoradgang?",
- "removeOwnAdminRoleConfirmDescription": "Du vil ikke længere have administratorrettigheder i denne organisation efter gemning. En anden administrator kan gendanne adgang hvis nødvendig.",
- "removeOwnAdminRoleConfirmButton": "Fjern min administratoradgang",
- "removeOwnAdminRoleConfirmPhrase": "FJERN MIN ADMINISTRATORADGANG",
- "ownerMustRetainAdminRole": "Organisationsejer skal beholde mindst én administratorrolle.",
- "usernameRequired": "Brugernavn er påkrævet",
- "idpSelectPlease": "Vælg venligst en identitetsudbyder",
- "idpGenericOidc": "Generisk OAuth2/OIDC-udbyder.",
- "accessRoleErrorFetch": "En fejl opstod under hentning af roller",
- "accessRoleErrorFetchDescription": "En fejl opstod under hentning af rollene",
- "idpErrorFetch": "En fejl opstod under hentning af identitetsudbydere",
- "idpErrorFetchDescription": "En fejl opstod ved hentning af identitetsudbydere",
- "userErrorExists": "Bruger findes allerede",
- "userErrorExistsDescription": "Denne bruger er allerede medlem af organisationen.",
- "inviteError": "Kunne ikke invitere bruger",
- "inviteErrorDescription": "En fejl opstod under invitering af brugeren",
- "userInvited": "Bruger inviteret",
- "userInvitedDescription": "Brugeren er inviteret.",
- "userErrorCreate": "Kunne ikke oprette bruger",
- "userErrorCreateDescription": "Det opstod en fejl under oprettelse af brugeren",
- "userCreated": "Bruger oprettet",
- "userCreatedDescription": "Brugeren er blevet oprettet.",
- "userTypeInternal": "Intern bruger",
- "userTypeInternalDescription": "Inviter en bruger til at blive med direkte i organisationen.",
- "userTypeExternal": "Ekstern bruger",
- "userTypeExternalDescription": "Opret en bruger med en ekstern identitetsudbyder.",
- "accessUserCreateDescription": "Følg stegene under for at oprette en ny bruger",
- "userSeeAll": "Se alle brugere",
- "userTypeTitle": "Brugertype",
- "userTypeDescription": "Bestem hvordan du vil oprette brugeren",
- "userSettings": "Brugerinformation",
- "userSettingsDescription": "Indtast detaljerne for den nye brugeren",
- "inviteEmailSent": "Send invitasjonsepost til bruger",
- "inviteValid": "Gyldig for",
- "selectDuration": "Vælg varighed",
- "selectResource": "Vælg ressource",
- "filterByResource": "Filtrer efter ressourcer",
- "selectApprovalState": "Vælg godkendelsesstatus",
- "filterByApprovalState": "Filtrer efter godkendelsesstatus",
- "approvalListEmpty": "Ingen godkendelser",
- "approvalState": "Godkendelses tilstand",
- "approvalLoadMore": "Indlæs mere",
- "loadingApprovals": "Indlæser godkendelser",
- "approve": "Godkend",
- "approved": "Godkendt",
- "denied": "Afviste",
- "deniedApproval": "Afvist godkendelse",
- "all": "Alle",
- "deny": "Afvis",
- "viewDetails": "Vis detaljer",
- "requestingNewDeviceApproval": "har anmodet om en ny enhed",
- "resetFilters": "Nulstil filtre",
- "totalBlocked": "Forespørgsler blokeret af Pangolin",
- "totalRequests": "Totalt antal forespørgsler",
- "requestsByCountry": "Forespørgsler fra land",
- "requestsByDay": "Forespørgsler per dag",
- "blocked": "Blokeret",
- "allowed": "Tilladt",
- "topCountries": "Flest land",
- "accessRoleSelect": "Vælg rolle",
- "inviteEmailSentDescription": "En e-mail er sendt til brugeren med adgangslinket nedenfor. De skal åbne linket for at acceptere invitationen.",
- "inviteSentDescription": "Brugeren er blevet inviteret. De skal åbne linket nedenfor for at acceptere invitationen.",
- "inviteExpiresIn": "Invitationen udløber om {days, plural, one {én dag} other {# dage}}.",
- "idpTitle": "Identitetsudbyder",
- "idpSelect": "Vælg identitetsudbyderen for den eksterne brugeren",
- "idpNotConfigured": "Ingen identitetsudbydere er konfigureret. Venligst konfigurer en identitetsudbyder før du opretter eksterne brugere.",
- "usernameUniq": "Dette skal matche det unikke brugernavn som findes i den valgte identitetsudbyder.",
- "emailOptional": "E-mail (Valgfrit)",
- "nameOptional": "Navn (valgfrit)",
- "accessControls": "Adgangskontroller",
- "userDescription2": "Administrer indstillingerne for denne bruger",
- "accessRoleErrorAdd": "Kunne ikke tilføje bruger i rolle",
- "accessRoleErrorAddDescription": "Det opstod en fejl under tildeling af brugeren til rollen.",
- "userSaved": "Bruger gemt",
- "userSavedDescription": "Brugeren er blevet opdateret.",
- "autoProvisioned": "Automatisk provisioneret",
- "autoProvisionSettings": "Autoprovisioneringsindstillinger",
- "autoProvisionedDescription": "Tillad denne bruger at blive automatisk administrert af en identitetsudbyder",
- "accessControlsDescription": "Administrer, hvad denne bruger kan få adgang til og gøre i organisationen",
- "accessControlsSubmit": "Gem adgangskontroller",
- "singleRolePerUserPlanNotice": "Din plan understøtter kun én rolle per bruger.",
- "singleRolePerUserEditionNotice": "Denne udgaven understøtter kun én rolle per bruger.",
- "roles": "Roller",
- "accessUsersRoles": "Administrer brugere og roller",
- "accessUsersRolesDescription": "Inviter brugere og legg dem til roller for at administrer adgang til organisationen",
- "key": "Nøgle",
- "createdAt": "Oprettet",
- "proxyErrorInvalidHeader": "Ugyldig værdi for brugerdefineret host-header. Brug domænenavnformat, eller gem feltet tomt for at fjerne den brugerdefinerede host-header.",
- "proxyErrorTls": "Ugyldig TLS-servernavn. Brug domænenavnformat, eller lad stå tomt for at fjerne TLS-servernavnet.",
- "proxyEnableSSL": "Aktivér TLS",
- "proxyEnableSSLDescription": "Aktivér SSL/TLS-kryptering for en sikker HTTPS-forbindelse til målene.",
- "target": "Mål",
- "configureTarget": "Konfigurer mål",
- "targetErrorFetch": "Kunne ikke hente mål",
- "targetErrorFetchDescription": "Det opstod en fejl under hentning af mål",
- "siteErrorFetch": "Kunne ikke hente ressource",
- "siteErrorFetchDescription": "Det opstod en fejl under hentning af ressource",
- "targetErrorDuplicate": "Dupliker mål",
- "targetErrorDuplicateDescription": "Et mål med disse indstillingerne findes allerede",
- "targetWireGuardErrorInvalidIp": "Ugyldig mål-IP",
- "targetWireGuardErrorInvalidIpDescription": "Mål-IP skal være i sitets subnet.",
- "targetsUpdated": "Mål opdateret",
- "targetsUpdatedDescription": "Mål og indstillinger opdateret med succes",
- "targetsErrorUpdate": "Mislykkedes at opdatere mål",
- "targetsErrorUpdateDescription": "En fejl opstod under opdatering af mål",
- "targetTlsUpdate": "TLS-indstillinger opdateret",
- "targetTlsUpdateDescription": "TLS-indstillinger er blevet opdateret",
- "targetErrorTlsUpdate": "Mislykkedes under opdatering af TLS-indstillinger",
- "targetErrorTlsUpdateDescription": "Det opstod en fejl under opdatering af TLS-indstillinger",
- "proxyUpdated": "Proxyindstillinger opdateret",
- "proxyUpdatedDescription": "Proxy indstillinger er blevet opdateret",
- "proxyErrorUpdate": "En fejl opstod under opdatering af proxyindstillinger",
- "proxyErrorUpdateDescription": "En fejl opstod under opdatering af proxyindstillinger",
- "targetAddr": "Vært",
- "targetPort": "Port",
- "targetProtocol": "Protokol",
- "targetTlsSettings": "Sikker forbindelseskonfiguration",
- "targetTlsSettingsDescription": "Konfigurer SSL/TLS-indstillinger for ressourcen",
- "targetTlsSettingsAdvanced": "Avanserte TLS-indstillinger",
- "targetTlsSni": "TLS servernavn",
- "targetTlsSniDescription": "TLS-servernavnet som skal bruges for SNI. Lad stå tomt for at bruge standardverdien.",
- "targetTlsSubmit": "Gem indstillinger",
- "targets": "Målkonfiguration",
- "targetsDescription": "Opsæt mål for routetrafik til backendtjenesterne",
- "targetStickySessions": "Aktivér sticky sessions",
- "targetStickySessionsDescription": "Behold forbindelser på samme backend-mål gennem hele sessionen.",
- "methodSelect": "Vælg metode",
- "targetSubmit": "Tilføj mål",
- "targetNoOne": "Denne ressource har ikke nogen mål. Tilføj et mål for at konfigurere hvor du vil sende forespørgsler til backend.",
- "targetNoOneDescription": "AT tilføje mere enn ét mål ovenfor vil aktivere load balancing.",
- "targetsSubmit": "Gem indstillinger",
- "addTarget": "Tilføj mål",
- "proxyMultiSiteRoundRobinNodeHelp": "Round-robin-routing vil ikke fungere mellem steder der ikke er forbundet til samme node, men failover vil fungere.",
- "targetErrorInvalidIp": "Ugyldig IP-adresse",
- "targetErrorInvalidIpDescription": "Indtast en gyldig IP-adresse eller hostnavn",
- "targetErrorInvalidPort": "Ugyldig port",
- "targetErrorInvalidPortDescription": "Indtast venligst et gyldig portnummer",
- "targetErrorNoSite": "Intet site valgt",
- "targetErrorNoSiteDescription": "Vælg et site for målet",
- "targetTargetsCleared": "Mål ryddet",
- "targetTargetsClearedDescription": "Alle mål er blevet fjernet fra denne ressource",
- "targetCreated": "Mål oprettet",
- "targetCreatedDescription": "Målet er blevet oprettet",
- "targetErrorCreate": "Kunne ikke oprette målet",
- "targetErrorCreateDescription": "Det opstod en fejl under oprettelse af målet",
- "tlsServerName": "TLS servernavn",
- "tlsServerNameDescription": "Servernavnet som skal bruges for SNI",
- "save": "Gem",
- "proxyAdditional": "Yderligere Proxyindstillinger",
- "proxyAdditionalDescription": "Konfigurer hvordan ressourcen håndterer proxy-indstillingerne",
- "proxyCustomHeader": "Tilpasset host-header",
- "proxyCustomHeaderDescription": "Host-header som skal settes ved videresendelse af forespørgsler. Lad stå tom for at bruge standardinnstillingen.",
- "proxyAdditionalSubmit": "Gem proxyindstillinger",
- "subnetMaskErrorInvalid": "Ugyldig subnetmaske. Skal være mellem 0 og 32.",
- "ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat",
- "ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet",
- "path": "Sti",
- "matchPath": "Match sti",
- "ipAddressRange": "IP-område",
- "rulesErrorFetch": "Kunne ikke hente regler",
- "rulesErrorFetchDescription": "Det opstod en fejl under hentning af regler",
- "rulesErrorDuplicate": "Duplikeret regel",
- "rulesErrorDuplicateDescription": "En regel med disse indstillingerne findes allerede",
- "rulesErrorInvalidIpAddressRange": "Ugyldig CIDR",
- "rulesErrorInvalidIpAddressRangeDescription": "Indtast et gyldig CIDR-site (f.eks. 10.0.0.0/8).",
- "rulesErrorInvalidUrl": "Ugyldig sti",
- "rulesErrorInvalidUrlDescription": "Indtast en gyldig URL-sti eller et mønster (f.eks., /api/*).",
- "rulesErrorInvalidIpAddress": "Ugyldig IP-adresse",
- "rulesErrorInvalidIpAddressDescription": "Indtast en gyldig IPv4 eller IPv6 adresse.",
- "rulesErrorUpdate": "Kunne ikke opdatere regler",
- "rulesErrorUpdateDescription": "Det opstod en fejl under opdatering af regler",
- "rulesUpdated": "Aktivér Regler",
- "rulesUpdatedDescription": "Regelevalueringen er blevet opdateret",
- "rulesMatchIpAddressRangeDescription": "Angiv en adresse i CIDR-format (f.eks., 103.21.244.0/22)",
- "rulesMatchIpAddress": "Angiv en IP-adresse (f.eks. 103.21.244.12)",
- "rulesMatchUrl": "Indtast en URL-sti eller et mønster (f.eks. /api/v1/todos eller /api/v1/*)",
- "rulesErrorInvalidPriority": "Ugyldig prioritet",
- "rulesErrorInvalidPriorityDescription": "Indtast et heltall på 1 eller højere.",
- "rulesErrorDuplicatePriority": "Duplikerede prioriteter",
- "rulesErrorDuplicatePriorityDescription": "Hver regel skal have et unikt prioritetstall.",
- "rulesErrorValidation": "Ugyldige regler",
- "rulesErrorValidationRuleDescription": "Regel {ruleNumber}: {message}",
- "rulesErrorInvalidMatchTypeDescription": "Vælg en gyldig matchtype (sti, IP, CIDR, land, region eller ASN).",
- "rulesErrorValueRequired": "Indtast en værdi for denne reglen.",
- "rulesErrorInvalidCountry": "Ugyldig land",
- "rulesErrorInvalidCountryDescription": "Vælg et gyldig land.",
- "rulesErrorInvalidAsn": "Ugyldig ASN",
- "rulesErrorInvalidAsnDescription": "Indtast en gyldig ASN (f.eks., AS15169).",
- "ruleUpdated": "Regler opdateret",
- "ruleUpdatedDescription": "Reglerne er opdateret",
- "ruleErrorUpdate": "Operation mislykkedes",
- "ruleErrorUpdateDescription": "En fejl opstod under gemmehandlingen",
- "rulesPriority": "Prioritet",
- "rulesReorderDragHandle": "Dra for at omorganisere regelprioriteringen",
- "rulesAction": "Handling",
- "rulesMatchType": "Matchtype",
- "value": "Værdi",
- "rulesAbout": "Om regler",
- "rulesAboutDescription": "Regler giver mulighed til at kontrollere adgangen til ressourcen baseret på et sett af kriterier. Du kan oprette regler for at tillade eller afvise adgang baseret på IP-adresse eller URL-sti.",
- "rulesActions": "Handlinger",
- "rulesActionAlwaysAllow": "Tillad altid: Omgå alle autentiserings metoder",
- "rulesActionAlwaysDeny": "Afvis altid: Bloker alle forespørgsler; ingen autentificering kan forsøges",
- "rulesActionPassToAuth": "Send til autentificering: Tillad, at autentificeringsmetoder kan forsøges",
- "rulesMatchCriteria": "Matchende kriterier",
- "rulesMatchCriteriaIpAddress": "Match en specifik IP-adresse",
- "rulesMatchCriteriaIpAddressRange": "Match et IP-adresseområde i CIDR-notation",
- "rulesMatchCriteriaUrl": "Match en URL-sti eller et mønster",
- "rulesEnable": "Aktivér regler",
- "rulesEnableDescription": "Aktivér eller deaktivér regelevaluering for denne ressource",
- "rulesResource": "Konfiguration af ressourceregler",
- "rulesResourceDescription": "Konfigurer regler for at kontrollere adgang til ressourcen",
- "ruleSubmit": "Tilføj regel",
- "rulesNoOne": "Ingen regler endnu.",
- "rulesOrder": "Regler evalueres efter prioritet i stigende rækkefølge.",
- "rulesSubmit": "Gem regler",
- "policyErrorCreate": "Fejl ved oprettelse af politik",
- "policyErrorCreateDescription": "Det opstod en fejl under oprettelse af politikken",
- "policyErrorCreateMessageDescription": "En uventet fejl opstod",
- "policyErrorUpdate": "Fejl ved opdatering af politik",
- "policyErrorUpdateDescription": "Det opstod en fejl ved opdatering af politikken",
- "policyErrorUpdateMessageDescription": "En uventet fejl opstod",
- "policyCreatedSuccess": "Ressourcepolitikken blev oprettet med succes",
- "policyUpdatedSuccess": "Ressourcepolitikken blev opdateret med succes",
- "authMethodsSave": "Gem indstillinger",
- "policyAuthStackTitle": "Autentificering",
- "policyAuthStackDescription": "Kontroller hvilke autentificeringsmetoder som kræves for at få adgang til denne ressource",
- "policyAuthOrLogicTitle": "Flere autentificeringsmetoder aktive",
- "policyAuthOrLogicBanner": "Besøgende kan autentificere sig med en hvilken som helst af de aktive metoder nedenfor. De behøver ikke at fuldføre dem alle.",
- "policyAuthMethodActive": "Aktiv",
- "policyAuthMethodOff": "Af",
- "policyAuthSsoTitle": "Platform-SSO",
- "policyAuthSsoDescription": "Kræv påloging gennem din organisations identitetsudbyder",
- "policyAuthSsoSummary": "{idp} · {users} brugere, {roles} roller",
- "policyAuthSsoDefaultIdp": "Standardudbyder",
- "policyAuthAddDefaultIdentityProvider": "Tilføj standardidentitetsudbyder",
- "policyAuthOtherMethodsTitle": "Andre metoder",
- "policyAuthOtherMethodsDescription": "Valgfrie metoder, som besøgende kan bruge i stedet for eller ud over platform-SSO",
- "policyAuthPasscodeTitle": "Adgangskode",
- "policyAuthPasscodeDescription": "Kræv en delt alfanumerisk adgangskode for at få adgang til ressourcen",
- "policyAuthPasscodeSummary": "Adgangskode sat",
- "policyAuthPincodeTitle": "PIN-kode",
- "policyAuthPincodeDescription": "En kort numerisk kode kræves for at få adgang til ressourcen",
- "policyAuthPincodeSummary": "6-sifret PIN sat",
- "policyAuthEmailTitle": "E-mail hviteliste",
- "policyAuthEmailDescription": "Tillad angivne e-mailadresser med engangskode",
- "policyAuthEmailSummary": "{count} adresser tilladt",
- "policyAuthEmailOtpCallout": "Aktivering af e-mail hviteliste sender en engangskode til den besøgendes e-mail ved login.",
- "policyAuthHeaderAuthTitle": "Grunnleggende Header Autentificering",
- "policyAuthHeaderAuthDescription": "Bekræft et tilpasset HTTP-headernavn og værdi ved hver forespørgsel",
- "policyAuthHeaderAuthSummary": "Header konfigureret",
- "policyAuthHeaderName": "Brugernavn",
- "policyAuthHeaderValue": "Adgangskode",
- "policyAuthSetPasscode": "Angiv adgangskode",
- "policyAuthSetPincode": "Sett PIN-kode",
- "policyAuthSetEmailWhitelist": "Angiv e-mail hviteliste",
- "policyAuthSetHeaderAuth": "Sett grunnleggende Header Autentificering",
- "policyAccessRulesTitle": "Adgangsregler",
- "policyAccessRulesEnableDescription": "Når aktiveret, bliver regler evalueret i faldende rækkefølge til en evaluerer til sann.",
- "policyAccessRulesFirstMatch": "Regler evalueres oppefra og ned. Den første matchende regel bestemmer resultatet.",
- "policyAccessRulesHowItWorks": "Regler matcher forespørgsler efter sti, IP-adresse, placering eller andre kriterier. Hver regel anvender en handling: omgå autentificering, blokere adgang, eller sende til autentificering. Hvis ingen regler matcher, fortsetter trafikken til autentificering.",
- "policyAccessRulesFallthroughOff": "Når regler er deaktiveret, går all trafik gennem til autentificering.",
- "policyAccessRulesFallthroughOn": "Når ingen regler matcher, fortsetter trafikken til autentificering.",
- "rulesPlaceholderCidr": "10.0.0.0/8",
- "rulesPlaceholderPath": "/admin/*",
- "rulesPlaceholderGeo": "RU, KP",
- "rulesSave": "Gem Regler",
- "resourceErrorCreate": "Fejl under oprettelse af ressource",
- "resourceErrorCreateDescription": "Det opstod en fejl under oprettelse af ressourcen",
- "resourceErrorCreateMessage": "Fejl ved oprettelse af ressource:",
- "resourceErrorCreateMessageDescription": "En uventet fejl opstod",
- "sitesErrorFetch": "Fejl ved hentning af sites",
- "sitesErrorFetchDescription": "En fejl opstod ved hentning af områdene",
- "domainsErrorFetch": "Kunne ikke hente domæner",
- "domainsErrorFetchDescription": "Der opstod en fejl under hentning af domænerne",
- "none": "Ingen",
- "unknown": "Ukendt",
- "resources": "Ressourcer",
- "resourcesDescription": "Ressourcer er proxyer til applikationer, der kører på det private netværk. Opret en ressource for enhver HTTP/HTTPS- eller rå TCP/UDP-tjeneste på dit private netværk. Hver ressource skal kobles til et site for at aktivere privat og sikker forbindelse gennem en krypteret WireGuard-tunnel.",
- "resourcesWireGuardConnect": "Sikker forbindelse med WireGuard-kryptering",
- "resourcesMultipleAuthenticationMethods": "Konfigurer flere autentificeringsmetoder",
- "resourcesUsersRolesAccess": "Bruger- og rollebaseret adgangskontrol",
- "resourcesErrorUpdate": "Mislykkedes at slå af/på ressource",
- "resourcesErrorUpdateDescription": "En fejl opstod under opdatering af ressourcen",
- "access": "Adgang",
- "accessControl": "Adgangskontrol",
- "shareLink": "{resource} Delbart link",
- "resourceSelect": "Vælg ressource",
- "shareLinks": "Delbare links",
- "share": "Delbare links",
- "shareDescription2": "Opret delbare links til ressourcer. Links giver midlertidig eller ubegrænset adgang til din ressource. Du kan konfigurere udløbsvarigheden på linket når du opretter en.",
- "shareEasyCreate": "Enkelt at oprette og dele",
- "shareConfigurableExpirationDuration": "Konfigurerbar udløbsvarighed",
- "shareSecureAndRevocable": "Sikker og kan tilbagekaldes",
- "nameMin": "Navn skal være mindst {len} tegn.",
- "nameMax": "Navn kan ikke være længere enn {len} tegn.",
- "sitesConfirmCopy": "Venligst bekræft at du har kopieret konfigurationen.",
- "unknownCommand": "Ukjent kommando",
- "newtErrorFetchReleases": "Mislykkedes at hente utgivelsesinfo: {err}",
- "newtErrorFetchLatest": "Fejl ved hentning af seneste utgivelse: {err}",
- "newtEndpoint": "Endpoint",
- "newtId": "ID",
- "newtSecretKey": "Sikkerhedsnøgle",
- "newtVersion": "Version",
- "architecture": "Arkitektur",
- "sites": "Websteder",
- "siteWgAnyClients": "Brug hvilken som helst WireGuard klient til at oprette forbindelse til. Du skal adressere interne ressourcer ved hjælp af peer IP.",
- "siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter",
- "siteWgManualConfigurationRequired": "Manuel konfiguration påkrævet",
- "userErrorNotAdminOrOwner": "Bruger er ikke administrator eller ejer",
- "pangolinSettings": "Indstillinger - Pangolin",
- "accessRoleYour": "Din rolle:",
- "accessRoleSelect2": "Vælg roller",
- "accessUserSelect": "Vælg brugere",
- "otpEmailEnter": "Indtast én e-mail",
- "otpEmailEnterDescription": "Trykk enter for at tilføje en e-mail efter at have tastet den ind i tekstfeltet.",
- "otpEmailErrorInvalid": "Ugyldig e-mailadresse. Wildcardet (*) skal være hele lokaldelen.",
- "otpEmailSmtpRequired": "SMTP påkrævet",
- "otpEmailSmtpRequiredDescription": "SMTP skal være aktiveret på serveren for at bruge engangskode-autentificering.",
- "otpEmailTitle": "Engangskode",
- "otpEmailTitleDescription": "Kræv e-mailbaseret autentificering for ressourceadgang",
- "otpEmailWhitelist": "E-mailwhitelist",
- "otpEmailWhitelistList": "Whitelistede e-mails",
- "otpEmailWhitelistListDescription": "Kun brugere med disse e-mailadresser får adgang til denne ressource. De bliver bedt om at indtaste en engangskode sendt til deres e-mail. Wildcards (*@example.com) kan bruges til at tillade enhver e-mailadresse fra et domæne.",
- "otpEmailWhitelistSave": "Gem whitelist",
- "passwordAdd": "Tilføj adgangskode",
- "passwordRemove": "Fjern adgangskode",
- "pincodeAdd": "Tilføj PIN-kode",
- "pincodeRemove": "Fjern PIN-kode",
- "resourceAuthMethods": "Autentificeringsmetoder",
- "resourcePolicyAuthMethodsEmpty": "Ingen autentificeringsmetode",
- "resourcePolicyOtpEmpty": "Ingen engangskode",
- "resourcePolicyReadOnly": "Denne politikken er kun læsebar",
- "resourcePolicyReadOnlyDescription": "Denne ressourcereglen deles på tværs af flere ressourcer, du kan ikke redigere den på denne side.",
- "editSharedPolicy": "Rediger Delte Rekvireringer",
- "resourcePolicyTypeSave": "Gem Ressourcetype",
- "resourcePolicySelect": "Vælg ressourcepolitik",
- "resourcePolicySelectError": "Vælg en ressourceregel",
- "resourcePolicyNotFound": "Politik ikke fundet",
- "resourcePolicySearch": "Søg efter regler",
- "resourcePolicyRulesEmpty": "Ingen autentificeringsregler",
- "resourceAuthMethodsDescriptions": "Tillad adgang til ressourcen via yderligere autentificeringsmetoder",
- "resourceAuthSettingsSave": "Gemt med succes",
- "resourceAuthSettingsSaveDescription": "Autentificeringsindstillinger er gemt",
- "resourceErrorAuthFetch": "Kunne ikke hente data",
- "resourceErrorAuthFetchDescription": "Det opstod en fejl ved hentning af data",
- "resourceErrorPasswordRemove": "Fejl ved fjernelse af adgangskode for ressource",
- "resourceErrorPasswordRemoveDescription": "Det opstod en fejl ved fjernelse af ressourceadgangskoden",
- "resourceErrorPasswordSetup": "Fejl ved indstilling af ressourceadgangskode",
- "resourceErrorPasswordSetupDescription": "Det opstod en fejl ved indstilling af ressourceadgangskoden",
- "resourceErrorPincodeRemove": "Fejl ved fjernelse af ressource-PIN-koden",
- "resourceErrorPincodeRemoveDescription": "Det opstod en fejl under fjernelse af ressource-pinkoden",
- "resourceErrorPincodeSetup": "Fejl ved indstilling af ressource-PIN-kode",
- "resourceErrorPincodeSetupDescription": "Det opstod en fejl under indstilling af ressourcens PIN-kode",
- "resourceErrorUsersRolesSave": "Kunne ikke sette roller",
- "resourceErrorUsersRolesSaveDescription": "En fejl opstod ved indstilling af rollene",
- "resourceErrorWhitelistSave": "Mislykkedes at gem whitelist",
- "resourceErrorWhitelistSaveDescription": "Det opstod en fejl under lagring af whitelisten",
- "resourcePasswordSubmit": "Aktivér adgangskodebeskyttelse",
- "resourcePasswordProtection": "Adgangskodebeskyttelse {status}",
- "resourcePasswordRemove": "Ressourceadgangskode fjernet",
- "resourcePasswordRemoveDescription": "Fjernelse af ressourceadgangskoden var med succes",
- "resourcePasswordSetup": "Ressourceadgangskode sat",
- "resourcePasswordSetupDescription": "Ressourceadgangskoden er blevet sat",
- "resourcePasswordSetupTitle": "Angiv adgangskode",
- "resourcePasswordSetupTitleDescription": "Sett et adgangskode for at beskytte denne ressource",
- "resourcePincode": "PIN-kode",
- "resourcePincodeSubmit": "Aktivér PIN-kodebeskyttelse",
- "resourcePincodeProtection": "PIN-kodebeskyttelse {status}",
- "resourcePincodeRemove": "Ressource PIN-kode fjernet",
- "resourcePincodeRemoveDescription": "Ressourceadgangskoden blev fjernet",
- "resourcePincodeSetup": "Ressource PIN-kode sat",
- "resourcePincodeSetupDescription": "Ressource PIN-kode er sat med succes",
- "resourcePincodeSetupTitle": "Angiv PIN-kode",
- "resourcePincodeSetupTitleDescription": "Sett en PIN-kode for at beskytte denne ressource",
- "resourceRoleDescription": "Administratorer har altid adgang til denne ressource.",
- "resourcePolicySelectTitle": "Ressourceadgangspolitik",
- "resourcePolicySelectDescription": "Vælg politiktype for autentificering",
- "resourcePolicyTypeLabel": "Politiktype",
- "resourcePolicyLabel": "Ressourcepolitik",
- "resourcePolicyInline": "Inline ressourceregler",
- "resourcePolicyInlineDescription": "Adgangspolitik som kun er gyldig for denne ressource",
- "resourcePolicyShared": "Delte ressourceregler",
- "resourcePolicySharedDescription": "Denne ressource bruger en delt politik.",
- "sharedPolicy": "Delt politik",
- "sharedPolicyNoneDescription": "Denne ressource har sin egen politik.",
- "resourceSharedPolicyOwnDescription": "Denne ressource har sine egne autentiserings- og adgangskontroller.",
- "resourceSharedPolicyInheritedDescription": "Denne ressource arver fra {policyName}.",
- "resourceSharedPolicyAuthenticationNotice": "Denne ressource bruger en delt politik. Nogen autentificeringsindstillinger kan redigeres på denne ressource for at tilføje politikken. For at ændre den underliggende politikken, skal du redigere {policyName}.",
- "resourceSharedPolicyRulesNotice": "Denne ressource bruger en delt politik. Nogen adgangsregler kan redigeres på denne ressource. For at ændre den underliggende politikken, skal du redigere {policyName}.",
- "resourceUsersRoles": "Adgangskontroller",
- "resourceUsersRolesDescription": "Konfigurer hvilke brugere og roller som har adgang til denne ressource",
- "resourceUsersRolesSubmit": "Gem adgangskontroller",
- "resourceWhitelistSave": "Gem med succes",
- "resourceWhitelistSaveDescription": "Whitelist-indstillinger er gemt",
- "ssoUse": "Brug platform-SSO",
- "ssoUseDescription": "Eksisterende brugere behøver kun at loge på én gang for alle ressourcer som har dette aktiveret.",
- "proxyErrorInvalidPort": "Ugyldig portnummer",
- "subdomainErrorInvalid": "Ugyldig underdomæne",
- "domainErrorFetch": "Fejl ved hentning af domæner",
- "domainErrorFetchDescription": "Der opstod en fejl ved hentning af domænerne",
- "resourceErrorUpdate": "Mislykkedes at opdatere ressource",
- "resourceErrorUpdateDescription": "Det opstod en fejl under opdatering af ressourcen",
- "resourceUpdated": "Ressource opdateret",
- "resourceUpdatedDescription": "Ressourcen er opdateret med succes",
- "resourceErrorTransfer": "Kunne ikke overføre ressource",
- "resourceErrorTransferDescription": "En fejl opstod under overføring af ressourcen",
- "resourceTransferred": "Ressource overført",
- "resourceTransferredDescription": "Ressourcen er overført med succes.",
- "resourceErrorToggle": "Mislykkedes at veksle ressource",
- "resourceErrorToggleDescription": "Det opstod en fejl ved opdatering af ressourcen",
- "resourceVisibilityTitle": "Synlighed",
- "resourceVisibilityTitleDescription": "Fuldstændigt aktivér eller deaktivér ressourcesynlighed",
- "resourceGeneral": "Generelle indstillinger",
- "resourceGeneralDescription": "Konfigurer navn, adresse og adgangspolitik for denne ressource.",
- "resourceGeneralDetailsSubsection": "Ressourcedetaljer",
- "resourceGeneralDetailsSubsectionDescription": "Angiv visningsnavn, identifikator og offentligt tilgængelig domæne for denne ressource.",
- "resourceGeneralDetailsSubsectionPortDescription": "Angiv visningsnavn, identifikator og offentlig port for denne ressource.",
- "resourceGeneralPublicAddressSubsection": "Offentlig adresse",
- "resourceGeneralPublicAddressSubsectionDescription": "Konfigurer hvordan brugere får adgang til denne ressource.",
- "resourceGeneralAuthenticationAccessSubsection": "Autentificering og adgang",
- "resourceGeneralAuthenticationAccessSubsectionDescription": "Vælg om denne ressource bruger sin egen politik eller arver fra en delt politik.",
- "resourceEnable": "Aktivér ressource",
- "resourceTransfer": "Overfør Ressource",
- "resourceTransferDescription": "Overfør denne ressource til et andet site",
- "resourceTransferSubmit": "Overfør ressource",
- "siteDestination": "Destinasjonsområde",
- "searchSites": "Søg i sites",
- "countries": "Land",
- "accessRoleCreate": "Opret rolle",
- "accessRoleCreateDescription": "Opret en ny rolle for at gruppere brugere og administrer deres tilladelser.",
- "accessRoleEdit": "Rediger rolle",
- "accessRoleEditDescription": "Rediger rolleinformation.",
- "accessRoleCreateSubmit": "Opret rolle",
- "accessRoleCreated": "Rolle oprettet",
- "accessRoleCreatedDescription": "Rollen er med succes oprettet.",
- "accessRoleErrorCreate": "Kunne ikke oprette rolle",
- "accessRoleErrorCreateDescription": "Det opstod en fejl under oprettelse af rollen.",
- "accessRoleUpdateSubmit": "Opdater rolle",
- "accessRoleUpdated": "Rollen opdateret",
- "accessRoleUpdatedDescription": "Rollen er blevet opdateret.",
- "accessApprovalUpdated": "Godkendelse behandlet",
- "accessApprovalApprovedDescription": "Sett godkendelsesanmodning om at acceptere.",
- "accessApprovalDeniedDescription": "Sett godkendelsesanmodning om at afvise.",
- "accessRoleErrorUpdate": "Kunne ikke opdatere rolle",
- "accessRoleErrorUpdateDescription": "Det opstod en fejl under opdatering af rollen.",
- "accessApprovalErrorUpdate": "Kunne ikke administrer godkendelse",
- "accessApprovalErrorUpdateDescription": "Det opstod en fejl under behandling af godkendelsen.",
- "accessRoleErrorNewRequired": "Ny rolle kræves",
- "accessRoleErrorRemove": "Kunne ikke fjerne rolle",
- "accessRoleErrorRemoveDescription": "Det opstod en fejl under fjernelse af rollen.",
- "accessRoleName": "Rollenavn",
- "accessRoleQuestionRemove": "Du er ved at slette rollen `{name}`. Du kan ikke fortryde denne handling.",
- "accessRoleRemove": "Fjern Rolle",
- "accessRoleRemoveDescription": "Fjern en rolle fra organisationen",
- "accessRoleRemoveSubmit": "Fjern Rolle",
- "accessRoleRemoved": "Rolle fjernet",
- "accessRoleRemovedDescription": "Rollen er med succes fjernet.",
- "accessRoleRequiredRemove": "Før du sletter denne rolle, vælg venligst en ny rolle at overføre eksisterende medlemmer til.",
- "network": "Netværk",
- "manage": "Administrer",
- "sitesNotFound": "Ingen sites fundet.",
- "pangolinServerAdmin": "Serveradmin - Pangolin",
- "licenseTierProfessional": "Professionel licens",
- "licenseTierEnterprise": "Enterprise-licens",
- "licenseTierPersonal": "Personlig licens",
- "licensed": "Licenseret",
- "yes": "Ja",
- "no": "Nej",
- "sitesAdditional": "Yderligere sites",
- "licenseKeys": "Licensnøgler",
- "sitestCountDecrease": "Reducer antal sites",
- "sitestCountIncrease": "Øk antal sites",
- "idpManage": "Administrer Identitetsudbydere",
- "idpManageDescription": "Vis og administrer identitetsudbydere i systemet",
- "idpGlobalModeBanner": "Identitetsudbydere (IdPs) per organisation er deaktiveret på denne serveren. Den bruger globale IdP (delt på tværs af alle organisationer). Administrer globale IdP'er i adminpanelet. For at aktivere IdP per organisation, rediger serverkonfigurationen og sett IdP-tilstand til org. Se dokumentasjonen. Hvis du vil fortsætte at bruge globale IdPs og få denne til at forsvinne fra organisationens indstillinger, sat eksplicit tilstanden til global i konfigurationen.",
- "idpGlobalModeBannerUpgradeRequired": "Identitetsudbydere (IdP'er) pr. organisation er deaktiveret på denne server. Den bruger globale IdP'er (delt på tværs af alle organisationer). Administrer globale IdP'er i administrationspanelet. For at bruge identitetsudbydere pr. organisation skal du opgradere til Enterprise-udgaven.",
- "idpGlobalModeBannerLicenseRequired": "Identitetsudbydere (IdPs) per organisation er deaktiveret på denne serveren. Den bruger globale IdPs (delt på tværs af alle organisationer). Administrer globale IdPs i administrationspanelet. For at bruge identitetsudbydere per organisation, kræves en Enterprise-licens.",
- "idpDeletedDescription": "Identitetsudbyder slettet med succes",
- "idpOidc": "OAuth2/OIDC",
- "idpQuestionRemove": "Er du sikker på at du vil slette identitetsudbyder permanent?",
- "idpMessageRemove": "Dette vil fjerne identitetsudbyderen og alle tilhørende konfigurationer. Brugere, der autentificerer sig via denne udbyder, vil ikke længere kunne logge ind.",
- "idpMessageConfirm": "For at bekræfte, indtast venligst navnet på identitetsudbyderen nedenfor.",
- "idpConfirmDelete": "Bekræft Sletning af Identitetsudbyder",
- "idpDelete": "Slet identitetsudbyder",
- "idp": "Identitetsudbydere",
- "idpSearch": "Søg identitetsudbydere...",
- "idpAdd": "Tilføj Identitetsudbyder",
- "idpClientIdRequired": "Klient-ID er påkrævet.",
- "idpClientSecretRequired": "Klienthemmelighed er påkrævet.",
- "idpErrorAuthUrlInvalid": "Autentiserings-URL skal være en gyldig URL.",
- "idpErrorTokenUrlInvalid": "Token-URL skal være en gyldig URL.",
- "idpPathRequired": "Identifikatorbane er påkrævet.",
- "idpScopeRequired": "Scopes kræves.",
- "idpOidcDescription": "Konfigurer en OpenID Connect identitetsudbyder",
- "idpCreatedDescription": "Identitetsudbyder oprettet med succes.",
- "idpCreate": "Opret identitetsudbyder",
- "idpCreateDescription": "Konfigurer en ny identitetsudbyder for brugerautentificering",
- "idpSeeAll": "Se alle identitetsudbydere",
- "idpSettingsDescription": "Konfigurer grunnleggende information for din identitetsudbyder",
- "idpDisplayName": "Et visningsnavn for denne identitetsudbyder",
- "idpAutoProvisionUsers": "Automatisk brugerprovisionering",
- "idpAutoProvisionUsersDescription": "Når aktiveret, oprettes brugere automatisk i systemet ved første login, med mulighed til at tildele brugere til roller og organisationer.",
- "idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisioneringsindstillingerne når identitetsudbyderen er oprettet.",
- "licenseBadge": "EE",
- "idpType": "Udbydertype",
- "idpTypeDescription": "Vælg typen identitetsudbyder du ønsker at konfigurere",
- "idpOidcConfigure": "OAuth2/OIDC-konfiguration",
- "idpOidcConfigureDescription": "Konfigurer OAuth2/OIDC-leverandørens endpoints og legitimationsoplysninger",
- "idpClientId": "Klient-ID",
- "idpClientIdDescription": "OAuth2 klient-ID fra identitetsudbyderen",
- "idpClientSecret": "Klienthemmelighed",
- "idpClientSecretDescription": "Klient-hemmeligheten med OAuth2 fra identitetsudbyderen",
- "idpAuthUrl": "Autorisations-URL",
- "idpAuthUrlDescription": "OAuth2 autorisasjonsendepunkt URL",
- "idpTokenUrl": "Token-URL",
- "idpTokenUrlDescription": "OAuth2-tokenendepunkt-URL",
- "idpOidcConfigureAlert": "Viktig information",
- "idpOidcConfigureAlertDescription": "Efter at du har oprettet identitetsudbyderen, skal du konfigurere callback-URL'en i identitetsudbyderens indstillinger. Callback-URL vil blive tilføjet efter med succes oprettelse.",
- "idpToken": "Token-konfiguration",
- "idpTokenDescription": "Konfigurer hvordan brugerinformation trekkes ud fra ID-tokenet",
- "idpJmespathAbout": "Om JMESPath",
- "idpJmespathAboutDescription": "Stierne nedenfor bruger JMESPath-syntaks for at hente ud verdier fra ID-tokenet.",
- "idpJmespathAboutDescriptionLink": "Læs mere om JMESPath",
- "idpJmespathLabel": "Identifikatorsti",
- "idpJmespathLabelDescription": "Stien til brugeridentifikatoren i ID-tokenet",
- "idpJmespathEmailPathOptional": "E-mailsti (Valgfrit)",
- "idpJmespathEmailPathOptionalDescription": "Stien til brugerens e-mailadresse i ID-tokenet",
- "idpJmespathNamePathOptional": "Navn Sti (Valgfrit)",
- "idpJmespathNamePathOptionalDescription": "Stien til brugerens navn i ID-tokenet",
- "idpOidcConfigureScopes": "Scopes",
- "idpOidcConfigureScopesDescription": "Mellemrumsepareret liste over OAuth2-scopes at be om",
- "idpSubmit": "Opret identitetsudbyder",
- "orgPolicies": "Organisationspolitikker",
- "idpSettings": "{idpName} Indstillinger",
- "idpCreateSettingsDescription": "Konfigurer indstillingerne for identitetsudbyderen",
- "roleMapping": "Rolletildeling",
- "orgMapping": "Organisationsmapping",
- "orgPoliciesSearch": "Søg i organisationens politikker...",
- "orgPoliciesAdd": "Tilføj organisationspolitik",
- "orgRequired": "Organisation er påkrævet",
- "error": "Fejl",
- "success": "Succes",
- "orgPolicyAddedDescription": "Politik med succes tilføjet",
- "orgPolicyUpdatedDescription": "Politikken er med succes opdateret",
- "orgPolicyDeletedDescription": "Politik slettet med succes",
- "defaultMappingsUpdatedDescription": "Standardtildelinger opdateret med succes",
- "orgPoliciesAbout": "Om organisationens politikker",
- "orgPoliciesAboutDescription": "Organisationspolitikker bruges til at kontrollere adgang til organisationer baseret på brugerens ID-token. Du kan angive JMESPath-udtryk for at udtrække rolle- og organisationsinformation fra ID-tokenet.",
- "orgPoliciesAboutDescriptionLink": "Se dokumentation, for mere information.",
- "defaultMappingsOptional": "Standardtildelinger (Valgfrit)",
- "defaultMappingsOptionalDescription": "Standardtildelingerne bruges når det ikke er defineret en organisationspolitik for en organisation. Du kan angive standard rolle- og organisationstildelinger som det kan faldes tilbage på her.",
- "defaultMappingsRole": "Standard rolletildeling",
- "defaultMappingsRoleDescription": "Resultatet af dette udtryk skal returnere rollenavnet, som det er defineret i organisationen, som en streng.",
- "defaultMappingsOrg": "Standard organisationstildeling",
- "defaultMappingsOrgDescription": "Når denne er sat, skal udtrykket returnere organisations-IDen eller «true» for at brugeren skal få adgang til den organisationen. Når den ikke er sat, er det nok at definere en rolletildeling: brugeren får adgang så længe en gyldig rolletilknytning kan løses for dem i organisationen.",
- "defaultMappingsSubmit": "Gem standardtildelinger",
- "orgPoliciesEdit": "Rediger Organisationspolitik",
- "org": "Organisation",
- "orgSelect": "Vælg organisation",
- "orgSearch": "Søg organisation",
- "orgNotFound": "Ingen organisation fundet.",
- "roleMappingPathOptional": "Rollemappingsti (Valgfrit)",
- "orgMappingPathOptional": "Organisationstildelingssti (valgfrit)",
- "orgPolicyUpdate": "Opdater politik",
- "orgPolicyAdd": "Tilføj politik",
- "orgPolicyConfig": "Konfigurer adgang for en organisation",
- "idpUpdatedDescription": "Identitetsudbyder med succes opdateret",
- "redirectUrl": "Omdirigerings-URL",
- "orgIdpRedirectUrls": "Omdirigerings-URL'er",
- "redirectUrlAbout": "Om omdirigerings-URL",
- "redirectUrlAboutDescription": "Dette er URL'en som brugere vil blive omdirigeret efter autentificering. Du skal konfigurere denne URL'en i identitetsudbyderens indstillinger.",
- "pangolinAuth": "Autentificering - Pangolin",
- "verificationCodeLengthRequirements": "Din bekræftelseskode skal være 8 tegn.",
- "errorOccurred": "Det opstod en fejl",
- "emailErrorVerify": "Kunne ikke verificere e-mail:",
- "emailVerified": "E-mailen er bekræftet! Omdirigerer deg...",
- "verificationCodeErrorResend": "Kunne ikke sende bekræftelseskode igen:",
- "verificationCodeResend": "Bekræftelseskode sendt igen",
- "verificationCodeResendDescription": "Vi har sendt en ny bekræftelseskode til din e-mailadresse. Tjek venligst din indbakke.",
- "emailVerify": "Bekræft e-mail",
- "emailVerifyDescription": "Indtast bekræftelseskoden, der er sendt til din e-mailadresse.",
- "verificationCode": "Bekræftelseskode",
- "verificationCodeEmailSent": "Vi har sendt en bekræftelseskode til din e-mailadresse.",
- "submit": "Send",
- "emailVerifyResendProgress": "Sender igen...",
- "emailVerifyResend": "Har du ikke modtaget en kode? Klik her for at sende igen",
- "passwordNotMatch": "Adgangskoderne matcher ikke",
- "signupError": "Det opstod en fejl ved registrering",
- "pangolinLogoAlt": "Pangolin-logo",
- "inviteAlready": "Ser ud til at du er blevet inviteret!",
- "inviteAlreadyDescription": "For at acceptere invitationen skal du logge ind eller oprette en konto.",
- "signupQuestion": "Har du allerede en konto?",
- "login": "Log ind",
- "resourceNotFound": "Ressource ikke fundet",
- "resourceNotFoundDescription": "Ressourcen du prøver at få adgang til findes ikke.",
- "pincodeRequirementsLength": "PIN skal være præcis 6 cifre",
- "pincodeRequirementsChars": "PIN skal kun inneholde tal",
- "passwordRequirementsLength": "Adgangskode skal være mindst 1 tegn langt",
- "passwordRequirementsTitle": "Adgangskodekrav:",
- "passwordRequirementLength": "Mindst 8 tegn lang",
- "passwordRequirementUppercase": "Mindst én stort bogstav",
- "passwordRequirementLowercase": "Mindst ét lille bogstav",
- "passwordRequirementNumber": "Mindst ét tal",
- "passwordRequirementSpecial": "Mindst ét specialtegn",
- "passwordRequirementsMet": "✓ Adgangskoden opfylder alle krav",
- "passwordStrength": "Adgangskodestyrke",
- "passwordStrengthWeak": "Svag",
- "passwordStrengthMedium": "Medium",
- "passwordStrengthStrong": "Stærk",
- "passwordRequirements": "Krav:",
- "passwordRequirementLengthText": "8+ tegn",
- "passwordRequirementUppercaseText": "Stort bogstav (A-Z)",
- "passwordRequirementLowercaseText": "Lille bogstav (a-z)",
- "passwordRequirementNumberText": "Tal (0-9)",
- "passwordRequirementSpecialText": "Specialtegn (!@#$%...)",
- "passwordsDoNotMatch": "Adgangskoderne matcher ikke",
- "otpEmailRequirementsLength": "OTP skal være mindst 1 tegn lang.",
- "otpEmailSent": "OTP sendt",
- "otpEmailSentDescription": "En OTP er sendt til din e-mail",
- "otpEmailErrorAuthenticate": "Mislykkedes at autentificere med e-mail",
- "pincodeErrorAuthenticate": "Kunne ikke autentificere med PIN-kode",
- "passwordErrorAuthenticate": "Kunne ikke autentificere med adgangskode",
- "poweredBy": "Drevet af",
- "authenticationRequired": "Autentificering påkrævet",
- "authenticationMethodChoose": "Vælg din foretrukne metode for at få adgang til {name}",
- "authenticationRequest": "Du skal autentificere dig for at få adgang til {name}",
- "user": "Bruger",
- "pincodeInput": "6-sifret PIN-kode",
- "pincodeSubmit": "Log ind med PIN",
- "passwordSubmit": "Log ind med adgangskode",
- "otpEmailDescription": "En engangskode vil blive sendt til denne e-mailen.",
- "otpEmailSend": "Send engangskode",
- "otpEmail": "Engangskode (OTP)",
- "otpEmailSubmit": "Send OTP",
- "backToEmail": "Tilbage til E-mail",
- "noSupportKey": "Serveren kører uden en supporterlicens. Overvej at støtte prosjektet!",
- "accessDenied": "Adgang nægtet",
- "accessDeniedDescription": "Du har ikke adgang til denne ressource. Hvis dette er en fejl, venligst kontakt administratoren.",
- "accessTokenError": "Fejl ved tjek af adgangstoken",
- "accessGranted": "Adgang givet",
- "accessUrlInvalid": "Ugyldig adgangs-URL",
- "accessGrantedDescription": "Du har fått adgang til denne ressource. Omdirigerer deg...",
- "accessUrlInvalidDescription": "Denne delings-URL er ugyldig. Kontakt venligst ressourceejeren for en ny URL.",
- "tokenInvalid": "Ugyldigt token",
- "pincodeInvalid": "Ugyldig kode",
- "passwordErrorRequestReset": "Forespørgsel om nulstilling mislykkedes",
- "passwordErrorReset": "Kunne ikke nulstille adgangskode:",
- "passwordResetSuccess": "Adgangskoden er nulstillet! Går tilbage til login...",
- "passwordReset": "Nulstil adgangskode",
- "passwordResetDescription": "Følg trinene for at nulstille din adgangskode",
- "passwordResetSent": "Vi sender en kode til nulstilling af adgangskoden til denne e-mailadresse.",
- "passwordResetCode": "Nulstillingskode",
- "passwordResetCodeDescription": "Tjek e-mailen din for nulstillingskoden.",
- "generatePasswordResetCode": "Lag nulstillingskode for adgangskode",
- "passwordResetCodeGenerated": "Adgangskode nulstillingskoden er generert",
- "passwordResetCodeGeneratedDescription": "Del denne kode med brugeren. De kan bruge den til at nulstille adgangskoden.",
- "passwordResetUrl": "Nulstillings-URL",
- "passwordNew": "Ny adgangskode",
- "passwordNewConfirm": "Bekræft ny adgangskode",
- "changePassword": "Skift adgangskode",
- "changePasswordDescription": "Opdater adgangskoden for din konto",
- "oldPassword": "Nuværende adgangskode",
- "newPassword": "Ny adgangskode",
- "confirmNewPassword": "Bekræft ny adgangskode",
- "changePasswordError": "Kunne ikke ændre adgangskode",
- "changePasswordErrorDescription": "Der opstod en fejl under ændring af adgangskoden",
- "changePasswordSuccess": "Adgangskoden er ændret",
- "changePasswordSuccessDescription": "Dit adgangskode blev opdateret",
- "passwordExpiryRequired": "Adgangskodeudløb kræves",
- "passwordExpiryDescription": "Denne organisation kræver at du bytter adgangskode hver {maxDays} dag.",
- "changePasswordNow": "Bytt adgangskode nu",
- "pincodeAuth": "Godkendelseskode",
- "pincodeSubmit2": "Send kode",
- "passwordResetSubmit": "Be om nulstilling",
- "passwordResetAlreadyHaveCode": "Indtast koden",
- "passwordResetSmtpRequired": "Kontakt din administrator",
- "passwordResetSmtpRequiredDescription": "En adgangskode nulstillingskode kræves for at nulstille adgangskoden. Kontakt systemansvarlig for assistanse.",
- "passwordBack": "Tilbage til adgangskode",
- "loginBack": "Gå tilbage til loginsiden for hovedkontoen",
- "signup": "Tilmeld dig",
- "loginStart": "Log ind for at komme i gang",
- "idpOidcTokenValidating": "Validerer OIDC-token",
- "idpOidcTokenResponse": "Valider OIDC-tokensvar",
- "idpErrorOidcTokenValidating": "Fejl ved validering af OIDC-token",
- "idpConnectingTo": "Opretter forbindelse til {name}",
- "idpConnectingToDescription": "Validerer din identitet",
- "idpConnectingToProcess": "Opretter forbindelse til...",
- "idpConnectingToFinished": "Tilsluttet",
- "idpErrorConnectingTo": "Det opstod et problem med at oprette forbindelse til {name}. Venligst kontakt din administrator.",
- "idpErrorNotFound": "IdP ikke fundet",
- "inviteInvalid": "Ugyldig invitation",
- "labels": "Etiketter",
- "orgLabelsDescription": "Administrer etiketter i denne organisation.",
- "addLabels": "Tilføj etiketter",
- "siteLabelsTab": "Etiketter",
- "siteLabelsDescription": "Administrer etiketter knyttet til dette sitet.",
- "labelsNotFound": "Ingen etiketter fundet.",
- "labelsEmptyCreateHint": "Start at skrive ovenfor for at oprette en etikett.",
- "labelSearch": "Søg efter etiketter",
- "labelSearchOrCreate": "Søg eller opret en etikett",
- "accessLabelFilterCount": "{count, plural, one {en etikett} other {# etiketter}}",
- "labelOverflowCount": "+{count, plural, one {en etikett} other {# etiketter}}",
- "accessLabelFilterClear": "Fjern etikettfiltre",
- "accessFilterClear": "Fjern filtre",
- "selectColor": "Vælg farve",
- "createNewLabel": "Opret ny org-etikett \"{label}\"",
- "inviteInvalidDescription": "Invitationslinket er ugyldig.",
- "inviteErrorWrongUser": "Invitationen er ikke for denne bruger",
- "inviteErrorUserNotExists": "Brugeren findes ikke. Venligst opret en konto først.",
- "inviteErrorLoginRequired": "Du skal være loget ind for at acceptere en invitation",
- "inviteErrorExpired": "Invitationen kan have udløbet",
- "inviteErrorRevoked": "Invitationen kan have blevet trukket tilbage",
- "inviteErrorTypo": "Det kan være en stavefejl i invitationslinket",
- "pangolinSetup": "Opsætning - Pangolin",
- "orgNameRequired": "Organisationsnavn er påkrævet",
- "orgIdRequired": "Organisations-ID er påkrævet",
- "orgIdMaxLength": "Organisations-ID må højst være 32 tegn",
- "orgErrorCreate": "En fejl opstod under oprettelse af organisation",
- "pageNotFound": "Siden blev ikke fundet",
- "pageNotFoundDescription": "Ups! Siden, du leder efter, findes ikke.",
- "overview": "Oversigt",
- "home": "Hjem",
- "settings": "Indstillinger",
- "usersAll": "Alle brugere",
- "license": "Licens",
- "pangolinDashboard": "Dashboard - Pangolin",
- "noResults": "Ingen resultater fundet.",
- "terabytes": "{count} TB",
- "gigabytes": "{count} GB",
- "megabytes": "{count} MB",
- "tagsEntered": "Inntastede tager",
- "tagsEnteredDescription": "Dette er tagene du har tastet ind.",
- "tagsWarnCannotBeLessThanZero": "maxTags og minTags kan ikke være mindre end 0",
- "tagsWarnNotAllowedAutocompleteOptions": "Tag ikke tilladt i henhold til autofuldførelsesmuligheder",
- "tagsWarnInvalid": "Ugyldigt tag i henhold til validateTag",
- "tagWarnTooShort": "Tag {tagText} er for kort",
- "tagWarnTooLong": "Tag {tagText} er for langt",
- "tagsWarnReachedMaxNumber": "Maksimalt antal tilladte tager er nådd",
- "tagWarnDuplicate": "Duplikeret tag {tagText} blev ikke tilføjet",
- "supportKeyInvalid": "Ugyldig nøgle",
- "supportKeyInvalidDescription": "Din supporternøgle er ugyldig.",
- "supportKeyValid": "Gyldig nøgle",
- "supportKeyValidDescription": "Din supporternøgle er valideret. Tak for din støtte!",
- "supportKeyErrorValidationDescription": "Kunne ikke validere supporternøgle.",
- "supportKey": "Støtt utviklingen og adopter en Pangolin!",
- "supportKeyDescription": "Køb en supporternøgle for at hjelpe oss med at fortsætte utviklingen af Pangolin for fællesskabet. Dit bidrag lader oss bruge mere tid på at vedligeholde og tilføje nye funktioner i applikationen for alle. Vi vil aldrig bruge dette til at legge funktioner bak en betalingsmur. Dette er atskilt fra enhver kommerciel udgave.",
- "supportKeyPet": "Du vil også få adoptere og møte din helt egen kæledyr-Pangolin!",
- "supportKeyPurchase": "Betalinger behandles via GitHub. Etterpå kan du hente din nøgle på",
- "supportKeyPurchaseLink": "vores hjemmeside",
- "supportKeyPurchase2": "og løse den ind her.",
- "supportKeyLearnMore": "Læs mere.",
- "supportKeyOptions": "Vælg venligst den mulighed, der passer dig bedst.",
- "supportKetOptionFull": "Full supporter",
- "forWholeServer": "For hele serveren",
- "lifetimePurchase": "Livstidskøb",
- "supporterStatus": "Supporterstatus",
- "buy": "Køb",
- "supportKeyOptionLimited": "Begrænset supporter",
- "forFiveUsers": "For 5 eller færre brugere",
- "supportKeyRedeem": "Løs ind supporternøgle",
- "supportKeyHideSevenDays": "Skjul i 7 dage",
- "supportKeyEnter": "Indtast supporternøgle",
- "supportKeyEnterDescription": "Møt din helt egen kæledyr-Pangolin!",
- "githubUsername": "GitHub-brugernavn",
- "supportKeyInput": "Supporternøgle",
- "supportKeyBuy": "Køb supporternøgle",
- "logoutError": "Fejl ved logout",
- "signingAs": "Logget ind som",
- "serverAdmin": "Serveradministrator",
- "managedSelfhosted": "Administreret selvhostet",
- "otpEnable": "Aktivér tofaktor",
- "otpDisable": "Deaktivér tofaktor",
- "logout": "Log ud",
- "licenseTierProfessionalRequired": "Professionel udgave påkrævet",
- "licenseTierProfessionalRequiredDescription": "Denne funktion er kun tilgængelig i den professionelle udgave.",
- "actionGetOrg": "Hent organisation",
- "updateOrgUser": "Opdater organisationsbruger",
- "createOrgUser": "Opret organisationsbruger",
- "actionUpdateOrg": "Opdater organisation",
- "actionRemoveInvitation": "Fjern invitation",
- "actionUpdateUser": "Opdater bruger",
- "actionGetUser": "Hent bruger",
- "actionGetOrgUser": "Hent organisationsbruger",
- "actionListOrgDomains": "Vis organisationsdomæner",
- "actionGetDomain": "Hent domæne",
- "actionCreateOrgDomain": "Opret domæne",
- "actionUpdateOrgDomain": "Opdater domæne",
- "actionDeleteOrgDomain": "Slet domæne",
- "actionGetDNSRecords": "Hent DNS-poster",
- "actionRestartOrgDomain": "Genstart Domæne",
- "actionCreateSite": "Opret site",
- "actionDeleteSite": "Slet site",
- "actionGetSite": "Hent site",
- "actionListSites": "Vis sites",
- "actionApplyBlueprint": "Brug blueprint",
- "actionListBlueprints": "Vis blueprints",
- "actionGetBlueprint": "Hent blueprint",
- "actionCreateOrgWideLauncherView": "Opret org-dækkende launcher-visning",
- "setupToken": "Opsætningstoken",
- "setupTokenDescription": "Indtast opsætningstoken fra serverkonsollen.",
- "setupTokenRequired": "Opsætningstoken er nødvendig",
- "actionUpdateSite": "Opdater site",
- "actionResetSiteBandwidth": "Nulstil organisationsbåndbredde",
- "actionListSiteRoles": "Vis tilladte områderoller",
- "actionCreateResource": "Opret ressource",
- "actionDeleteResource": "Slet ressource",
- "actionGetResource": "Hent ressource",
- "actionListResource": "Vis ressourcer",
- "actionUpdateResource": "Opdater ressource",
- "actionListResourceUsers": "Vis ressourcebrugere",
- "actionSetResourceUsers": "Angiv ressourcebrugere",
- "actionSetAllowedResourceRoles": "Angiv tilladte ressourceroller",
- "actionListAllowedResourceRoles": "Vis tilladte ressourceroller",
- "actionSetResourcePassword": "Angiv ressourceadgangskode",
- "actionSetResourcePincode": "Angiv ressource-PIN-kode",
- "actionSetResourceEmailWhitelist": "Angiv e-mailwhitelist for ressource",
- "actionGetResourceEmailWhitelist": "Hent e-mailwhitelist for ressource",
- "actionGetResourcePolicy": "Få ressourceretningslinjen",
- "actionUpdateResourcePolicy": "Opdater ressourceretningslinjen",
- "actionSetResourcePolicyUsers": "Angiv ressourceretningslinjens brugere",
- "actionSetResourcePolicyRoles": "Angiv ressourceretningslinjens roller",
- "actionSetResourcePolicyPassword": "Angiv ressourceretningslinjen for adgangskode",
- "actionSetResourcePolicyPincode": "Angiv ressourceregler for PIN-kode",
- "actionSetResourcePolicyHeaderAuth": "Angiv ressourceretningslinjens header-authentifikation",
- "actionSetResourcePolicyWhitelist": "Angiv ressourceretningslinjen for e-mail whitelist",
- "actionSetResourcePolicyRules": "Angiv ressourceretningslinjens regler",
- "actionCreateTarget": "Opret mål",
- "actionDeleteTarget": "Slet mål",
- "actionGetTarget": "Hent mål",
- "actionListTargets": "Vis mål",
- "actionUpdateTarget": "Opdater mål",
- "actionCreateRole": "Opret rolle",
- "actionDeleteRole": "Slet rolle",
- "actionGetRole": "Hent rolle",
- "actionListRole": "Vis roller",
- "actionUpdateRole": "Opdater rolle",
- "actionListAllowedRoleResources": "Vis tilladte rolleressourcer",
- "actionInviteUser": "Inviter bruger",
- "actionRemoveUser": "Fjern bruger",
- "actionListUsers": "Vis brugere",
- "actionAddUserRole": "Tilføj brugerrolle",
- "actionSetUserOrgRoles": "Angiv brugerroller",
- "actionGenerateAccessToken": "Generer adgangstoken",
- "actionDeleteAccessToken": "Slet adgangstoken",
- "actionListAccessTokens": "Vis adgangstokens",
- "actionCreateResourceSessionToken": "Opret ressourcessession token",
- "actionCreateResourceRule": "Opret ressourceregel",
- "actionDeleteResourceRule": "Slet ressourceregel",
- "actionListResourceRules": "Vis ressourceregler",
- "actionUpdateResourceRule": "Opdater ressourceregel",
- "actionListOrgs": "Vis organisationer",
- "actionCheckOrgId": "Tjek ID",
- "actionCreateOrg": "Opret organisation",
- "actionDeleteOrg": "Slet organisation",
- "actionListApiKeys": "Vis API-nøgler",
- "actionListApiKeyActions": "Vis API-nøglehandlinger",
- "actionSetApiKeyActions": "Angiv tilladte handlinger for API-nøgle",
- "actionCreateApiKey": "Opret API-nøgle",
- "actionDeleteApiKey": "Slet API-nøgle",
- "actionCreateIdp": "Opret IdP",
- "actionUpdateIdp": "Opdater IdP",
- "actionDeleteIdp": "Slet IdP",
- "actionListIdps": "Vis IdP'er",
- "actionGetIdp": "Hent IdP",
- "actionCreateIdpOrg": "Opret IdP-organisationspolitik",
- "actionDeleteIdpOrg": "Slet IdP-organisationspolitik",
- "actionListIdpOrgs": "Vis IdP-organisationer",
- "actionUpdateIdpOrg": "Opdater IdP-organisation",
- "actionCreateClient": "Opret Klient",
- "actionDeleteClient": "Slet klient",
- "actionArchiveClient": "Arkiver klient",
- "actionUnarchiveClient": "Fjern arkivering klient",
- "actionBlockClient": "Bloker kunde",
- "actionUnblockClient": "Fjern blokering af klient",
- "actionUpdateClient": "Opdater klient",
- "actionListClients": "Vis klienter",
- "actionGetClient": "Hent klient",
- "actionCreateSiteResource": "Opret siteressource",
- "actionDeleteSiteResource": "Slet Siteressource",
- "actionGetSiteResource": "Hent Siteressource",
- "actionListSiteResources": "Vis Siteressourcer",
- "actionUpdateSiteResource": "Opdater Siteressource",
- "actionListInvitations": "Vis invitationer",
- "actionExportLogs": "Eksportér logs",
- "actionViewLogs": "Vis logs",
- "noneSelected": "Ingen valgt",
- "orgNotFound2": "Ingen organisationer fundet.",
- "search": "Søg…",
- "searchPlaceholder": "Søg...",
- "emptySearchOptions": "Ingen valg fundet",
- "create": "Opret",
- "orgs": "Organisationer",
- "loginError": "Der opstod en uventet fejl. Prøv venligst igen.",
- "loginRequiredForDevice": "Login er påkrævet for din enhed.",
- "passwordForgot": "Glemt adgangskoden dit?",
- "otpAuth": "Tofaktorgodkendelse",
- "otpAuthDescription": "Indtast koden fra autentiseringsappen din eller en af dine engangs reservekoder.",
- "otpAuthSubmit": "Send kode",
- "idpContinue": "Eller fortsæt med",
- "otpAuthBack": "Tilbage til adgangskode",
- "navbar": "Navigationsmenu",
- "navbarDescription": "Hovednavigasjonsmeny for applikationen",
- "navbarDocsLink": "Dokumentation",
- "commandPaletteTitle": "Kommando-paletten",
- "commandPaletteDescription": "Søg efter sider, organisationer, ressourcer og handlinger",
- "commandPaletteSearchPlaceholder": "Søg i sider, ressourcer, handlinger...",
- "commandPaletteNoResults": "Ingen resultater fundet.",
- "commandPaletteSearching": "Søger...",
- "commandPaletteNavigation": "Navigation",
- "commandPaletteOrganizations": "Organisationer",
- "commandPaletteSites": "Websteder",
- "commandPaletteResources": "Ressourcer",
- "commandPaletteUsers": "Brugere",
- "commandPaletteClients": "Maskinklienter",
- "commandPaletteActions": "Handlinger",
- "commandPaletteCreateSite": "Opret site",
- "commandPaletteCreateProxyResource": "Opret offentlig ressource",
- "commandPaletteCreatePrivateResource": "Opret privat ressource",
- "commandPaletteCreateUser": "Opret bruger",
- "commandPaletteCreateApiKey": "Opret API-nøgle",
- "commandPaletteCreateMachineClient": "Opret maskinklient",
- "commandPaletteCreateAlertRule": "Opret varslingsregel",
- "commandPaletteCreateIdentityProvider": "Opret identitetsudbyder",
- "commandPaletteToggleTheme": "Skift tema",
- "commandPaletteChooseOrganization": "Vælg organisation",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
- "otpErrorEnable": "Kunne ikke aktivere 2FA",
- "otpErrorEnableDescription": "En fejl opstod under aktivering af 2FA",
- "otpSetupCheckCode": "Indtast venligst en 6-sifret kode",
- "otpSetupCheckCodeRetry": "Ugyldig kode. Prøv venligst igen.",
- "otpSetup": "Aktivér tofaktorgodkendelse",
- "otpSetupDescription": "Sikre din konto med et ekstra lag med beskyttelse",
- "otpSetupScanQr": "Skann denne QR-koden med autentiseringsappen din eller indtast den hemmelige nøgle manuelt:",
- "otpSetupSecretCode": "Godkendelseskode",
- "otpSetupSuccess": "Tofaktorgodkendelse aktiveret",
- "otpSetupSuccessStoreBackupCodes": "Din konto er nu sikrere. Ikke glem at gem reservekodene dine.",
- "otpErrorDisable": "Kunne ikke deaktivere 2FA",
- "otpErrorDisableDescription": "En fejl opstod under deaktivering af 2FA",
- "otpRemove": "Deaktivér tofaktorgodkendelse",
- "otpRemoveDescription": "Deaktivér tofaktorgodkendelse for din konto",
- "otpRemoveSuccess": "Tofaktorgodkendelse deaktiveret",
- "otpRemoveSuccessMessage": "Tofaktorgodkendelse er deaktiveret for din konto. Du kan aktivere den tilbage når som helst.",
- "otpRemoveSubmit": "Deaktivér 2FA",
- "paginator": "Side {current} af {last}",
- "paginatorToFirst": "Gå til første side",
- "paginatorToPrevious": "Gå til forrige side",
- "paginatorToNext": "Gå til næste side",
- "paginatorToLast": "Gå til sidste side",
- "copyText": "Kopiér tekst",
- "copyTextFailed": "Kunne ikke kopiere tekst: ",
- "copyTextClipboard": "Kopiér til udklipsholder",
- "inviteErrorInvalidConfirmation": "Ugyldig bekræftelse",
- "passwordRequired": "Adgangskode er påkrævet",
- "allowAll": "Tillad alle",
- "permissionsAllowAll": "Tillad alle rettigheter",
- "githubUsernameRequired": "GitHub-brugernavn er påkrævet",
- "supportKeyRequired": "supporternøgle er påkrævet",
- "passwordRequirementsChars": "Adgangskoden skal være mindst 8 tegn",
- "language": "Sprog",
- "verificationCodeRequired": "Kode er påkrævet",
- "userErrorNoUpdate": "Ingen bruger at opdatere",
- "siteErrorNoUpdate": "Ingen site at opdatere",
- "resourceErrorNoUpdate": "Ingen ressource at opdatere",
- "authErrorNoUpdate": "Ingen autentificeringsinfo at opdatere",
- "orgErrorNoUpdate": "Ingen organisation at opdatere",
- "orgErrorNoProvided": "Ingen organisation angivet",
- "apiKeysErrorNoUpdate": "Ingen API-nøgle at opdatere",
- "sidebarOverview": "Oversigt",
- "sidebarHome": "Hjem",
- "sidebarSites": "Websteder",
- "sidebarApprovals": "Godkendelsesanmodninger",
- "sidebarResources": "Ressourcer",
- "sidebarProxyResources": "Offentlig",
- "sidebarClientResources": "Privat",
- "sidebarPolicies": "Delte politikker",
- "sidebarResourcePolicies": "Offentlige ressourcer",
- "sidebarAccessControl": "Adgangskontrol",
- "sidebarLogsAndAnalytics": "Logs og analyser",
- "sidebarTeam": "Lag",
- "sidebarUsers": "Brugere",
- "sidebarAdmin": "Administrator",
- "sidebarInvitations": "Invitationer",
- "sidebarRoles": "Roller",
- "sidebarShareableLinks": "Delbare links",
- "sidebarApiKeys": "API-nøgler",
- "sidebarProvisioning": "Provisionering",
- "sidebarSettings": "Indstillinger",
- "sidebarAllUsers": "Alle brugere",
- "sidebarIdentityProviders": "Identitetsudbydere",
- "sidebarLicense": "Licens",
- "sidebarClients": "Klienter",
- "sidebarUserDevices": "Bruger Enheder",
- "sidebarMachineClients": "Maskiner",
- "sidebarDomains": "Domæner",
- "sidebarGeneral": "Administrer",
- "sidebarLogAndAnalytics": "Logs og analyser",
- "sidebarBluePrints": "Blueprints",
- "sidebarAlerting": "Varsling",
- "sidebarHealthChecks": "Sundhedstjek",
- "sidebarOrganization": "Organisation",
- "sidebarManagement": "Administration",
- "sidebarBillingAndLicenses": "Fakturering & licenser",
- "sidebarLogsAnalytics": "Analyser",
- "commandSites": "Websteder",
- "commandActionModeInfo": "Skriv \">\" for at åbne handlingsmode",
- "commandResources": "Ressourcer",
- "commandProxyResources": "Offentlige ressourcer",
- "commandClientResources": "Private ressourcer",
- "commandClients": "Klienter",
- "commandUserDevices": "Brugerenheder",
- "commandMachineClients": "Maskinklienter",
- "commandDomains": "Domæner",
- "commandRemoteExitNodes": "Eksterne noder",
- "commandTeam": "Team",
- "commandUsers": "Brugere",
- "commandRoles": "Roller",
- "commandInvitations": "Invitationer",
- "commandPolicies": "Delte politikker",
- "commandResourcePolicies": "Offentlige ressourcepolitikker",
- "commandIdentityProviders": "Identitetsudbydere",
- "commandApprovals": "Godkendelsesanmodninger",
- "commandShareableLinks": "Delbare links",
- "commandOrganization": "Organisation",
- "commandLogsAndAnalytics": "Logs og analyser",
- "commandLogsAnalytics": "Analyser",
- "commandLogsRequest": "HTTP-forespørgselslogs",
- "commandLogsAccess": "Adgangslogs",
- "commandLogsAction": "Handlingsloger",
- "commandLogsConnection": "Forbindelseslogs",
- "commandLogsStreaming": "Hændelsesstreaming",
- "commandManagement": "Administration",
- "commandAlerting": "Varsling",
- "commandProvisioning": "Provisionering",
- "commandBluePrints": "Blueprints",
- "commandApiKeys": "API-nøgler",
- "commandBillingAndLicenses": "Fakturering & licenser",
- "commandBilling": "Fakturering",
- "commandEnterpriseLicenses": "Licenser",
- "commandSettings": "Indstillinger",
- "commandLauncher": "Launcher",
- "commandResourceLauncher": "Ressource-launcher",
- "commandSearchResults": "Søgeresultater",
- "alertingTitle": "Varsling",
- "alertingDescription": "Definer kilder, triggere og handlinger for varsler",
- "alertingRules": "Varslingsregler",
- "alertingSearchRules": "Søg i regler…",
- "alertingAddRule": "Opret regel",
- "alertingColumnSource": "Kilde",
- "alertingColumnTrigger": "Udløser",
- "alertingColumnActions": "Handlinger",
- "alertingColumnEnabled": "Aktiveret",
- "alertingDeleteQuestion": "Bekræft venligst, at du vil slette denne varslingsregel.",
- "alertingDeleteRule": "Slet varslingsregel",
- "alertingRuleDeleted": "Varslingsregel slettet",
- "alertingRuleSaved": "Varslingsregel gemt",
- "alertingRuleSavedCreatedDescription": "Din nye varslingsregel blev oprettet. Du kan fortsætte at redigere den på denne side.",
- "alertingRuleSavedUpdatedDescription": "Dine ændringer i denne varslingsreglen blev gemt.",
- "alertingEditRule": "Rediger varslingsregel",
- "alertingCreateRule": "Opret varslingsregel",
- "alertingRuleCredenzaDescription": "Vælg hvad som skal overvåges, når det skal varsles, og hvordan du vil blive informert",
- "alertingRuleNamePlaceholder": "Produktionssite nede",
- "alertingRuleEnabled": "Regel aktiveret",
- "alertingSectionSource": "Kilde",
- "alertingSourceType": "Kildetype",
- "alertingSourceSite": "Sted",
- "alertingSourceHealthCheck": "Sundhedstjek",
- "alertingPickSites": "Steder",
- "alertingPickHealthChecks": "Sundhedstjek",
- "alertingPickResources": "Ressourcer",
- "alertingAllSites": "Alle sites",
- "alertingAllSitesDescription": "Varsler for alle sites",
- "alertingSpecificSites": "Specifikke sites",
- "alertingSpecificSitesDescription": "Vælg specifikke sites for overvågning",
- "alertingAllHealthChecks": "Alle sundhedstjek",
- "alertingAllHealthChecksDescription": "Varsler for alle sundhedstjek",
- "alertingSpecificHealthChecks": "Specifikke sundhedstjek",
- "alertingSpecificHealthChecksDescription": "Vælg specifikke sundhedstjek for overvågning",
- "alertingAllResources": "Alle ressourcer",
- "alertingAllResourcesDescription": "Varsler for alle ressourcer",
- "alertingSpecificResources": "Specifikke ressourcer",
- "alertingSpecificResourcesDescription": "Vælg specifikke ressourcer for overvågning",
- "alertingSelectResources": "Vælg ressourcer…",
- "alertingResourcesSelected": "{count} ressourcer valgt",
- "alertingResourcesEmpty": "Ingen ressourcer med mål i de første 10 resultaterne.",
- "alertingSectionTrigger": "Udløser",
- "alertingTrigger": "Når skal det varsles",
- "alertingTriggerSiteOnline": "Site er online",
- "alertingTriggerSiteOffline": "Site er offline",
- "alertingTriggerSiteToggle": "Ændringer i sitestatus",
- "alertingTriggerHcHealthy": "Sundhedstjek sund",
- "alertingTriggerHcUnhealthy": "Sundhedstjek usund",
- "alertingTriggerHcToggle": "Ændringer i sundhedstjekstatus",
- "alertingTriggerResourceHealthy": "Ressource sund",
- "alertingTriggerResourceUnhealthy": "Ressource usund",
- "alertingTriggerResourceDegraded": "Ressource forringet",
- "alertingSearchHealthChecks": "Søg i sundhedstjek…",
- "alertingHealthChecksEmpty": "Ingen tilgængelige sundhedstjek.",
- "alertingTriggerResourceToggle": "Ændringer i ressourcestatus",
- "alertingSourceResource": "Ressource",
- "alertingSectionActions": "Handlinger",
- "alertingAddAction": "Tilføj handling",
- "alertingActionNotify": "E-mail",
- "alertingActionNotifyDescription": "Send e-mailvarsler til brugere eller roller",
- "alertingActionWebhook": "Webhook",
- "alertingActionWebhookDescription": "Send en HTTP-forespørgsel til et tilpasset endpoint",
- "alertingExternalIntegration": "Ekstern integration",
- "alertingExternalPagerDutyDescription": "Send varsler til PagerDuty for hændelseshåndtering",
- "alertingExternalOpsgenieDescription": "Rute varsler til Opsgenie for vagthåndtering",
- "alertingExternalServiceNowDescription": "Opret ServiceNow hændelser fra varslingshændelser",
- "alertingExternalIncidentIoDescription": "Utløs Incident.io arbejdsgange fra varsels hændelser",
- "alertingActionType": "Handlings type",
- "alertingNotifyUsers": "Brugere",
- "alertingNotifyRoles": "Roller",
- "alertingNotifyEmails": "E-mailadresser",
- "alertingEmailPlaceholder": "Tilføj e-mail og trykk Enter",
- "alertingWebhookMethod": "HTTP-metode",
- "alertingWebhookSecret": "Signeringshemmelighed (valgfrit)",
- "alertingWebhookSecretPlaceholder": "HMAC-hemmelig",
- "alertingWebhookHeaders": "Overskrifter",
- "alertingAddHeader": "Tilføj header",
- "alertingSelectSites": "Vælg sites…",
- "alertingSitesSelected": "{count} sites valgt",
- "alertingSelectHealthChecks": "Vælg sundhedstjek…",
- "alertingHealthChecksSelected": "{count} sundhedstjek valgt",
- "alertingNoHealthChecks": "Ingen mål med sundhedstjek aktiveret",
- "alertingHealthCheckStub": "Valg af sundhedstjekkilde er ikke færdiggjort endnu - du kan stadig konfigurere triggere og handlinger.",
- "alertingSelectUsers": "Vælg brugere…",
- "alertingUsersSelected": "{count} brugere valgt",
- "alertingSelectRoles": "Vælg roller…",
- "alertingRolesSelected": "{count} roller valgt",
- "alertingSummarySites": "Websteder ({count})",
- "alertingSummaryAllSites": "Alle sites",
- "alertingSummaryHealthChecks": "Sundhedstjek ({count})",
- "alertingSummaryAllHealthChecks": "Alle sundhedstjek",
- "alertingSummaryResources": "Ressourcer ({count})",
- "alertingSummaryAllResources": "Alle ressourcer",
- "alertingErrorNameRequired": "Indtast et navn",
- "alertingErrorActionsMin": "Tilføj mindst én handling",
- "alertingErrorPickSites": "Vælg mindst ét site",
- "alertingErrorPickHealthChecks": "Vælg mindst én sundhedstjek",
- "alertingErrorPickResources": "Vælg mindst én ressource",
- "alertingErrorTriggerSite": "Vælg en trigger for site",
- "alertingErrorTriggerHealth": "Vælg en trigger for sundhedstjek",
- "alertingErrorTriggerResource": "Vælg en trigger for ressource",
- "alertingErrorNotifyRecipients": "Vælg brugere, roller, eller mindst én e-mail",
- "alertingConfigureSource": "Konfigurer kilde",
- "alertingConfigureTrigger": "Konfigurer trigger",
- "alertingConfigureActions": "Konfigurer handlinger",
- "alertingBackToRules": "Tilbage til regler",
- "alertingRuleCooldown": "Cooldown (sekunder)",
- "alertingRuleCooldownDescription": "Minimumstid mellem gentagne varsler for samme regel. Sæt til 0 for at udløse hver gang.",
- "alertingDraftBadge": "Kladde - gem for at gem denne reglen",
- "alertingSidebarHint": "Klik på et steg på lærredet for at redigere det her.",
- "alertingGraphCanvasTitle": "Regelflow",
- "alertingGraphCanvasDescription": "Visuel oversigt over kilde, trigger og handlinger. Vælg en node for at redigere den i panelet.",
- "alertingNodeNotConfigured": "Ikke konfigureret endnu",
- "alertingNodeActionsCount": "{count, plural, one {# handling} other {# handlinger}}",
- "alertingNodeRoleSource": "Kilde",
- "alertingNodeRoleTrigger": "Udløser",
- "alertingNodeRoleAction": "Handling",
- "alertingTabRules": "Varslingsregler",
- "alertingTabHealthChecks": "Sundhedstjek",
- "alertingRulesBannerTitle": "Bli varslet",
- "alertingRulesBannerDescription": "Hver regel binder sammen hvad som skal overvåges (et site, sundhedstjek eller ressource), når det skal varsles (for eksempel offline eller usund), og hvordan varsle teamet dit via e-mail, webhooks eller integrationer. Brug denne listen for at oprette, aktivere og administrer disse reglerne.",
- "alertingHealthChecksBannerTitle": "Overvåg sundhed & ressourcer",
- "alertingHealthChecksBannerDescription": "Sundhedstjek er HTTP- eller TCP-monitorer, du definerer én gang. Du kan derefter bruge dem som kilder i varslingsregler, så du bliver varslet, når et mål bliver sundt eller usundt. Sundhedstjek på ressourcer vises også her.",
- "standaloneHcTableTitle": "Sundhedstjek",
- "standaloneHcSearchPlaceholder": "Søg i sundhedstjek…",
- "standaloneHcAddButton": "Opret sundhedstjek",
- "standaloneHcCreateTitle": "Opret sundhedstjek",
- "standaloneHcEditTitle": "Rediger sundhedstjek",
- "standaloneHcDescription": "Konfigurer en HTTP- eller TCP-sundhedstjek for brug i varslingsregler.",
- "standaloneHcNameLabel": "Navn",
- "standaloneHcNamePlaceholder": "Min HTTP-monitor",
- "standaloneHcDeleteTitle": "Slet sundhedstjek",
- "standaloneHcDeleteQuestion": "Bekræft venligst, at du vil slette dette sundhedstjek.",
- "standaloneHcDeleted": "Sundhedstjek slettet",
- "standaloneHcSaved": "Sundhedstjek gemt",
- "standaloneHcColumnHealth": "Sundhed",
- "standaloneHcColumnMode": "Tilstand",
- "standaloneHcColumnTarget": "Mål",
- "standaloneHcHealthStateHealthy": "Sund",
- "standaloneHcHealthStateUnhealthy": "Usund",
- "standaloneHcHealthStateUnknown": "Ukendt",
- "standaloneHcFilterAnySite": "Alle sites",
- "standaloneHcFilterAnyResource": "Alle ressourcer",
- "standaloneHcFilterMode": "Tilstand",
- "standaloneHcFilterModeHttp": "HTTP",
- "standaloneHcFilterModeTcp": "TCP",
- "standaloneHcFilterModeSnmp": "SNMP",
- "standaloneHcFilterModePing": "Ping",
- "standaloneHcFilterHealth": "Sundhed",
- "standaloneHcFilterEnabled": "Aktiveret",
- "standaloneHcFilterEnabledOn": "Aktiveret",
- "standaloneHcFilterEnabledOff": "Deaktiveret",
- "standaloneHcFilterSiteIdFallback": "Sted {id}",
- "standaloneHcFilterResourceIdFallback": "Ressource {id}",
- "blueprints": "Blueprints",
- "blueprintsLog": "Blueprint-log",
- "blueprintsDescription": "Se tidligere blueprint-applikationer og deres resultater, eller brug et nyt blueprint",
- "blueprintAdd": "Tilføj blueprint",
- "blueprintGoBack": "Se alle blueprints",
- "blueprintCreate": "Oprette skabelon",
- "blueprintCreateDescription2": "Følg trinene nedenfor for at oprette og bruge en ny blueprint",
- "blueprintDetails": "Blueprint detaljer",
- "blueprintDetailsDescription": "Se resultatet af den påførte blåkopien og alle fejl som opstod",
- "blueprintInfo": "Blueprint information",
- "message": "Meddelelse",
- "blueprintContentsDescription": "Definere indholdet til YAML som beskriver infrastrukturen",
- "blueprintErrorCreateDescription": "Det opstod en fejl da blueprintet blev tilføjet",
- "blueprintErrorCreate": "Fejl ved oprettelse af blueprint",
- "searchBlueprintProgress": "Søg efter plantegninger...",
- "appliedAt": "Anvendt den",
- "source": "Kilde",
- "contents": "Indhold",
- "parsedContents": "Parset indhold (skrivebeskyttet)",
- "enableDockerSocket": "Aktivér Docker-blueprint",
- "enableDockerSocketDescription": "Aktivér Docker Socket etikett skrubbing for blueprint etiketter. Socket sti skal angives til site-connectoren. Læs om hvordan dette fungerer i dokumentasjonen.",
- "newtAutoUpdate": "Aktivér Automatisk Opdatering af Site",
- "newtAutoUpdateDescription": "Når dette er aktiveret, vil site-connectorer automatisk downloade den nyeste version og genstarte sig selv. Dette kan tilsidesættes pr. site.",
- "siteAutoUpdate": "Automatisk Opdatering af Site",
- "siteAutoUpdateLabel": "Aktivér Automatisk Opdatering",
- "siteAutoUpdateDescription": "Når dette er aktiveret, vil dette sites connector automatisk downloade den nyeste version og genstarte sig selv.",
- "siteAutoUpdateOrgDefault": "Organisation standard: {state}",
- "siteAutoUpdateOverriding": "Overstyrer organisationens indstilling",
- "siteAutoUpdateResetToOrg": "Nulstil til Organisationsstandard",
- "siteAutoUpdateEnabled": "aktiveret",
- "siteAutoUpdateDisabled": "deaktiveret",
- "viewDockerContainers": "Vis Docker-containere",
- "containersIn": "Containere i {siteName}",
- "selectContainerDescription": "Vælg en hvilken som helst container for at bruge den som værtsnavn for dette mål. Klik på en port for at bruge den.",
- "containerName": "Navn",
- "containerImage": "Bilde",
- "containerState": "Tilstand",
- "containerNetworks": "Netværk",
- "containerHostnameIp": "Hostnavn/IP",
- "containerLabels": "Etiketter",
- "containerLabelsCount": "{count, plural, one {en etikett} other {# etiketter}}",
- "containerLabelsTitle": "Containeretiketter",
- "containerLabelEmpty": "",
- "containerPorts": "Porte",
- "containerPortsMore": "+{count} til",
- "containerActions": "Handlinger",
- "select": "Vælg",
- "noContainersMatchingFilters": "Ingen containere fundet som matcher de nuværende filtre.",
- "showContainersWithoutPorts": "Vis containere uden porte",
- "showStoppedContainers": "Vis stoppede containere",
- "noContainersFound": "Ingen containere fundet. Sørg for at Docker-containere kører.",
- "searchContainersPlaceholder": "Søg blandt {count} containere...",
- "searchResultsCount": "{count, plural, one {ét resultat} other {# resultater}}",
- "filters": "Filtre",
- "filterOptions": "Filterindstillinger",
- "filterPorts": "Porte",
- "filterStopped": "Stoppet",
- "clearAllFilters": "Ryd alle filtre",
- "columns": "Kolonner",
- "toggleColumns": "Vis/skjul kolonner",
- "refreshContainersList": "Opdater containerliste",
- "searching": "Søger...",
- "noContainersFoundMatching": "Ingen containere fundet som matcher \"{filter}\".",
- "light": "lys",
- "dark": "mørk",
- "system": "system",
- "theme": "Tema",
- "subnetRequired": "Subnet er påkrævet",
- "initialSetupTitle": "Førstegangsopsætning af server",
- "initialSetupDescription": "Opret den første serveradministratorkontoen. Det kan kun findes én serveradministrator. Du kan altid ændre denne logininformationen senere.",
- "createAdminAccount": "Opret administratorkonto",
- "setupErrorCreateAdmin": "En fejl opstod under oprettelsen af serveradministratorkontoen.",
- "certificateStatus": "Certifikat",
- "certificateStatusAutoRefreshHint": "Status opdateres automatisk.",
- "loading": "Indlæser",
- "loadingEllipsis": "Indlæser...",
- "loadingAnalytics": "Indlæser analyser",
- "restart": "Genstart",
- "domains": "Domæner",
- "domainsDescription": "Opret og administrer domæner som er tilgængelige i organisationen",
- "domainsSearch": "Søg i domæner...",
- "domainAdd": "Tilføj domæne",
- "domainAddDescription": "Registrer et nyt domæne med organisationen",
- "domainCreate": "Opret domæne",
- "domainCreatedDescription": "Domæne blev oprettet",
- "domainDeletedDescription": "Domæne blev slettet",
- "domainQuestionRemove": "Er du sikker på at du vil fjerne domænet?",
- "domainMessageRemove": "Når domænet er fjernet, vil det ikke længere være forbundet med organisationen.",
- "domainConfirmDelete": "Bekræft sletning af domæne",
- "domainDelete": "Slet domæne",
- "domain": "Domæne",
- "selectDomainTypeNsName": "Domenedelegering (NS)",
- "selectDomainTypeNsDescription": "Dette domænet og alle dets underdomæner. Brug dette når du vil kontrollere en hel domænezone.",
- "selectDomainTypeCnameName": "Enkelt domæne (CNAME)",
- "selectDomainTypeCnameDescription": "Kun dette specifikke domænet. Brug dette for individuelle underdomæner eller specifikke domæneposter.",
- "selectDomainTypeWildcardName": "Wildcard-domæne",
- "selectDomainTypeWildcardDescription": "Dette domænet og dets underdomæner.",
- "domainDelegation": "Enkelt domæne",
- "selectType": "Vælg en type",
- "actions": "Handlinger",
- "refresh": "Opdater",
- "refreshError": "Kunne ikke opdatere data",
- "verified": "Verificeret",
- "pending": "Afventer",
- "pendingApproval": "Venter på godkendelse",
- "sidebarBilling": "Fakturering",
- "billing": "Fakturering",
- "orgBillingDescription": "Administrer faktureringsinformation og abonnementer",
- "github": "GitHub",
- "pangolinHosted": "Hostet af Pangolin",
- "fossorial": "Fossorial",
- "completeAccountSetup": "Fuldfør kontoopsætning",
- "completeAccountSetupDescription": "Angiv adgangskoden dit for at komme i gang",
- "accountSetupSent": "Vi sender en opsætningskode for kontoen til denne e-mailadressen.",
- "accountSetupCode": "Opsætningskode",
- "accountSetupCodeDescription": "Tjek din e-mail for opsætningskoden.",
- "passwordCreate": "Opret adgangskode",
- "passwordCreateConfirm": "Bekræft adgangskode",
- "accountSetupSubmit": "Send opsætningskode",
- "completeSetup": "Fuldfør opsætning",
- "accountSetupSuccess": "Kontoopsætning fuldført! Velkommen til Pangolin!",
- "documentation": "Dokumentation",
- "saveAllSettings": "Gem alle indstillinger",
- "saveResourceTargets": "Gem indstillinger",
- "saveResourceHttp": "Gem indstillinger",
- "saveProxyProtocol": "Gem indstillinger",
- "settingsUpdated": "Indstillinger opdateret",
- "settingsUpdatedDescription": "Indstillinger opdateret med succes",
- "settingsErrorUpdate": "Kunne ikke opdatere indstillinger",
- "settingsErrorUpdateDescription": "En fejl opstod under opdatering af indstillinger",
- "sidebarCollapse": "Skjul",
- "sidebarExpand": "Udvid",
- "productUpdateMoreInfo": "{noOfUpdates} flere opdateringer",
- "productUpdateInfo": "{noOfUpdates} opdateringer",
- "productUpdateWhatsNew": "Hvad er nyt",
- "productUpdateTitle": "Opdateringer om produktet",
- "productUpdateEmpty": "Ingen opdateringer",
- "dismissAll": "Afvis alle",
- "pangolinUpdateAvailable": "Opdatering tilgængelig",
- "pangolinUpdateAvailableInfo": "Version {version} er klar til at installere",
- "pangolinUpdateAvailableReleaseNotes": "Se utgivelsesnotater",
- "newtUpdateAvailable": "Opdatering tilgængelig",
- "newtUpdateAvailableInfo": "En ny version af Newt er tilgængeligt. Opdater venligst til den nyeste version for den bedste oplevelse.",
- "pangolinNodeUpdateAvailableInfo": "En ny version af Pangolin Node er tilgængeligt. Opdater venligst til den nyeste version for den bedste oplevelse.",
- "domainPickerEnterDomain": "Domæne",
- "domainPickerPlaceholder": "minapp.eksempel.no",
- "domainPickerDescription": "Indtast hele domænet til ressourcen for at se tilgængelige alternativer.",
- "domainPickerDescriptionSaas": "Indtast et fullt domæne, underdomæne eller kun et navn for at se tilgængelige alternativer",
- "domainPickerTabAll": "Alle",
- "domainPickerTabOrganization": "Organisation",
- "domainPickerTabProvided": "Levert",
- "domainPickerSortAsc": "A-AT",
- "domainPickerSortDesc": "AT-A",
- "domainPickerCheckingAvailability": "Kontrollerer tilgængelighed...",
- "domainPickerNoMatchingDomains": "Ingen matchende domæner fundet. Prøv et andet domæne, eller kontroller organisationens domæneindstillinger.",
- "domainPickerOrganizationDomains": "Organisationsdomæner",
- "domainPickerProvidedDomains": "Leverte domæner",
- "domainPickerSubdomain": "Underdomæne: {subdomain}",
- "domainPickerNamespace": "Navnerom: {namespace}",
- "domainPickerShowMore": "Vis mere",
- "regionSelectorTitle": "Vælg Region",
- "domainPickerRemoteExitNodeWarning": "Leverede domæner understøttes ikke, når sites kobles til eksterne exitnoder. For ressourcer, der skal være tilgængelige på eksterne noder, skal du i stedet bruge et brugerdefineret domæne.",
- "regionSelectorInfo": "At vælge en region hjelper oss med at give bedre ydeevne for din placering. Du behøver ikke være i samme region som serveren.",
- "regionSelectorPlaceholder": "Vælg en region",
- "regionSelectorComingSoon": "Kommer snart",
- "billingLoadingSubscription": "Indlæser abonnement...",
- "billingFreeTier": "Gratis nivå",
- "billingWarningOverLimit": "Advarsel: Du har overskredet en eller flere forbrugsgrænser. Dine sites vil ikke opret forbindelse til før du ændrer dit abonnement eller justerer forbruget.",
- "billingUsageLimitsOverview": "Oversikt over forbrugsgrænser",
- "billingMonitorUsage": "Overvåg forbruget din i forhold til konfigurerte grænse. Hvis du behøver økte grænse, venligst kontakt support@pangolin.net.",
- "billingDataUsage": "Dataforbrug",
- "billingSites": "Websteder",
- "billingUsers": "Brugere",
- "billingDomains": "Domæner",
- "billingOrganizations": "Orger",
- "billingRemoteExitNodes": "Eksterne noder",
- "billingPublicResources": "Offentlige ressourcer",
- "billingPrivateResources": "Private ressourcer",
- "billingMachineClients": "Maskinklienter",
- "billingNoLimitConfigured": "Ingen grænse konfigureret",
- "billingEstimatedPeriod": "Estimert faktureringsperiode",
- "billingIncludedUsage": "Inklusive Brug",
- "billingIncludedUsageDescription": "Brug inklusive i din nuværende abonnementsplan",
- "billingFreeTierIncludedUsage": "Gratis nivå forbrugsgrænser",
- "billingIncluded": "inklusive",
- "billingEstimatedTotal": "Estimert Totalt:",
- "billingNotes": "Notater",
- "billingEstimateNote": "Dette er et estimat baseret på din nuværende brug.",
- "billingActualChargesMayVary": "Faktiske omkostninger kan variere.",
- "billingBilledAtEnd": "Du vil blive fakturert ved slutten af faktureringsperioden.",
- "billingModifySubscription": "Ændre abonnement",
- "billingStartSubscription": "Start abonnement",
- "billingRecurringCharge": "Indgående Avgift",
- "billingManageSubscriptionSettings": "Administrer abonnementsindstillinger",
- "billingNoActiveSubscription": "Du har ikke et aktivt abonnement. Start dit abonnement for at øge bruksgrensene.",
- "billingFailedToLoadSubscription": "Kunne ikke indlæse abonnement",
- "billingFailedToLoadUsage": "Kunne ikke indlæse bruksdata",
- "billingFailedToGetCheckoutUrl": "Mislykkedes at få betalingslenke",
- "billingPleaseTryAgainLater": "Prøv venligst igen senere.",
- "billingCheckoutError": "Kasserror",
- "billingFailedToGetPortalUrl": "Mislykkedes at hente portal URL",
- "billingPortalError": "Portalfeil",
- "billingDataUsageInfo": "Du bliver opkrævet for al data, der overføres gennem dine sikre tunneler, når du er forbundet til skyen. Dette inkluderer både indgående og udgående trafik på alle dine sites. Når du når din grænse, vil dine sites blive frakoblet, indtil du opgraderer din plan eller reducerer forbruget. Data belastes ikke ved brug af exitnode-grupper.",
- "billingSInfo": "Hvor mange sites du kan bruge",
- "billingUsersInfo": "Hvor mange brugere du kan bruge",
- "billingDomainInfo": "Hvor mange domæner du kan bruge",
- "billingRemoteExitNodesInfo": "Hvor mange fjernnoder du kan bruge",
- "billingPublicResourcesInfo": "Hvor mange offentlige ressourcer du kan bruge",
- "billingPrivateResourcesInfo": "Hvor mange private ressourcer du kan bruge",
- "billingMachineClientsInfo": "Hvor mange maskinklienter du kan bruge",
- "billingLicenseKeys": "Licensnøgler",
- "billingLicenseKeysDescription": "Administrer dine licensnøgleabonnementer",
- "billingLicenseSubscription": "Licens abonnement",
- "billingInactive": "Inaktiv",
- "billingLicenseItem": "Licens artikkel",
- "billingQuantity": "Antal",
- "billingTotal": "totalt",
- "billingModifyLicenses": "Ændre licensabonnement",
- "domainNotFound": "Domæne ikke fundet",
- "domainNotFoundDescription": "Denne ressource er deaktiveret fordi domænet ikke længere findes i systemet vores. Venligst angiv et nyt domæne for denne ressource.",
- "failed": "Mislykkedes",
- "createNewOrgDescription": "Opret en ny organisation",
- "organization": "Organisation",
- "primary": "Primær",
- "port": "Port",
- "securityKeyManage": "Administrer sikkerhedsnøgler",
- "securityKeyDescription": "Tilføj eller fjern sikkerhedsnøgler for adgangskodeløs autentificering",
- "securityKeyRegister": "Registrer ny sikkerhedsnøgle",
- "securityKeyList": "Dine sikkerhedsnøgler",
- "securityKeyNone": "Ingen sikkerhedsnøgler er registreret endnu",
- "securityKeyNameRequired": "Navn er påkrævet",
- "securityKeyRemove": "Fjern",
- "securityKeyLastUsed": "Sist brugt: {date}",
- "securityKeyNameLabel": "Navn på sikkerhedsnøgle",
- "securityKeyRegisterSuccess": "Sikkerhedsnøgle registreret",
- "securityKeyRegisterError": "Kunne ikke registrere sikkerhedsnøgle",
- "securityKeyRemoveSuccess": "Sikkerhedsnøgle fjernet",
- "securityKeyRemoveError": "Kunne ikke fjerne sikkerhedsnøgle",
- "securityKeyLoadError": "Kunne ikke indlæse ind sikkerhedsnøgler",
- "securityKeyLogin": "Brug sikkerhedsnøgle",
- "securityKeyAuthError": "Kunne ikke autentificere med sikkerhedsnøgle",
- "securityKeyRecommendation": "Registrer en reserve-sikkerhedsnøgle på en anden enhed for at sikre at du altid har adgang til din konto.",
- "registering": "Registrerer...",
- "securityKeyPrompt": "Venligst bekræft din identitet med sikkerhedsnøglen. Sørg for at sikkerhedsnøglen er forbundet til og klar.",
- "securityKeyBrowserNotSupported": "Browseren din understøtter ikke sikkerhedsnøgler. Venligst brug en moderne browser som Chrome, Firefox eller Safari.",
- "securityKeyPermissionDenied": "Tillad venligst adgang til din sikkerhedsnøgle for at fortsætte loginprocessen.",
- "securityKeyRemovedTooQuickly": "Hold venligst sikkerhedsnøglen tilsluttet, indtil loginprocessen er fuldført.",
- "securityKeyNotSupported": "Sikkerhedsnøglen din er muligvis ikke kompatibel. Venligst prøv en anden sikkerhedsnøgle.",
- "securityKeyUnknownError": "Der opstod et problem med at bruge din sikkerhedsnøgle. Prøv venligst igen.",
- "twoFactorRequired": "Tofaktorgodkendelse er påkrævet for at registrere en sikkerhedsnøgle.",
- "twoFactor": "Tofaktorgodkendelse",
- "twoFactorAuthentication": "To-faktor autentificering",
- "twoFactorDescription": "Denne organisation kræver to-faktor-autentificering.",
- "enableTwoFactor": "Aktivér to-faktor autentificering",
- "organizationSecurityPolicy": "Politikker for organisations sikkerhed",
- "organizationSecurityPolicyDescription": "Denne organisation har sikkerhedskrav som skal opfyldes før du får adgang til den",
- "securityRequirements": "Krav Til Sikkerhed",
- "allRequirementsMet": "Alle krav er opfyldt",
- "completeRequirementsToContinue": "Fuldfør kravene nedenfor for at fortsætte adgangen til denne organisation",
- "youCanNowAccessOrganization": "Du har nu adgang til denne organisation",
- "reauthenticationRequired": "Økt lengde",
- "reauthenticationDescription": "Denne organisation kræver at du logs på alle {maxDays} dage.",
- "reauthenticationDescriptionHours": "Denne organisation kræver at du logs ind hver {maxHours} time.",
- "reauthenticateNow": "Log ind tilbage",
- "adminEnabled2FaOnYourAccount": "Din administrator har aktiveret tofaktorgodkendelse for {email}. Fuldfør venligst opsætningsprocessen for at fortsætte.",
- "securityKeyAdd": "Tilføj sikkerhedsnøgle",
- "securityKeyRegisterTitle": "Registrer ny sikkerhedsnøgle",
- "securityKeyRegisterDescription": "Opret forbindelse til sikkerhedsnøglen og indtast et navn for at identifisere den",
- "securityKeyTwoFactorRequired": "Tofaktorgodkendelse påkrævet",
- "securityKeyTwoFactorDescription": "Indtast venligst koden for tofaktorgodkendelse for at registrere sikkerhedsnøglen",
- "securityKeyTwoFactorRemoveDescription": "Indtast venligst koden for tofaktorgodkendelse for at fjerne sikkerhedsnøglen",
- "securityKeyTwoFactorCode": "Tofaktorkode",
- "securityKeyRemoveTitle": "Fjern sikkerhedsnøgle",
- "securityKeyRemoveDescription": "Indtast adgangskoden dit for at fjerne sikkerhedsnøglen \"{name}\"",
- "securityKeyNoKeysRegistered": "Ingen sikkerhedsnøgler registreret",
- "securityKeyNoKeysDescription": "Tilføj en sikkerhedsnøgle for at øge sikkerheden for din konto",
- "createDomainRequired": "Domæne er påkrævet",
- "createDomainAddDnsRecords": "Tilføj DNS-poster",
- "createDomainAddDnsRecordsDescription": "Tilføj følgende DNS-poster hos din domæneudbyder for at fuldføre opsætningen.",
- "createDomainNsRecords": "NS-poster",
- "createDomainRecord": "Post",
- "createDomainType": "Type:",
- "createDomainName": "Navn:",
- "createDomainValue": "Værdi:",
- "createDomainCnameRecords": "CNAME-poster",
- "createDomainARecords": "A-poster",
- "createDomainRecordNumber": "Post {number}",
- "createDomainTxtRecords": "TXT-poster",
- "createDomainSaveTheseRecords": "Gem disse posterne",
- "createDomainSaveTheseRecordsDescription": "Sørg for at gem disse DNS-posterne, da du ikke vil se dem tilbage.",
- "createDomainDnsPropagation": "DNS-propagering",
- "createDomainDnsPropagationDescription": "DNS-ændringer kan tage lidt tid at propagere over internettet. Dette kan tage fra nogen få minutter til 48 timer, afhængigt af din DNS-udbyder og TTL-indstillinger.",
- "resourcePortRequired": "Portnummer er påkrævet for ikke-HTTP-ressourcer",
- "resourcePortNotAllowed": "Portnummer skal ikke angis for HTTP-ressourcer",
- "billingPricingCalculatorLink": "Pris Kalkulator",
- "billingYourPlan": "Din funktionsplan",
- "billingViewOrModifyPlan": "Vis eller ændre nuværende abonnement",
- "billingViewPlanDetails": "Se Planleggings detaljer",
- "billingUsageAndLimits": "Brug og grænse",
- "billingViewUsageAndLimits": "Se planets grænse og nuværende brug",
- "billingCurrentUsage": "Nuværende brug",
- "billingMaximumLimits": "Maks antal grænse",
- "billingRemoteNodes": "Eksterne noder",
- "billingUnlimited": "Ubegrænset",
- "billingPaidLicenseKeys": "Betalt licensnøgler",
- "billingManageLicenseSubscription": "Administrer abonnementet for betalte licensnøgler selv hostet",
- "billingCurrentKeys": "Nuværende nøgler",
- "billingModifyCurrentPlan": "Ændre nuværende plan",
- "billingManageLicenseSubscriptionDescription": "Administrer dit abonnement for betalte selvhostede licensnøgler og download fakturaer.",
- "billingConfirmUpgrade": "Bekræft opgradering",
- "billingConfirmDowngrade": "Bekræft nedgradering",
- "billingConfirmUpgradeDescription": "Du er ved at opgradere dit abonnement. Gå gennem de nye grænserne og pris nedenfor.",
- "billingConfirmDowngradeDescription": "Du er ved at nedgradere din plan. Gå gennem de nye grænserne og pris nedenfor.",
- "billingPlanIncludes": "Plan Inkluderer",
- "billingProcessing": "Behandler...",
- "billingConfirmUpgradeButton": "Bekræft opgradering",
- "billingConfirmDowngradeButton": "Bekræft nedgradering",
- "billingLimitViolationWarning": "Brug overbelastede grænse for ny plan",
- "billingLimitViolationDescription": "Dit nuværende forbrug overskrider grænserne for denne plan. Efter nedgradering deaktiveres alle handlinger, indtil du reducerer forbruget inden for de nye grænser. Gennemgå venligst funktionerne nedenfor, som i øjeblikket er over grænserne. Overskredne grænser:",
- "billingFeatureLossWarning": "Fremhev tilgængelig notifikation",
- "billingFeatureLossDescription": "Ved at nedgradere vil funktioner der ikke er tilgængelige i den nye planen automatisk blive deaktiveret. Nogen indstillinger og konfigurationer kan gå tapt. Venligst gennemgå prismatrisen for at forstå hvilke funktioner der ikke længere vil være tilgængelige.",
- "billingUsageExceedsLimit": "Nuværende brug ({current}) overskrider grænsen ({limit})",
- "billingPastDueTitle": "Betalingen har forfalden",
- "billingPastDueDescription": "Betalingen er forfalden. Opdater venligst din betalingsmetode for fortsat at bruge din nuværende funktionsplan. Hvis problemet ikke løses, bliver dit abonnement annulleret, og du bliver nulstillet til gratisniveauet.",
- "billingUnpaidTitle": "Abonnement ubetalt",
- "billingUnpaidDescription": "Dit abonnement er ubetalt, og du er blevet nulstillet til gratisniveauet. Opdater venligst din betalingsmetode for at gendanne abonnementet.",
- "billingIncompleteTitle": "Betaling ufullstendig",
- "billingIncompleteDescription": "Betalingen er ufullstendig. Venligst fuldfør betalingsprosessen for at aktivere abonnementet.",
- "billingIncompleteExpiredTitle": "Betaling udløbet",
- "billingIncompleteExpiredDescription": "Din betaling blev aldrig fuldført og er udløbet. Du er blevet nulstillet til gratisniveauet. Abonnér venligst igen for at gendanne adgangen til betalte funktioner.",
- "billingManageSubscription": "Administrer dit abonnement",
- "billingResolvePaymentIssue": "Venligst løs dit betalingsproblem før du opgraderer eller nedgraderer betalingen",
- "signUpTerms": {
- "IAgreeToThe": "Jeg accepterer",
- "termsOfService": "brugervilkårene",
- "and": "og",
- "privacyPolicy": "privatlivspolitikken"
- },
- "signUpMarketing": {
- "keepMeInTheLoop": "Hold mig opdateret med nyheder, opdateringer og nye funktioner via e-mail."
- },
- "siteRequired": "Site er påkrævet.",
- "olmTunnel": "Olm-tunnel",
- "olmTunnelDescription": "Brug Olm for klientforbindelse",
- "errorCreatingClient": "Fejl ved oprettelse af klient",
- "clientDefaultsNotFound": "Klientstandarder ikke fundet",
- "createClient": "Opret klient",
- "createClientDescription": "Oprette en ny klient for at få adgang til private ressourcer",
- "seeAllClients": "Se alle klienter",
- "clientInformation": "Klientinformation",
- "clientNamePlaceholder": "Klientnavn",
- "address": "Adresse",
- "subnetPlaceholder": "Subnet",
- "addressDescription": "Den interne adressen til klienten. Skal falle inden for organisationens subnet.",
- "selectSites": "Vælg sites",
- "selectLabels": "Vælg etiketter",
- "sitesDescription": "Klienten vil have forbindelse til de valgte områdene",
- "clientInstallOlm": "Installer Olm",
- "clientInstallOlmDescription": "Få Olm til at køre på dit system",
- "clientOlmCredentials": "Legitimationsoplysninger",
- "clientOlmCredentialsDescription": "Dette er hvordan klienten vil godkende med serveren",
- "olmEndpoint": "Endpoint",
- "olmId": "ID",
- "olmSecretKey": "Sikkerhedsnøgle",
- "clientCredentialsSave": "Gem brugeroplysninger",
- "clientCredentialsSaveDescription": "Du vil kun kunne se dette én gang. Sørg for at kopiere det til et sikkert sted.",
- "generalSettingsDescription": "Konfigurer de generelle indstillingerne for denne klienten",
- "clientUpdated": "Klient opdateret",
- "clientUpdatedDescription": "Klienten er blevet opdateret.",
- "clientUpdateFailed": "Kunne ikke opdatere klient",
- "clientUpdateError": "En fejl opstod under opdatering af klienten.",
- "sitesFetchFailed": "Kunne ikke hente sites",
- "sitesFetchError": "En fejl opstod under hentning af sites.",
- "olmErrorFetchReleases": "En fejl opstod under hentning af Olm-utgivelser.",
- "olmErrorFetchLatest": "En fejl opstod under hentning af den nyeste Olm-utgivelsen.",
- "enterCidrRange": "Indtast CIDR-site",
- "resourceEnableProxy": "Aktivér offentlig proxy",
- "resourceEnableProxyDescription": "Aktivér offentlig proxying til denne ressource. Dette giver adgang til ressourcen udefra netværket gennem skyen på en åben port. Kræver Traefik-konfiguration.",
- "externalProxyEnabled": "Ekstern proxy aktiveret",
- "addNewTarget": "Tilføj nyt mål",
- "targetsList": "Liste over mål",
- "advancedMode": "Avanceret tilstand",
- "advancedSettings": "Avanserte indstillinger",
- "targetErrorDuplicateTargetFound": "Duplikat af mål fundet",
- "healthCheckHealthy": "Sund",
- "healthCheckUnhealthy": "Usund",
- "healthCheckUnknown": "Ukendt",
- "healthCheck": "Sundhedstjek",
- "configureHealthCheck": "Konfigurer Sundhedstjek",
- "configureHealthCheckDescription": "Opsæt overvågning af din ressource for at sikre at den altid er tilgængelig",
- "enableHealthChecks": "Aktivér Sundhedstjek",
- "healthCheckDisabledStateDescription": "Når deaktiveret vil sitet ikke udføre sundhedstjek, og tilstanden vil blive betragtet som ukendt.",
- "enableHealthChecksDescription": "Overvåg sundheden for dette mål. Du kan overvåge et andet endpoint end målet, hvis det er nødvendigt.",
- "healthScheme": "Metode",
- "healthSelectScheme": "Vælg metode",
- "healthCheckPortInvalid": "Porten skal være mellem 1 og 65535",
- "healthCheckPath": "Sti",
- "healthHostname": "IP / Vært",
- "healthPort": "Port",
- "healthCheckPathDescription": "Stien for at tjekke helsestatus.",
- "healthyIntervalSeconds": "Sund intervall (sek)",
- "unhealthyIntervalSeconds": "Usundt interval (sek.)",
- "IntervalSeconds": "Sundt interval",
- "timeoutSeconds": "Tidsavbrudd (sek)",
- "timeIsInSeconds": "Tid er i sekunder",
- "requireDeviceApproval": "Kræv enhedsgodkendelse",
- "requireDeviceApprovalDescription": "Brugere med denne rolle skal have nye enheder godkendt af en administrator, før de kan oprette forbindelse og få adgang til ressourcer.",
- "sshSettings": "SSH Indstillinger",
- "sshAccess": "SSH-adgang",
- "rdpSettings": "RDP Indstillinger",
- "vncSettings": "VNC Indstillinger",
- "sshServer": "SSH-server",
- "rdpServer": "RDP-server",
- "vncServer": "VNC-server",
- "sshServerDescription": "Opsæt autentiseringsmetoden, daemonplasseringen, og serverens destination",
- "rdpServerDescription": "Konfigurer destinationen og porten til RDP-serveren",
- "vncServerDescription": "Konfigurer destinationen og porten til VNC-serveren",
- "sshServerMode": "Tilstand",
- "sshServerModeStandard": "Standard SSH-server",
- "sshServerModePangolin": "Pangolin SSH",
- "sshServerModeStandardDescription": "Ruter kommandoer over netværk til en SSH-server som OpenSSH.",
- "sshServerModeNative": "Innfødt SSH Server",
- "sshServerModeNativeDescription": "Udfører kommandoer direkte i verten via Site-connectoren. Ingen netværkskonfiguration kræves.",
- "sshAuthenticationMethod": "Autentificeringsmetode",
- "sshAuthMethodManual": "Manuel autentificering",
- "sshAuthMethodManualDescription": "Kræver eksisterende vertlegitimation. Omgår automatisk provisionering.",
- "sshAuthMethodAutomated": "Automatisk Provisionering",
- "sshAuthMethodAutomatedDescription": "Opretter automatisk brugere, grupper og sudo-tilladelser på verten.",
- "sshAuthDaemonLocation": "Autentificering Daemon-plassering",
- "sshDaemonLocationSiteDescription": "Utføres lokalt på maskinen som er host for site-connectoren.",
- "sshDaemonLocationRemote": "På Ekstern Host",
- "sshDaemonLocationRemoteDescription": "Utføres på en separat målenhet på samme netværk.",
- "sshDaemonDisclaimer": "Sørg for, at din målenhed er korrekt konfigureret til at køre autentificeringsdaemonen, før du fuldfører denne opsætning, ellers mislykkes provisioneringen.",
- "sshDaemonPort": "Daemon-port",
- "sshServerDestination": "Serverens Destination",
- "sshServerDestinationDescription": "Konfigurer destinationen for SSH-serveren",
- "destination": "Destination",
- "destinationRequired": "Destination er påkrævet.",
- "domainRequired": "Domæne er påkrævet.",
- "proxyPortRequired": "Port er påkrævet.",
- "invalidPathConfiguration": "Ugyldig sti-konfiguration.",
- "invalidRewritePathConfiguration": "Ugyldig omskrivningssti-konfiguration.",
- "bgTargetMultiSiteDisclaimer": "Ved at vælge flere sites aktiveres robust routing og failover for høj tilgængelighed.",
- "roleAllowSsh": "Tillad SSH",
- "roleAllowSshAllow": "Tillad",
- "roleAllowSshDisallow": "Forby",
- "roleAllowSshDescription": "Tillad brugere med denne rolle at oprette forbindelse til ressourcer via SSH. Når deaktiveret får rollen ikke adgang til SSH.",
- "sshSudoMode": "Sudo adgang",
- "sshSudoModeNone": "Ingen",
- "sshSudoModeNoneDescription": "Brugeren kan ikke køre kommandoer med sudo.",
- "sshSudoModeFull": "Fuld Sudo",
- "sshSudoModeFullDescription": "Brugeren kan køre hvilken som helst kommando med sudo.",
- "sshSudoModeCommands": "Kommandoer",
- "sshSudoModeCommandsDescription": "Brugeren kan kun køre de angitte kommandoene med sudo.",
- "sshSudo": "Tillad sudo",
- "sshSudoCommands": "Sudo kommandoer",
- "sshSudoCommandsDescription": "Liste over kommandoer brugeren har lov til at køre med sudo, separert med komma, mellemrum eller nye linjer. Absolutte stier skal bruges.",
- "sshCreateHomeDir": "Opret hjemmappe",
- "sshUnixGroups": "Unix grupper",
- "sshUnixGroupsDescription": "Unix-grupper at tilføje brugeren i på målverten, separert med komma, mellemrum eller nye linjer.",
- "roleTextFieldPlaceholder": "Indtast verdier, eller slipp en.txt eller.csv fil",
- "roleTextImportTitle": "Importer fra fil",
- "roleTextImportDescription": "Importerer {fileName} til {fieldLabel}.",
- "roleTextImportSkipHeader": "Hopp over første rad (header)",
- "roleTextImportOverride": "Erstatte eksisterende",
- "roleTextImportAppend": "Tilføj eksisterende",
- "roleTextImportMode": "Importtilstand",
- "roleTextImportPreview": "Forhåndsvisning",
- "roleTextImportItemCount": "{count, plural, =0 {Ingen elementer at importere} one {ét element at importere} other {# elementer at importere}}",
- "roleTextImportTotalCount": "{existing} eksisterende + {imported} importert = {total} totalt",
- "roleTextImportConfirm": "Importer",
- "roleTextImportInvalidFile": "Ustøttet filtype",
- "roleTextImportInvalidFileDescription": "Kun.txt og.csv filer er støttet.",
- "roleTextImportEmpty": "Ingen elementer fundet i filen",
- "roleTextImportEmptyDescription": "Filen indeholder ingen importerbare elementer.",
- "retryAttempts": "Forsøg igen",
- "expectedResponseCodes": "Forventede svarkoder",
- "expectedResponseCodesDescription": "HTTP-statuskode som indikerer sund status. Hvis den bliver stående tom, regnes 200-300 som sund.",
- "customHeaders": "Brugerdefinerede headers",
- "customHeadersDescription": "Headers som er adskilt med linje: Overskriftsnavn: værdi",
- "headersValidationError": "Header skal være i formatet: header-navn: værdi.",
- "saveHealthCheck": "Gem Sundhedstjek",
- "healthCheckSaved": "Sundhedstjek Gemt",
- "healthCheckSavedDescription": "Sundhedstjekkonfigurationen blev gemt",
- "healthCheckError": "Sundhedstjekfeil",
- "healthCheckErrorDescription": "Det opstod en fejl under lagring af sundhedstjekkonfigurationen",
- "healthCheckPathRequired": "Sundhedstjeksti er påkrævet",
- "healthCheckMethodRequired": "HTTP-metode er påkrævet",
- "healthCheckIntervalMin": "Sjekkeintervallet skal være mindst 5 sekunder",
- "healthCheckTimeoutMin": "Timeout skal være mindst 1 sekund",
- "healthCheckRetryMin": "Forsøg igen skal være mindst 1",
- "healthCheckMode": "Tjek tilstand",
- "healthCheckStrategy": "Strategi",
- "healthCheckModeDescription": "TCP-tilstand verificerer kun forbindelsen. HTTP-tilstand validerer HTTP-responsen.",
- "healthyThreshold": "Sunnhets terskel",
- "healthyThresholdDescription": "Suksesser på rad som kræves før man markerer som sund.",
- "unhealthyThreshold": "Usund terskel",
- "unhealthyThresholdDescription": "Fejl på rad som kræves før man markerer som usund.",
- "healthCheckHealthyThresholdMin": "Sunnhet terskel skal være mindst 1",
- "healthCheckUnhealthyThresholdMin": "Usund terskel skal være mindst 1",
- "httpMethod": "HTTP-metode",
- "selectHttpMethod": "Vælg HTTP-metode",
- "domainPickerSubdomainLabel": "Underdomæne",
- "domainPickerWildcard": "Jokertegn",
- "domainPickerWildcardPaidOnly": "Wildcard-underdomæner er en betalt funktion. Opgrader venligst for at få adgang til denne funktion.",
- "domainPickerBaseDomainLabel": "Basisdomæne",
- "domainPickerSearchDomains": "Søg i domæner...",
- "domainPickerNoDomainsFound": "Ingen domæner fundet",
- "domainPickerLoadingDomains": "Indlæser domæner...",
- "domainPickerSelectBaseDomain": "Vælg basisdomæne...",
- "domainPickerNotAvailableForCname": "Ikke tilgængelig for CNAME-domæner",
- "domainPickerEnterSubdomainOrLeaveBlank": "Indtast underdomæne eller la feltet stå tomt for at bruge basisdomæne.",
- "domainPickerEnterSubdomainToSearch": "Indtast et underdomæne for at søge og vælge blandt tilgængelige gratis domæner.",
- "domainPickerFreeDomains": "Gratis domæner",
- "domainPickerSearchForAvailableDomains": "Søg efter tilgængelige domæner",
- "domainPickerNotWorkSelfHosted": "Merk: Gratis tilbudte domæner er ikke tilgængelig for selvhostede instanser akkurat nu.",
- "resourceDomain": "Domæne",
- "resourceEditDomain": "Rediger domæne",
- "siteName": "Områdenavn",
- "proxyPort": "Port",
- "resourcesTableProxyResources": "Offentlig",
- "resourcesTableClientResources": "Privat",
- "resourcesTableNoProxyResourcesFound": "Ingen proxy-ressourcer fundet.",
- "resourcesTableNoInternalResourcesFound": "Ingen interne ressourcer fundet.",
- "resourcesTableDestination": "Destination",
- "resourcesTableAlias": "Alias",
- "resourcesTableAliasAddress": "Alias adresse",
- "resourcesTableAliasAddressInfo": "Denne adressen er en del af organisationens subnet. Den bruges til at løse aliasposter ved hjælp af intern DNS-opløsning.",
- "resourcesTableClients": "Klienter",
- "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgængelig internt når de er forbundet til med en klient.",
- "resourcesTableHealthy": "Frisk",
- "resourcesTableDegraded": "Nedgradert",
- "resourcesTableUnhealthy": "Usund",
- "resourcesTableUnknown": "Ukendt",
- "resourcesTableNotMonitored": "Ikke overvåket",
- "resourcesTableNoTargets": "Ingen mål",
- "editInternalResourceDialogEditClientResource": "Rediger Private Ressourcer",
- "editInternalResourceDialogUpdateResourceProperties": "Opdater ressourcekonfigurationen og få adgangskontroller for {resourceName}",
- "editInternalResourceDialogResourceProperties": "Ressourceegenskaber",
- "editInternalResourceDialogName": "Navn",
- "editInternalResourceDialogProtocol": "Protokol",
- "editInternalResourceDialogSitePort": "Områdeport",
- "editInternalResourceDialogTargetConfiguration": "Målkonfiguration",
- "editInternalResourceDialogCancel": "Annuller",
- "editInternalResourceDialogSaveResource": "Gem ressource",
- "editInternalResourceDialogSuccess": "Succes",
- "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressource opdateret med succes",
- "editInternalResourceDialogError": "Fejl",
- "editInternalResourceDialogFailedToUpdateInternalResource": "Mislykkedes at opdatere intern ressource",
- "editInternalResourceDialogNameRequired": "Navn er påkrævet",
- "editInternalResourceDialogNameMaxLength": "Navn kan ikke være længere enn 255 tegn",
- "editInternalResourceDialogProxyPortMin": "Proxy-port skal være mindst 1",
- "editInternalResourceDialogProxyPortMax": "Proxy-port skal være mindre end 65536",
- "editInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat",
- "editInternalResourceDialogDestinationPortMin": "Destinasjonsport skal være mindst 1",
- "editInternalResourceDialogDestinationPortMax": "Destinasjonsport skal være mindre end 65536",
- "editInternalResourceDialogPortModeRequired": "Protokol, proxy-port og målport er påkrævet for porttilstand",
- "editInternalResourceDialogMode": "Tilstand",
- "editInternalResourceDialogModePort": "Port",
- "editInternalResourceDialogModeHost": "Vært",
- "editInternalResourceDialogModeCidr": "CIDR",
- "editInternalResourceDialogModeHttp": "HTTP",
- "editInternalResourceDialogModeHttps": "HTTPS",
- "editInternalResourceDialogModeSsh": "SSH",
- "editInternalResourceDialogScheme": "Skema",
- "editInternalResourceDialogEnableSsl": "Aktivér TLS",
- "editInternalResourceDialogEnableSslDescription": "Aktivér SSL/TLS-kryptering for sikre HTTPS-forbindelser til destinationen.",
- "editInternalResourceDialogDestination": "Destination",
- "editInternalResourceDialogDestinationHostDescription": "IP-adressen eller værtsnavnet til ressourcen på sitets netværk.",
- "editInternalResourceDialogDestinationIPDescription": "IP eller hostnavn til ressourcen på sitets netværk.",
- "editInternalResourceDialogDestinationCidrDescription": "CIDR-området til ressourcen på sitets netværk.",
- "editInternalResourceDialogAlias": "Alias",
- "editInternalResourceDialogAliasDescription": "Et valgfrit internt DNS-alias for denne ressource.",
- "createInternalResourceDialogNoSitesAvailable": "Ingen tilgængelige steder",
- "createInternalResourceDialogNoSitesAvailableDescription": "Du skal have mindst ét Newt-site med et konfigureret subnet for at oprette interne ressourcer.",
- "createInternalResourceDialogClose": "Luk",
- "createInternalResourceDialogCreateClientResource": "Opret privat ressource",
- "createInternalResourceDialogCreateClientResourceDescription": "Opret en ny ressource som kun vil være tilgængelig for kunder som er forbundet til organisationen",
- "privateResourceGeneralDescription": "Konfigurer navnet, identifikatoren, og andre generelle indstillinger for ressourcen.",
- "privateResourceCreatePageSeeAll": "Se alle private ressourcer",
- "privateResourceAllowIcmpPing": "Tillad ICMP-ping",
- "privateResourceNetworkAccess": "Netværksadgang",
- "privateResourceNetworkAccessDescription": "Styr TCP/UDP-portadgang og om ICMP-ping er tilladt for denne ressource.",
- "hostSettings": "Værtindstillinger",
- "cidrSettings": "CIDR-indstillinger",
- "createInternalResourceDialogResourceProperties": "Ressourceegenskaber",
- "createInternalResourceDialogName": "Navn",
- "createInternalResourceDialogSite": "Websted",
- "selectSite": "Vælg site...",
- "multiSitesSelectorSitesCount": "{count, plural, one {# sted} other {# steder}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# etiket} other {# etiketter}}",
- "noSitesFound": "Ingen sites fundet.",
- "createInternalResourceDialogProtocol": "Protokol",
- "createInternalResourceDialogTcp": "TCP",
- "createInternalResourceDialogUdp": "UDP",
- "createInternalResourceDialogSitePort": "Områdeport",
- "createInternalResourceDialogSitePortDescription": "Brug denne port for at få adgang til ressourcen på sitet, når du er tilsluttet med en klient.",
- "createInternalResourceDialogTargetConfiguration": "Målkonfiguration",
- "createInternalResourceDialogDestinationIPDescription": "IP eller hostnavn til ressourcen på sitets netværk.",
- "createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressourcen kan nås.",
- "createInternalResourceDialogCancel": "Annuller",
- "createInternalResourceDialogCreateResource": "Opret ressource",
- "createInternalResourceDialogSuccess": "Succes",
- "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressource oprettet med succes",
- "createInternalResourceDialogError": "Fejl",
- "createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke oprette intern ressource",
- "createInternalResourceDialogNameRequired": "Navn er påkrævet",
- "createInternalResourceDialogNameMaxLength": "Navn kan ikke være længere enn 255 tegn",
- "createInternalResourceDialogPleaseSelectSite": "Vælg venligst et site",
- "createInternalResourceDialogProxyPortMin": "Proxy-port skal være mindst 1",
- "createInternalResourceDialogProxyPortMax": "Proxy-port skal være mindre end 65536",
- "createInternalResourceDialogInvalidIPAddressFormat": "Ugyldig IP-adresseformat",
- "createInternalResourceDialogDestinationPortMin": "Destinasjonsport skal være mindst 1",
- "createInternalResourceDialogDestinationPortMax": "Destinasjonsport skal være mindre end 65536",
- "createInternalResourceDialogPortModeRequired": "Protokol, proxy-port og målport er påkrævet for porttilstand",
- "createInternalResourceDialogMode": "Tilstand",
- "createInternalResourceDialogModePort": "Port",
- "createInternalResourceDialogModeHost": "Vært",
- "createInternalResourceDialogModeCidr": "CIDR",
- "createInternalResourceDialogModeHttp": "HTTP",
- "createInternalResourceDialogModeHttps": "HTTPS",
- "createInternalResourceDialogModeSsh": "SSH",
- "scheme": "Skema",
- "createInternalResourceDialogScheme": "Skema",
- "createInternalResourceDialogEnableSsl": "Aktivér TLS",
- "createInternalResourceDialogEnableSslDescription": "Aktivér SSL/TLS-kryptering for sikre HTTPS-forbindelser til destinationen.",
- "createInternalResourceDialogDestination": "Destination",
- "createInternalResourceDialogDestinationHostDescription": "IP-adressen eller værtsnavnet til ressourcen på sitets netværk.",
- "createInternalResourceDialogDestinationCidrDescription": "CIDR-området til ressourcen på sitets netværk.",
- "createInternalResourceDialogAlias": "Alias",
- "createInternalResourceDialogAliasDescription": "Et valgfrit internt DNS-alias for denne ressource.",
- "internalResourceAliasLocalWarning": "Aliasser, der ender på .local, kan forårsage opløsningsproblemer på grund af mDNS på nogle netværk.",
- "internalResourceDownstreamSchemeRequired": "Skema er påkrævet for HTTP-ressourcer",
- "internalResourceHttpPortRequired": "Destinasjonsport er nødvendig for HTTP-ressourcer",
- "siteConfiguration": "Konfiguration",
- "siteAcceptClientConnections": "Acceptere klientforbindelser",
- "siteAcceptClientConnectionsDescription": "Tillad brugere og klienter at få adgang til ressourcer på denne side. Dette kan endres senere.",
- "siteAddress": "Siteadresse (avanceret)",
- "siteAddressDescription": "Den interne adresse til sitet. Skal falde inden for organisationens subnet.",
- "siteNameDescription": "Visningsnavnet på sitet, som kan ændres senere.",
- "autoLoginExternalIdp": "Automatisk login med ekstern IdP",
- "autoLoginExternalIdpDescription": "Omdiriger brugeren umiddelbart til den eksterne identitetsudbyderen for autentificering.",
- "selectIdp": "Vælg IdP",
- "selectIdpPlaceholder": "Vælg en IdP...",
- "selectIdpRequired": "Vælg venligst en IdP når automatisk login er aktiveret.",
- "autoLoginTitle": "Omdirigering",
- "autoLoginDescription": "Omdirigerer dig til den eksterne identitetsudbyderen for autentificering.",
- "autoLoginProcessing": "Forbereder autentificering...",
- "autoLoginRedirecting": "Omdirigerer til login...",
- "autoLoginError": "Fejl ved automatisk login",
- "autoLoginErrorNoRedirectUrl": "Ingen omdirigerings-URL modtaget fra identitetsudbyderen.",
- "autoLoginErrorGeneratingUrl": "Kunne ikke generere autentiserings-URL.",
- "remoteExitNodeManageRemoteExitNodes": "Eksterne noder",
- "remoteExitNodeDescription": "Selvhost din egen eksterne relay- og proxyservernode",
- "remoteExitNodes": "Noder",
- "searchRemoteExitNodes": "Søg noder...",
- "remoteExitNodeAdd": "Tilføj Node",
- "remoteExitNodeErrorDelete": "Fejl ved sletning af node",
- "remoteExitNodeQuestionRemove": "Er du sikker på at du vil fjerne noden fra organisationen?",
- "remoteExitNodeMessageRemove": "Når noden er fjernet, vil ikke længere være tilgængeligt.",
- "remoteExitNodeConfirmDelete": "Bekræft sletning af Node",
- "remoteExitNodeDelete": "Slet Node",
- "sidebarRemoteExitNodes": "Eksterne noder",
- "remoteExitNodeId": "ID",
- "remoteExitNodeSecretKey": "Sikkerhedsnøgle",
- "remoteExitNodeNetworkingTitle": "Netværksindstillinger",
- "remoteExitNodeNetworkingDescription": "Konfigurér, hvordan denne fjerne exit-node skal dirigere trafik, og hvilke sites der foretrækkes at oprette forbindelse gennem den. Avancerede funktioner til brug med backhaul-netværkskonfigurationer.",
- "remoteExitNodeNetworkingSave": "Gem indstillinger",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Netværksindstillinger gemt",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Netværksindstillinger er blevet opdateret.",
- "remoteExitNodeNetworkingSaveError": "Kunne ikke gemme netværksindstillinger",
- "remoteExitNodeNetworkingSubnetsTitle": "Fjern Subnets",
- "remoteExitNodeNetworkingSubnetsDescription": "Definér de CIDR-områder, som denne fjerne exit-node vil dirigere trafik til. Indtast en gyldig CIDR (f.eks. 10.0.0.0/8) og tryk Enter for at tilføje.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Tilføj et CIDR-område (f.eks. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Kunne ikke indlæse subnets",
- "remoteExitNodeNetworkingLabelsTitle": "Præference Etiketter",
- "remoteExitNodeNetworkingLabelsDescription": "Sites med disse etiketter vil blive tvunget til at oprette forbindelse gennem denne fjerne exit-node.",
- "remoteExitNodeNetworkingLabelsButtonText": "Vælg etiketter...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Søg efter etiketter...",
- "remoteExitNodeNetworkingLabelsLoadError": "Kunne ikke indlæse etiketter",
- "remoteExitNodeCreate": {
- "title": "Opret ekstern node",
- "description": "Opret en ny selvhostet ekstern relay- og proxyservernode",
- "viewAllButton": "Vis alle noder",
- "strategy": {
- "title": "Oprettelsesstrategi",
- "description": "Vælg, hvordan du vil oprette den eksterne node",
- "adopt": {
- "title": "Adopter node",
- "description": "Vælg dette, hvis du allerede har legitimationsoplysninger til noden."
- },
- "generate": {
- "title": "Generér nøgler",
- "description": "Vælg dette, hvis du vil generere nye nøgler til noden."
- }
- },
- "adopt": {
- "title": "Adopter eksisterende node",
- "description": "Indtast oplysningerne for den eksisterende node, du vil adoptere",
- "nodeIdLabel": "Node-ID",
- "nodeIdDescription": "ID’et til den eksisterende node, du vil adoptere",
- "secretLabel": "Sikkerhedsnøgle",
- "secretDescription": "Den hemmelige nøgle til en eksisterende node",
- "submitButton": "Adopter node"
- },
- "generate": {
- "title": "Genererede legitimationsoplysninger",
- "description": "Brug disse genererede oplysninger til at konfigurere noden",
- "nodeIdTitle": "Node-ID",
- "secretTitle": "Sikkerhed",
- "saveCredentialsTitle": "Tilføj legitimationsoplysninger til konfigurationen",
- "saveCredentialsDescription": "Tilføj disse legitimationsoplysninger i din selvhostede Pangolin-nodekonfigurationsfil for at fuldføre forbindelsen.",
- "submitButton": "Opret node"
- },
- "validation": {
- "adoptRequired": "Node-ID og hemmelighed er påkrævet, når du adopterer en eksisterende node"
- },
- "errors": {
- "loadDefaultsFailed": "Fejl ved indlæsning af standardværdier",
- "defaultsNotLoaded": "Standardværdier er ikke indlæst",
- "createFailed": "Kan ikke oprette node"
- },
- "success": {
- "created": "Node oprettet"
- }
- },
- "remoteExitNodeSelection": "Nodevalg",
- "remoteExitNodeSelectionDescription": "Vælg en node til at sende trafik gennem for dette lokale site",
- "remoteExitNodeRequired": "En node skal vælges for lokale sites",
- "noRemoteExitNodesAvailable": "Ingen noder tilgængelige",
- "noRemoteExitNodesAvailableDescription": "Ingen noder er tilgængelige for denne organisation. Opret en node først for at bruge lokale sites.",
- "exitNode": "Exitnode",
- "country": "Land",
- "countryIsNot": "Land er ikke",
- "rulesMatchCountry": "I øjeblikket baseret på kilde-IP",
- "region": "Fylke",
- "selectRegion": "Vælg region",
- "searchRegions": "Søg efter sites...",
- "noRegionFound": "Ingen region fundet.",
- "rulesMatchRegion": "Vælg en regional gruppering af land",
- "rulesErrorInvalidRegion": "Ugyldig site",
- "rulesErrorInvalidRegionDescription": "Vælg venligst et gyldig site.",
- "regionAfrica": "Afrika",
- "regionNorthernAfrica": "[country name] Nord-Afrika",
- "regionEasternAfrica": "Øst-Afrika",
- "regionMiddleAfrica": "Mellemafrika",
- "regionSouthernAfrica": "Sør-Afrika",
- "regionWesternAfrica": "[country name] Vest-Afrika",
- "regionAmericas": "Amerika",
- "regionCaribbean": "Karibia",
- "regionCentralAmerica": "Sentral-Amerika",
- "regionSouthAmerica": "Sør-Amerika",
- "regionNorthernAmerica": "Nord-Amerika",
- "regionAsia": "Asien",
- "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": "Sydeuropa",
- "regionWesternEurope": "Vest-Europa",
- "regionOceania": "Oceanien",
- "regionAustraliaAndNewZealand": "Australia og New Zealand",
- "regionMelanesia": "Melanesien",
- "regionMicronesia": "Mikronesien",
- "regionPolynesia": "Polynesien",
- "managedSelfHosted": {
- "title": "Administreret selvhostet",
- "description": "Sikre, selvhostede Pangolin-servere med lav vedligeholdelse og ekstra bells and whistles",
- "introTitle": "Administreret self-hosted Pangolin",
- "introDescription": "er en mulighed udviklet til personer, der ønsker enkelhed og ekstra pålidelighed, mens de stadig holder deres data private og selvhostede.",
- "introDetail": "Med dette valg kører du stadig din egen Pangolin-node — tunneller, TLS-terminering og trafik ligger på din server. Forskellen er, at administration og overvågning håndteres via vores cloud-dashboard, som låser op for en række fordele:",
- "benefitSimplerOperations": {
- "title": "Nemmere operationer",
- "description": "Ingen grund til at køre din egen e-mailserver eller opsætte kompleks varsling. Du får sundhedstjek og nedetidsvarsler out of the box."
- },
- "benefitAutomaticUpdates": {
- "title": "Automatiske opdateringer",
- "description": "Cloud-dashboardet udvikler sig hurtigt, så du får nye funktioner og fejlrettelser uden at skulle hente nye containere manuelt hver gang."
- },
- "benefitLessMaintenance": {
- "title": "Mindre vedligeholdelse",
- "description": "Ingen databasestyring, sikkerhedskopier eller ekstra infrastruktur for at administrer. Vi håndterer det i skyen."
- },
- "benefitCloudFailover": {
- "title": "Cloud-failover",
- "description": "Hvis EK-gruppen din går ned, kan tunnellerne midlertidig mislykkes i at nu våre cloud-endpoints til du tager den tilbage online."
- },
- "benefitHighAvailability": {
- "title": "Høj tilgængelighed (PoPs)",
- "description": "Du kan også tilføje flere noder til din konto for redundans og bedre ydeevne."
- },
- "benefitFutureEnhancements": {
- "title": "Fremtidige forbedringer",
- "description": "Vi planlægger at tilføje flere analyser, varsler og administrationsværktøjer for at gøre din installation endnu mere robust."
- },
- "docsAlert": {
- "text": "Læs mere om Managed Self-Hosted-muligheden i vores",
- "documentation": "dokumentation"
- },
- "convertButton": "Konverter denne node til manuel drift"
- },
- "internationaldomaindetected": "Internasjonalt domæne registreret",
- "willbestoredas": "Vil blive gemt som:",
- "roleMappingDescription": "Bestem hvordan roller tildeles brugere når de logs ind med denne identitetsudbyder.",
- "selectRole": "Vælg en rolle",
- "roleMappingExpression": "Udtryk",
- "selectRolePlaceholder": "Vælg en rolle",
- "selectRoleDescription": "Vælg en rolle at tildele alle brugere fra denne identitetsudbyder",
- "roleMappingExpressionDescription": "Indtast et JMESPath udtryk for at hente rolleinformation fra ID-nøglen",
- "idpTenantIdRequired": "Bedriftens ID kræves",
- "invalidValue": "Ugyldig værdi",
- "idpTypeLabel": "Identitet udbyder type",
- "roleMappingExpressionPlaceholder": "F.eks. indeholder(grupper, 'admin') && 'Admin' ⋅'Medlem'",
- "roleMappingModeFixedRoles": "Fast roller",
- "roleMappingModeMappingBuilder": "Kartlegger bygger",
- "roleMappingModeRawExpression": "Rå udtryk",
- "roleMappingFixedRolesPlaceholderSelect": "Vælg en eller flere roller",
- "roleMappingFixedRolesPlaceholderFreeform": "Indtast rollenavn (eksakt treff per organisation)",
- "roleMappingFixedRolesDescriptionSameForAll": "Tildele den samme rollen som er sat til hver automatisk midlertidig bruger.",
- "roleMappingFixedRolesDescriptionDefaultPolicy": "For standardpolitikker, indtast rollenavne som findes i hver organisation der brugerne tilbydes. Navn skal stemme præcis.",
- "roleMappingClaimPath": "Kræv sti",
- "roleMappingClaimPathPlaceholder": "grupper",
- "roleMappingClaimPathDescription": "Sti i i token payload som indeholder kildeverdier (for eksempel grupper).",
- "roleMappingMatchValue": "Treff værdi",
- "roleMappingAssignRoles": "Tildele roller",
- "roleMappingAddMappingRule": "Tilføj tilordningsregel",
- "roleMappingRawExpressionResultDescription": "Udtryk skal vurderes til en streng eller en tekststreng.",
- "roleMappingRawExpressionResultDescriptionSingleRole": "Udtryk skal evaluere til en streng (en rollenavn).",
- "roleMappingMatchValuePlaceholder": "Match værdi (for eksempel: admin)",
- "roleMappingAssignRolesPlaceholderFreeform": "Angiv rollenavn (eksakt per org)",
- "roleMappingBuilderFreeformRowHint": "Rollenavn skal samsvare med en rolle i hver målorganisation.",
- "roleMappingRemoveRule": "Fjern",
- "idpGoogleConfiguration": "Google Konfiguration",
- "idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimationsoplysningerne",
- "idpGoogleClientIdDescription": "Google OAuth2-klient-ID",
- "idpGoogleClientSecretDescription": "Google OAuth2-klienten hemmelighed",
- "idpAzureConfiguration": "Azure Entra ID konfiguration",
- "idpAzureConfigurationDescription": "Konfigurer Azure Entra ID OAuth2 legitimationsoplysninger",
- "idpTenantId": "Leietaker-ID",
- "idpTenantIdPlaceholder": "lejer-id",
- "idpAzureTenantIdDescription": "Azure leant ID (fundet i Azure Active Directory-oversikten)",
- "idpAzureClientIdDescription": "Azure App registrerings klient-ID",
- "idpAzureClientSecretDescription": "Azure App Registrering Klient Hemmelig",
- "idpGoogleTitle": "Google",
- "idpGoogleAlt": "Google",
- "idpAzureTitle": "Azure Entra ID",
- "idpAzureAlt": "Azure",
- "idpGoogleConfigurationTitle": "Google Konfiguration",
- "idpAzureConfigurationTitle": "Azure Entra ID konfiguration",
- "idpTenantIdLabel": "Leietaker-ID",
- "idpAzureClientIdDescription2": "Azure App registrerings klient-ID",
- "idpAzureClientSecretDescription2": "Azure App Registrering Klient Hemmelig",
- "idpGoogleDescription": "Google OAuth2/OIDC udbyder",
- "idpAzureDescription": "Microsoft Azure OAuth2/OIDC leverandør",
- "subnet": "Subnet",
- "utilitySubnet": "Forsynings subnet",
- "subnetDescription": "Subnettet for denne organisations netværkskonfiguration.",
- "customDomain": "Brugerdefineret domæne",
- "authPage": "Autentiseringssider",
- "authPageDescription": "Sett et brugerdefineret domæne for organisationens autentiseringssider",
- "authPageDomain": "Autentiseringsside domæne",
- "authPageBranding": "Brugerdefineret merkevarebygging",
- "authPageBrandingDescription": "Konfigurer merkevarebyggingen der vises på autentiseringssidene for denne organisation",
- "authPageBrandingUpdated": "Autentiseringsside-markedsføring opdateret med succes",
- "authPageBrandingRemoved": "Autentiseringsside-markedsføring fjernet med succes",
- "authPageBrandingRemoveTitle": "Fjern markedsføring for autentiseringsside",
- "authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
- "authPageBrandingDeleteConfirm": "Bekræft sletning af merkevarebygging",
- "brandingLogoURL": "Logo URL",
- "brandingLogoURLOrPath": "Logoen URL eller sti",
- "brandingLogoPathDescription": "Indtast en URL eller en lokal sti.",
- "brandingLogoURLDescription": "Indtast en offentligt tilgængelig webadresse til din logobillede.",
- "brandingPrimaryColor": "Primærfarge",
- "brandingLogoWidth": "Bredde (px)",
- "brandingLogoHeight": "Høyde (px)",
- "brandingOrgTitle": "Titel for organisationens autentiseringsside",
- "brandingOrgDescription": "{orgName} vil blive erstattet med organisationens navn",
- "brandingOrgSubtitle": "Undertittel for organisationens autentiseringsside",
- "brandingResourceTitle": "Titel for ressourcens autentiseringsside",
- "brandingResourceSubtitle": "Undertittel for ressourcens autentiseringsside",
- "brandingResourceDescription": "{resourceName} vil blive erstattet med organisationens navn",
- "saveAuthPageDomain": "Gem domæne",
- "saveAuthPageBranding": "Gem merkevarebygging",
- "removeAuthPageBranding": "Fjern merkevarebygging",
- "noDomainSet": "Ingen domæne valgt",
- "changeDomain": "Ændre domæne",
- "selectDomain": "Vælg domæne",
- "restartCertificate": "Genstart certifikat",
- "editAuthPageDomain": "Rediger auth-sidens domæne",
- "setAuthPageDomain": "Angiv autoriseringsside domæne",
- "failedToFetchCertificate": "Kunne ikke hente certifikat",
- "failedToRestartCertificate": "Kan ikke starte certifikat",
- "addDomainToEnableCustomAuthPages": "Brugere vil kunne få adgang til organisationens loginside og fuldføre ressourceautentificering ved at bruge dette domænet.",
- "selectDomainForOrgAuthPage": "Vælg et domæne for organisationens autentiseringsside",
- "domainPickerProvidedDomain": "Givet domæne",
- "domainPickerFreeProvidedDomain": "Gratis angivet domæne",
- "domainPickerFreeDomainsPaidFeature": "Angitte domæner er en betalingsfunktion. Abonner for at få et domæne inklusive i din plan – ingen behov for at tage med dit eget.",
- "domainPickerVerified": "Bekræftet",
- "domainPickerUnverified": "Uverifisert",
- "domainPickerManual": "Manuel",
- "domainPickerInvalidSubdomainStructure": "Ugyldige tegn bliver saniteret, når de gemmes.",
- "domainPickerError": "Fejl",
- "domainPickerErrorLoadDomains": "Kan ikke indlæse organisationens domæner",
- "domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domænetilgængelighed",
- "domainPickerInvalidSubdomain": "Ugyldig underdomæne",
- "domainPickerInvalidSubdomainRemoved": "Inndata \"{sub}\" blev fjernet fordi det ikke er gyldig.",
- "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kunne ikke gøres gyldigt for {domain}.",
- "domainPickerSubdomainSanitized": "Underdomænet som blev sanivert",
- "domainPickerSubdomainCorrected": "\"{sub}\" var korrigert til \"{sanitized}\"",
- "orgAuthSignInTitle": "Organisationslogin",
- "orgAuthChooseIdpDescription": "Vælg din identitet udbyder for at fortsætte",
- "orgAuthNoIdpConfigured": "Denne organisation har ikke nogen identitetstjeneste konfigureret. Du kan i stedet logge ind med din Pangolin-identitet.",
- "orgAuthSignInWithPangolin": "Log ind med Pangolin",
- "orgAuthSignInToOrg": "Organisationens identitetsudbyder (SSO)",
- "orgAuthSelectOrgTitle": "Organisationslogin",
- "orgAuthSelectOrgDescription": "Indtast organisations-ID-en din for at fortsætte",
- "orgAuthOrgIdPlaceholder": "din-organisation",
- "orgAuthOrgIdHelp": "Indtast organisationens unikke identifikator",
- "orgAuthSelectOrgHelp": "Efter at have skrevet ind din organisations-ID, bliver du videresendt til din organisations loginside hvor du kan bruge SSO eller organisationens legitimationsoplysninger.",
- "orgAuthRememberOrgId": "Husk denne organisations-ID-en",
- "orgAuthBackToSignIn": "Tilbage til standard login",
- "orgAuthNoAccount": "Har du ikke en konto?",
- "subscriptionRequiredToUse": "Et abonnement er påkrævet for at bruge denne funktion.",
- "mustUpgradeToUse": "Du skal opgradere dit abonnement for at bruge denne funktion.",
- "subscriptionRequiredTierToUse": "Denne funktion kræver {tier} eller højere.",
- "upgradeToTierToUse": "Opgrader til {tier} eller højere for at bruge denne funktion.",
- "subscriptionTierTier1": "Hjem",
- "subscriptionTierTier2": "Lag",
- "subscriptionTierTier3": "Forretninger",
- "subscriptionTierEnterprise": "Bedrift",
- "idpDisabled": "Identitetsudbydere er deaktiveret.",
- "orgAuthPageDisabled": "Organisationens informationsside er deaktiveret.",
- "domainRestartedDescription": "Domæneverificeringen blev genstartet",
- "resourceAddEntrypointsEditFile": "Rediger fil: config/Traefik/Traefik_config.yml",
- "resourceExposePortsEditFile": "Rediger fil: docker-compose.yml",
- "emailVerificationRequired": "E-mailbekræftelse er nødvendig. Log ind igen via {dashboardUrl}/auth/login og fuldfør dette trinnet. Kom derefter tilbage her.",
- "twoFactorSetupRequired": "Opsætning af tofaktorgodkendelse er påkrævet. Log venligst ind igen via {dashboardUrl}/auth/login og fuldfør dette trin. Kom derefter tilbage hertil.",
- "additionalSecurityRequired": "Ekstra sikkerhed kræves",
- "organizationRequiresAdditionalSteps": "Denne organisation kræver yderligere sikkerhedstrin, før du får adgang til ressourcer.",
- "sessionExpired": "Session udløbet",
- "sessionExpiredReauthRequired": "Din session er udløbet i henhold til din organisations sikkerhedspolitik. Genbekræft venligst for at fortsætte.",
- "reauthenticate": "Re-autentificer",
- "completeTheseSteps": "Fuldfør disse trinene",
- "enableTwoFactorAuthentication": "Aktivér to-faktor autentificering",
- "completeSecuritySteps": "Fuldfør sikkerhedstrinene",
- "securitySettings": "Sikkerhed indstillinger",
- "dangerSection": "Farezone",
- "dangerSectionDescription": "Slet permanent alle data tilknyttet denne organisation",
- "securitySettingsDescription": "Konfigurer sikkerhedspolitikker for organisationen",
- "requireTwoFactorForAllUsers": "Kræv to-faktor autentificering for alle brugere",
- "requireTwoFactorDescription": "Når aktiveret skal alle interne brugere i denne organisation have to-faktorautentisering aktiveret for at få adgang til organisationen.",
- "requireTwoFactorDisabledDescription": "Denne funktion kræver en gyldig licens (Enterprise) eller aktivt abonnement (SaaS)",
- "requireTwoFactorCannotEnableDescription": "Du skal aktivere to-faktor-autentificering for din konto før det håndhæves for alle brugere",
- "maxSessionLength": "Maksimal øktlengde",
- "maxSessionLengthDescription": "Angiv maksimal varighed for brugersessioner. Efter denne periode skal brugerne logge ind igen.",
- "maxSessionLengthDisabledDescription": "Denne funktion kræver en gyldig licens (Enterprise) eller aktivt abonnement (SaaS)",
- "selectSessionLength": "Vælg øktlengde",
- "unenforced": "Tvungen",
- "1Hour": "1 time",
- "3Hours": "3 timer",
- "6Hours": "6 timer",
- "12Hours": "12 timer",
- "1DaySession": "1 dag",
- "3Days": "3 dage",
- "7Days": "7 dage",
- "14Days": "14 dage",
- "30DaysSession": "30 dage",
- "90DaysSession": "90 dage",
- "180DaysSession": "180 dage",
- "passwordExpiryDays": "Adgangskode udløber",
- "editPasswordExpiryDescription": "Angiv antal dage før brugere skal ændre adgangskoden sitt.",
- "selectPasswordExpiry": "Vælg adgangskodeudløb",
- "30Days": "30 dage",
- "1Day": "1 dag",
- "60Days": "60 dage",
- "90Days": "90 dage",
- "180Days": "180 dage",
- "1Year": "1 år",
- "subscriptionBadge": "Abonnement kræves",
- "securityPolicyChangeWarning": "Advarsel om ændring af sikkerhedsregler",
- "securityPolicyChangeDescription": "Du er ved at ændre indstillingerne for sikkerhedspolitik. Når du har gemt, skal du muligvis logge ind igen for at opfylde disse politikopdateringer. Alle brugere, der ikke matcher, skal også autentificere sig igen.",
- "securityPolicyChangeConfirmMessage": "Jeg bekræfter",
- "securityPolicyChangeWarningText": "Dette vil påvirke alle brugere i organisationen",
- "authPageErrorUpdateMessage": "Det opstod en fejl under opdatering af indstillingerne for godkendelsessiden",
- "authPageErrorUpdate": "Kunne ikke opdatere autoriseringssiden",
- "authPageDomainUpdated": "Autentiseringsside-domænet blev opdateret med succes",
- "healthCheckNotAvailable": "Lokal",
- "rewritePath": "Omskriv sti",
- "rewritePathDescription": "Valgfrit omskrive stien før videresendelse til målet.",
- "continueToApplication": "Fortsæt til applikationen",
- "checkingInvite": "Kontrollerer invitation",
- "setResourceHeaderAuth": "setResourceHeaderAuth",
- "resourceHeaderAuthRemove": "Fjern header-auth",
- "resourceHeaderAuthRemoveDescription": "Header autentificering fjernet.",
- "resourceErrorHeaderAuthRemove": "Kunne ikke fjerne header-autentificering",
- "resourceErrorHeaderAuthRemoveDescription": "Kunne ikke fjerne header-autentificering for ressourcen.",
- "resourceHeaderAuthProtectionEnabled": "Header autentificering aktiveret",
- "resourceHeaderAuthProtectionDisabled": "Header autentificering deaktiveret",
- "headerAuthRemove": "Fjern header-auth",
- "headerAuthAdd": "Tilføj header-godkendelse",
- "resourceErrorHeaderAuthSetup": "Kunne ikke angive header-autentificering",
- "resourceErrorHeaderAuthSetupDescription": "Kunne ikke sette topplinje autentificering for ressourcen.",
- "resourceHeaderAuthSetup": "Header godkendelsessæt var med succes",
- "resourceHeaderAuthSetupDescription": "Header-autentificering er blevet gemt.",
- "resourceHeaderAuthSetupTitle": "Angiv header-godkendelse",
- "resourceHeaderAuthSetupTitleDescription": "Angiv grunnleggende auth legitimationsoplysninger (brugernavn og adgangskode) for at beskytte denne ressource med HTTP Header autentificering. Adgang til det ved hjælp af formatet HTTPS://username:password@resource.example.com",
- "resourceHeaderAuthSubmit": "Angiv header-godkendelse",
- "actionSetResourceHeaderAuth": "Angiv header-godkendelse",
- "enterpriseEdition": "Enterprise-udgave",
- "unlicensed": "Ikke licenseret",
- "beta": "beta",
- "manageUserDevices": "Bruger Enheder",
- "manageUserDevicesDescription": "Se og administrer enheder som brugere bruger for privat forbindelse til ressourcer",
- "downloadClientBannerTitle": "Download Pangolin-klienten",
- "downloadClientBannerDescription": "Download Pangolin-klienten til dit system for at oprette forbindelse til Pangolin-netværket og få privat adgang til ressourcer.",
- "manageMachineClients": "Administrer maskinneklienter",
- "manageMachineClientsDescription": "Opret og administrer klienter som servere og systemer bruger for privat forbindelse til ressourcer",
- "machineClientsBannerTitle": "Servere og automatiserte systemer",
- "machineClientsBannerDescription": "Maskinklienter er for servere og automatiserte systemer der ikke er tilknyttet en specifik bruger. De autentiserer med en ID og et hemmelighetsnummer, og kan køre med Pangolin CLI, Olm CLI eller Olm som en container.",
- "machineClientsBannerPangolinCLI": "Pangolin CLI",
- "machineClientsBannerOlmCLI": "Olm CLI",
- "machineClientsBannerOlmContainer": "Olm Container",
- "clientsTableUserClients": "Bruger",
- "clientsTableMachineClients": "Maskin",
- "licenseTableValidUntil": "Gyldig til",
- "saasLicenseKeysSettingsTitle": "Bedriftstillatelse Licenser",
- "saasLicenseKeysSettingsDescription": "Generer og administrer Enterprise licensnøgler for selvbetjente Pangolin forekomster",
- "sidebarEnterpriseLicenses": "Licenser",
- "generateLicenseKey": "Generér licensnøgle",
- "generateLicenseKeyForm": {
- "validation": {
- "emailRequired": "Indtast venligst en gyldig e-mailadresse",
- "useCaseTypeRequired": "Vælg venligst en brugstype",
- "firstNameRequired": "Fornavn er påkrævet",
- "lastNameRequired": "Efternavn er påkrævet",
- "primaryUseRequired": "Beskriv din primære brug",
- "jobTitleRequiredBusiness": "Jobtitel er påkrævet til erhvervsbrug",
- "industryRequiredBusiness": "Branche skal til forretningsbruk",
- "stateProvinceRegionRequired": "Stat/provins/region er påkrævet",
- "postalZipCodeRequired": "Postnummer er påkrævet",
- "companyNameRequiredBusiness": "Firmanavn er påkrævet til erhvervsbrug",
- "countryOfResidenceRequiredBusiness": "Bopælsland er påkrævet til erhvervsbrug",
- "countryRequiredPersonal": "Land er påkrævet for personlig brug",
- "agreeToTermsRequired": "Du skal acceptere vilkårene",
- "complianceConfirmationRequired": "Du skal bekræfte at du overholder Fossorial Kommerciel licens"
- },
- "useCaseOptions": {
- "personal": {
- "title": "Personlig brug",
- "description": "For enkeltpersoner, ikke-kommerciel brug som læring, personlige prosjekter eller eksperimentering."
- },
- "business": {
- "title": "Erhvervsbrug",
- "description": "Til brug inden for organisationer eller virksomheter eller forretningsmessige indtægter eller aktiviteter."
- }
- },
- "steps": {
- "emailLicenseType": {
- "title": "E-mail og licenstype",
- "description": "Indtast din e-mailadresse og vælg licenstypen din"
- },
- "personalInformation": {
- "title": "Personlige oplysninger",
- "description": "Fortell oss om dig selv"
- },
- "contactInformation": {
- "title": "Kontaktoplysninger",
- "description": "Dine kontaktopplysninger"
- },
- "termsGenerate": {
- "title": "Vilkår og generering",
- "description": "Se gennem og acceptere vilkårene for at generere licensen"
- }
- },
- "alerts": {
- "commercialUseDisclosure": {
- "title": "Brug utlevering",
- "description": "Vælg licensniveauet som reflekterer præcis din tiltænkte brug. Personlige licenser tillader fri brug af software for enkelte, ikke-kommercielle eller småskala kommercielle aktiviteter, med en årlig brutto indtægt på under 100 000 amerikanske dollar. All brug ud over disse grænserne – inklusive brug inden for en virksomhed, organisation eller andre indtægter miljø - kræver en gyldig Enterprise licens og betaling af nuværende licensafgift. Alle brugere, enten personlig eller Enterprise, skal overholde de kommercielle tilladelserne på Fossorial."
- },
- "trialPeriodInformation": {
- "title": "Information om prøveperiode",
- "description": "Denne licensnøglen tillader funktioner i Enterprise for en 7-dagers evalueringsperiode. Stadig adgang til betalt funktioner uden for evalueringsperioden kræver aktivering under en gyldig Personlig eller Enterprise License. For Enterprise licensing, kontakt sales@pangolin.net."
- }
- },
- "form": {
- "useCaseQuestion": "Bruger du Pangolin for personlig eller forretningsbruk?",
- "firstName": "Fornavn",
- "lastName": "Efternavn",
- "jobTitle": "Jobtitel",
- "primaryUseQuestion": "Hvad planlegger du først og fremst at bruge Pangolin for?",
- "industryQuestion": "Hvilken branche er du i?",
- "prospectiveUsersQuestion": "Hvor mange prospektive brugere forventer du at have?",
- "prospectiveSitesQuestion": "Hvor mange prospektive sites (tunnels) forventer du at have?",
- "companyName": "Virksomhedsnavn",
- "countryOfResidence": "Bopælsland",
- "stateProvinceRegion": "Stat/provins/region",
- "postalZipCode": "Postnummer",
- "companyWebsite": "Virksomhedens hjemmeside",
- "companyPhoneNumber": "Virksomhedens telefonnummer",
- "country": "Land",
- "phoneNumberOptional": "Telefonnummer (valgfrit)",
- "complianceConfirmation": "Jeg bekræfter, at de oplysninger, jeg har angivet, er korrekte, og at jeg overholder Fossorials kommercielle licens. Indberetning af unøjagtige oplysninger eller forkert angivelse af produktbrug bryder licensen og kan føre til, at din nøgle tilbagekaldes."
- },
- "buttons": {
- "close": "Luk",
- "previous": "Forrige",
- "next": "Næste",
- "generateLicenseKey": "Generér licensnøgle"
- },
- "toasts": {
- "success": {
- "title": "Licensnøgle blev genereret",
- "description": "Din licensnøgle er blevet genereret og er klar til brug."
- },
- "error": {
- "title": "Kan ikke generere licensnøgle",
- "description": "Det opstod en fejl ved generering af licensnøglen."
- }
- }
- },
- "newPricingLicenseForm": {
- "title": "Få en licens",
- "description": "Vælg en plan, og fortæl os, hvordan du planlægger at bruge Pangolin.",
- "chooseTier": "Vælg din funktionsplan",
- "viewPricingLink": "Se priser, funktioner og grænser",
- "tiers": {
- "starter": {
- "title": "Starter",
- "description": "Enterprise features, 25 brugere, 25 sites og støtte fra fællesskabet."
- },
- "scale": {
- "title": "Skala",
- "description": "Enterprise-funktioner, 50 brugere, 100 sites og prioriteret support."
- }
- },
- "personalUseOnly": "Kun personlig brug (gratis licens - ingen checkout)",
- "buttons": {
- "continueToCheckout": "Fortsæt til checkout"
- },
- "toasts": {
- "checkoutError": {
- "title": "Fejl ved checkout",
- "description": "Kan ikke starte checkout. Prøv igen."
- }
- }
- },
- "priority": "Prioritet",
- "priorityDescription": "Ruter med højere prioritet evalueres først. Prioritet = 100 betyder automatisk rækkefølge (systembeslutninger). Brug et andet tal for at gennemtvinge manuel prioritet.",
- "instanceName": "Forekomst navn",
- "pathMatchModalTitle": "Konfigurere matching af sti",
- "pathMatchModalDescription": "Opsæt hvordan indgående forespørgsler skal matches baseret på deres sti.",
- "pathMatchType": "Matchtype",
- "pathMatchPrefix": "Prefiks",
- "pathMatchExact": "Præcis",
- "pathMatchRegex": "Regex",
- "pathMatchValue": "Værdi for sti",
- "clear": "Tøm",
- "saveChanges": "Gem ændringer",
- "pathMatchRegexPlaceholder": "^/api/.*",
- "pathMatchDefaultPlaceholder": "/sti",
- "pathMatchPrefixHelp": "Eksempel: /api matcher /api, /api/users, etc.",
- "pathMatchExactHelp": "Eksempel: /api passer kun /api",
- "pathMatchRegexHelp": "Eksempel: ^/api/.* matcher /api/alt",
- "pathRewriteModalTitle": "Konfigurer ferdigskriving af sti",
- "pathRewriteModalDescription": "Overfør den matchende sti, før den videresendes til målet.",
- "pathRewriteType": "Omskriv type",
- "pathRewritePrefixOption": "Prefiks - erstatt prefiks",
- "pathRewriteExactOption": "Eksakt - Erstatt hele banen",
- "pathRewriteRegexOption": "Regex - Ekkobilde",
- "pathRewriteStripPrefixOption": "Tage bort prefiks - fjern prefiks",
- "pathRewriteValue": "Omskriv værdi",
- "pathRewriteRegexPlaceholder": "/ny/$1",
- "pathRewriteDefaultPlaceholder": "/ny sti",
- "pathRewritePrefixHelp": "Erstatt det matchende prefikset med denne værdien",
- "pathRewriteExactHelp": "Erstatt hele banen med denne værdien når stien matcher præcis",
- "pathRewriteRegexHelp": "Brug capture groups som $1, $2 til erstatning",
- "pathRewriteStripPrefixHelp": "Lad feltet være tomt for at fjerne præfikset eller tilføje et nyt præfiks",
- "pathRewritePrefix": "Prefiks",
- "pathRewriteExact": "Præcis",
- "pathRewriteRegex": "Regex",
- "pathRewriteStrip": "Stripe",
- "pathRewriteStripLabel": "stripe",
- "sidebarEnableEnterpriseLicense": "Aktivér Enterprise licens",
- "cannotbeUndone": "Dette kan ikke fortrydes.",
- "toConfirm": "at bekræfte.",
- "deleteClientQuestion": "Er du sikker på at du vil fjerne klienten fra sitet og organisationen?",
- "clientMessageRemove": "Når klienten er fjernet, kan den ikke længere oprette forbindelse til sitet.",
- "sidebarLogs": "Logfiler",
- "request": "Forespørgsel",
- "requests": "Forespørgsler",
- "logs": "Logfiler",
- "logsSettingsDescription": "Overvåg logs samlet ind fra denne organisation",
- "searchLogs": "Søg i logs...",
- "action": "Handling",
- "actor": "Aktør",
- "timestamp": "Tidsstempel",
- "accessLogs": "Adgangslogs",
- "exportCsv": "Eksportere CSV",
- "exportError": "Ukjent fejl ved eksport af CSV",
- "exportCsvTooltip": "Inden for tidsramme",
- "actorId": "Skuespiller ID",
- "allowedByRule": "Tilladt efter regel",
- "allowedNoAuth": "Tilladt Ingen Auth",
- "validAccessToken": "Gyldig adgangsnøgle",
- "validHeaderAuth": "Gyldig header-auth",
- "validPincode": "Valid PIN-kode",
- "validPassword": "Gyldig adgangskode",
- "validEmail": "Gyldig e-mail",
- "validSSO": "Gyldig SSO",
- "view": "Vis",
- "configManaged": "Konfiguration administrert",
- "connectedClient": "Tilsluttet klient",
- "resourceBlocked": "Ressource blokeret",
- "droppedByRule": "Legg i reglen",
- "noSessions": "Ingen økter",
- "temporaryRequestToken": "Midlertidig forespørgsel Token",
- "noMoreAuthMethods": "Ingen gyldig auth",
- "ip": "IP",
- "reason": "Grund",
- "requestLogs": "HTTP-forespørgselslogs",
- "requestAnalytics": "Be om analyser",
- "host": "Vært",
- "location": "Sted",
- "actionLogs": "Handlingsloger",
- "sidebarLogsRequest": "HTTP-forespørgselslogs",
- "sidebarLogsAccess": "Adgangslogs",
- "sidebarLogsAction": "Handlingsloger",
- "logRetention": "Logopbevaring",
- "logRetentionDescription": "Håndter hvor længe ulike typer logs beholdes for denne organisation, eller deaktivér dem",
- "requestLogsDescription": "Se detaljerede forespørgselslogs for ressourcer i denne organisation",
- "requestAnalyticsDescription": "Se detaljeret anmodningsanalyse for ressourcer i denne organisation",
- "logRetentionRequestLabel": "Be om logbevaring",
- "logRetentionRequestDescription": "Hvor længe du vil beholde forespørgselslogs",
- "logRetentionAccessLabel": "Få adgang til logoverføring",
- "logRetentionAccessDescription": "Hvor længe du vil beholde adgangsloger",
- "logRetentionActionLabel": "Handlings log nytt",
- "logRetentionActionDescription": "Hvor længe handlingen skal gemmes",
- "logRetentionConnectionLabel": "Forbindelseslog",
- "logRetentionConnectionDescription": "Hvor længe du vil beholde forbindelseslogs",
- "logRetentionDisabled": "Deaktiveret",
- "logRetention3Days": "3 dage",
- "logRetention7Days": "7 dage",
- "logRetention14Days": "14 dage",
- "logRetention30Days": "30 dage",
- "logRetention90Days": "90 dage",
- "logRetentionForever": "Altid",
- "logRetentionEndOfFollowingYear": "Slutt på næste år",
- "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisation",
- "accessLogsDescription": "Vis autorisationsforespørgsler for ressourcer i denne organisation",
- "connectionLogs": "Forbindelseslogs",
- "connectionLogsDescription": "Vis forbindelseslogs for tunneler i denne organisation",
- "sidebarLogsConnection": "Forbindelseslogs",
- "sidebarLogsStreaming": "Streaming",
- "sourceAddress": "Kildeadresse",
- "destinationAddress": "Måladresse ",
- "duration": "Varighed",
- "licenseRequiredToUse": "En Enterprise Edition-licens eller Pangolin Cloud er påkrævet for at bruge denne funktion. Book en demo eller POC-prøveversion.",
- "ossEnterpriseEditionRequired": "Enterprise Edition er nødvendig for at bruge denne funktion. Denne funktion er også tilgængelig i Pangolin Cloud. Book en demo eller POC-prøve.",
- "certResolver": "Certifikatløser",
- "certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.",
- "selectCertResolver": "Vælg certifikatløser",
- "enterCustomResolver": "Indtast brugerdefineret resolver",
- "preferWildcardCert": "Foretrekk Wildcard certifikat",
- "unverified": "Uverifisert",
- "domainSetting": "Domæne indstillinger",
- "domainSettingDescription": "Konfigurer indstillinger for domænet",
- "preferWildcardCertDescription": "Forsøg at generere et wildcard-certifikat (kræver en korrekt konfigureret certifikatløser).",
- "recordName": "Gem navn",
- "auto": "Automatisk",
- "TTL": "TTL",
- "howToAddRecords": "Hvordan tilføje poster",
- "dnsRecord": "DNS registre",
- "required": "Påkrævet",
- "domainSettingsUpdated": "Domæne indstillinger blev opdateret",
- "orgOrDomainIdMissing": "ID for organisation eller domæne mangler",
- "loadingDNSRecords": "Indlæser DNS-poster...",
- "olmUpdateAvailableInfo": "En opdateret version af Olm er tilgængeligt. Opdater til den nyeste version for at få den bedste oplevelse.",
- "updateAvailableInfo": "En opdateret version er tilgængelig. Opdater til den nyeste version for at få den bedste oplevelse.",
- "client": "Klient",
- "proxyProtocol": "Protokol indstillinger for Protokol",
- "proxyProtocolDescription": "Konfigurer Proxy-protokol for at bevare klientens IP-adresser til TCP-tjenester.",
- "enableProxyProtocol": "Aktivér Proxy-protokol",
- "proxyProtocolInfo": "Bevar klientens IP-adresser for TCP backends",
- "proxyProtocolVersion": "Proxy protokol version",
- "version1": "Version 1 (Anbefalet)",
- "version2": "Version 2",
- "version1Description": "Tekstbaseret og bredt støttet. Sørg for at servertransport er tilføjet dynamisk konfiguration.",
- "version2Description": "Binært og mere effektivt, men mindre kompatibel. Sørg for at servertransport er tilføjet dynamisk konfiguration.",
- "warning": "Advarsel",
- "proxyProtocolWarning": "Backend-applikationen skal konfigureres til at acceptere forbindelser med Proxy Protocol. Hvis backenden ikke understøtter Proxy Protocol, vil aktivering af dette ødelægge alle forbindelser. Aktivér derfor kun dette, hvis du ved, hvad du gør. Sørg for at konfigurere backenden til at stole på Proxy Protocol-headers fra Traefik.",
- "restarting": "Restarter...",
- "manual": "Manuel",
- "messageSupport": "Støtte for meddelelse",
- "supportNotAvailableTitle": "Støtte ikke tilgængelig",
- "supportNotAvailableDescription": "Støtte er ikke tilgængelig akkurat nu. Du kan sende en e-mail til support@pangolin.net.",
- "supportRequestSentTitle": "Supportforespørgsel sendt",
- "supportRequestSentDescription": "Din meddelelse er sendt.",
- "supportRequestFailedTitle": "Kunne ikke sende forespørgsel",
- "supportRequestFailedDescription": "En fejl opstod under sending af din forespørgsel om støtte.",
- "supportSubjectRequired": "Emne er påkrævet",
- "supportSubjectMaxLength": "Emne skal være 255 tegn eller mindre",
- "supportMessageRequired": "Meddelelse er påkrævet",
- "supportReplyTo": "Svar til",
- "supportSubject": "Emne",
- "supportSubjectPlaceholder": "Angiv emne",
- "supportMessage": "Meddelelse",
- "supportMessagePlaceholder": "Skriv din meddelelse",
- "supportSending": "Sender...",
- "supportSend": "Sende",
- "supportMessageSent": "Meddelelse sendt!",
- "supportWillContact": "Vi kommer raskt til at tage kontakt!",
- "selectLogRetention": "Vælg logopbevaring",
- "terms": "Vilkår",
- "privacy": "Privatliv",
- "security": "Sikkerhed",
- "docs": "Dokumenter",
- "deviceActivation": "Aktivering af enhed",
- "deviceCodeInvalidFormat": "Kode skal inneholde 9 tegn (f.eks A1AJ-N5JD)",
- "deviceCodeInvalidOrExpired": "Ugyldig eller udløbet kode",
- "deviceCodeVerifyFailed": "Kunne ikke bekræfte enhedskoden",
- "deviceCodeValidating": "Validerer enhedskode...",
- "deviceCodeVerifying": "Bekræfter enhedens godkendelse...",
- "signedInAs": "Logget ind som",
- "deviceCodeEnterPrompt": "Indtast koden, der vises på enheden",
- "continue": "Fortsæt",
- "deviceUnknownLocation": "Ukjent plassering",
- "deviceAuthorizationRequested": "Denne godkendelse blev anmodet fra {location} den {date}. Sørg for, at du stoler på denne enhed, da den får adgang til kontoen.",
- "deviceLabel": "Enhed: {deviceName}",
- "deviceWantsAccess": "ønsker at få adgang til din konto",
- "deviceExistingAccess": "Eksisterende adgang:",
- "deviceFullAccess": "Full adgang til din konto",
- "deviceOrganizationsAccess": "Adgang til alle organisationer din konto har adgang til",
- "deviceAuthorize": "Autoriser {applicationName}",
- "deviceConnected": "Enhed tilsluttet!",
- "deviceAuthorizedMessage": "Enheden er autoriseret til adgang til kontoen. Gå venligst tilbage til klientapplikationen.",
- "pangolinCloud": "Pangolin Cloud",
- "viewDevices": "Vis enheder",
- "viewDevicesDescription": "Administrer tilsluttede enheder",
- "noDevices": "Ingen enheder fundet",
- "dateCreated": "Oprettet dato",
- "unnamedDevice": "Navnløs enhed",
- "deviceQuestionRemove": "Er du sikker på, at du vil slette denne enhed?",
- "deviceMessageRemove": "Denne handling kan ikke fortrydes.",
- "deviceDeleteConfirm": "Slet enhed",
- "deleteDevice": "Slet enhed",
- "errorLoadingDevices": "Fejl ved indlæsning af enheder",
- "failedToLoadDevices": "Kunne ikke indlæse enheder",
- "deviceDeleted": "Enheden er slettet",
- "deviceDeletedDescription": "Enheden er blevet slettet.",
- "errorDeletingDevice": "Fejl ved sletning af enhed",
- "failedToDeleteDevice": "Kunne ikke slette enheden",
- "showColumns": "Vis kolonner",
- "hideColumns": "Skjul kolonner",
- "columnVisibility": "Kolonne Synlighed",
- "toggleColumn": "Veksle {columnName} kolonne",
- "allColumns": "Alle kolonner",
- "defaultColumns": "Standard kolonner",
- "customizeView": "Tilpass visning",
- "viewOptions": "Vis alternativer",
- "selectAll": "Vælg alle",
- "selectNone": "Vælg ingen",
- "selectedResources": "Valgte ressourcer",
- "enableSelected": "Aktivér valgte",
- "disableSelected": "Deaktivér valgte",
- "checkSelectedStatus": "Kontroller status for valgte",
- "clients": "Klienter",
- "accessClientSelect": "Vælg maskinklienter",
- "resourceClientDescription": "Maskinklienter som har adgang til denne ressource",
- "regenerate": "Regenerer",
- "credentials": "Legitimationsoplysninger",
- "savecredentials": "Gem brugeroplysninger",
- "regenerateCredentialsButton": "Regenerer brugeroplysninger",
- "regenerateCredentials": "Regenerer brugeroplysninger",
- "generatedcredentials": "Genererede brugeroplysninger",
- "copyandsavethesecredentials": "Kopiér og gem disse oplysningerne",
- "copyandsavethesecredentialsdescription": "Disse oplysninger vil ikke blive vist igen, når du forlader siden. Gem dem sikkert nu.",
- "credentialsSaved": "Loginoplysninger gemt",
- "credentialsSavedDescription": "Loginoplysningerne er blevet regenereret og gemt.",
- "credentialsSaveError": "Fejl ved gemning af loginoplysninger",
- "credentialsSaveErrorDescription": "En fejl opstod under regenerering og lagring af legitimationsoplysninger.",
- "regenerateCredentialsWarning": "Regenerering af legitimationsoplysninger vil ugyldiggøre de forrige og forårsage en frakobling. Sørg for at opdatere alle konfigurationer, der bruger disse legitimationsoplysninger.",
- "confirm": "Bekræft",
- "regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimationerne?",
- "endpoint": "Endpoint",
- "Id": "Id",
- "SecretKey": "Hemmelig nøgle",
- "niceId": "God ID",
- "niceIdUpdated": "Flott ID opdateret",
- "niceIdUpdatedSuccessfully": "Id-en blev opdateret",
- "niceIdUpdateError": "Fejl under opdatering af hyggelig ID",
- "niceIdUpdateErrorDescription": "Det opstod en fejl under opdatering af Nice ID.",
- "niceIdCannotBeEmpty": "God ID kan ikke være tom",
- "enterIdentifier": "Angiv identifikator",
- "identifier": "Identifikator",
- "deviceLoginUseDifferentAccount": "Ikke du? Brug en anden konto.",
- "deviceLoginDeviceRequestingAccessToAccount": "En enhed ber om adgang til denne kontoen.",
- "loginSelectAuthenticationMethod": "Vælg en autentificeringsmetode for at fortsætte.",
- "noData": "Ingen data",
- "machineClients": "Maskinklienter",
- "install": "Installer",
- "run": "Kjør",
- "envFile": "Miljøfil",
- "serviceFile": "Tjenestefil",
- "enableAndStart": "Aktivér og start",
- "clientNameDescription": "Visningsnavnet til klienten, der kan endres senere.",
- "clientAddress": "Klientadresse (avanceret)",
- "setupFailedToFetchSubnet": "Kunne ikke hente standard subnet",
- "setupSubnetAdvanced": "Subnet (avanceret)",
- "setupSubnetDescription": "Subnet for denne organisations interne netværk.",
- "setupUtilitySubnet": "Utility Subnet (Avanceret)",
- "setupUtilitySubnetDescription": "Subnettet for denne organisations aliasadresser og DNS-server.",
- "siteRegenerateAndDisconnect": "Regenerer og frakobl",
- "siteRegenerateAndDisconnectConfirmation": "Er du sikker på, at du vil regenerere legitimationsoplysninger og frakoble dette site?",
- "siteRegenerateAndDisconnectWarning": "Dette vil regenerere legitimationsoplysninger og straks frakoble sitet. Sitet skal startes igen med de nye legitimationsoplysninger.",
- "siteRegenerateCredentialsConfirmation": "Er du sikker på, at du vil regenerere legitimationsoplysninger for dette site?",
- "siteRegenerateCredentialsWarning": "Dette vil regenerere legitimationsoplysningerne. Sitet forbliver tilsluttet, indtil du manuelt genstarter det og bruger de nye legitimationsoplysninger.",
- "clientRegenerateAndDisconnect": "Regenerer og frakobl",
- "clientRegenerateAndDisconnectConfirmation": "Er du sikker på, at du vil regenerere legitimationsoplysninger og frakoble denne klient?",
- "clientRegenerateAndDisconnectWarning": "Dette vil regenerere legitimationsoplysninger og straks frakoble klienten. Klienten skal startes igen med de nye legitimationsoplysninger.",
- "clientRegenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legitimationsoplysninger for denne klienten?",
- "clientRegenerateCredentialsWarning": "Dette vil regenerere legitimationsoplysninger. Klienten forbliver tilsluttet, indtil du manuelt genstarter den og bruger de nye legitimationsoplysninger.",
- "remoteExitNodeRegenerateAndDisconnect": "Regenerer og frakobl",
- "remoteExitNodeRegenerateAndDisconnectConfirmation": "Er du sikker på, at du vil regenerere legitimationsoplysninger og frakoble denne eksterne exitnode?",
- "remoteExitNodeRegenerateAndDisconnectWarning": "Dette vil regenerere loginoplysningerne og straks frakoble den eksterne exitnode. Den eksterne exitnode skal startes igen med de nye oplysninger.",
- "remoteExitNodeRegenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere loginoplysningerne for denne eksterne avslutningen?",
- "remoteExitNodeRegenerateCredentialsWarning": "Dette vil regenerere legitimationsoplysninger. Den eksterne exitnode forbliver tilsluttet, indtil du manuelt genstarter den og bruger de nye legitimationsoplysninger.",
- "agent": "Agent",
- "personalUseOnly": "Kun til personlig brug",
- "loginPageLicenseWatermark": "Denne instansen er licenseret kun for personlig brug.",
- "instanceIsUnlicensed": "Denne instans er ulicenseret.",
- "portRestrictions": "Portbegrensninger",
- "allPorts": "Alle",
- "custom": "Brugerdefineret",
- "allPortsAllowed": "Alle porte tilladt",
- "allPortsBlocked": "Alle porte blokeret",
- "tcpPortsDescription": "Angiv hvilke TCP-porte, der er tilladt for denne ressource. Brug '*' for alle porte, lad feltet stå tomt for at blokere alle, eller indtast en kommasepareret liste over porte og intervaller (f.eks. 80,443,8000-9000).",
- "udpPortsDescription": "Angiv hvilke UDP-porte, der er tilladt for denne ressource. Brug '*' for alle porte, lad feltet stå tomt for at blokere alle, eller indtast en kommasepareret liste over porte og intervaller (f.eks. 53,123,500-600).",
- "organizationLoginPageTitle": "Organisationens loginside",
- "organizationLoginPageDescription": "Tilpass loginsiden for denne organisation",
- "resourceLoginPageTitle": "Ressourcens loginside",
- "resourceLoginPageDescription": "Tilpass loginsiden for individuelle ressourcer",
- "enterConfirmation": "Indtast bekræftelse",
- "blueprintViewDetails": "Detaljer",
- "defaultIdentityProvider": "Standard identitetsudbyder",
- "defaultIdentityProviderDescription": "Brugeren vil automatisk blive videresendt til denne identitetsudbyder for autentificering.",
- "editInternalResourceDialogNetworkSettings": "Netværksindstillinger",
- "editInternalResourceDialogAccessPolicy": "Adgangsregler for adgang",
- "editInternalResourceDialogAddRoles": "Tilføj roller",
- "editInternalResourceDialogAddUsers": "Tilføj brugere",
- "editInternalResourceDialogAddClients": "Tilføj klienter",
- "editInternalResourceDialogDestinationLabel": "Destination",
- "editInternalResourceDialogDestinationDescription": "Konfigurer hvordan klienter får adgang til denne ressource.",
- "internalResourceFormMultiSiteRoutingHelp": "Valg af flere sites muliggør robust routing og failover for høj tilgængelighed.",
- "internalResourceFormMultiSiteRoutingHelpLearnMore": "Læs mere",
- "editInternalResourceDialogPortRestrictionsDescription": "Begrens adgang til specifikke TCP/UDP-porte eller tillade/blokere alle porte.",
- "createInternalResourceDialogHttpConfiguration": "HTTP-konfiguration",
- "createInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
- "editInternalResourceDialogHttpConfiguration": "HTTP-konfiguration",
- "editInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
- "editInternalResourceDialogTcp": "TCP",
- "editInternalResourceDialogUdp": "UDP",
- "editInternalResourceDialogIcmp": "ICMP",
- "editInternalResourceDialogAccessControl": "Adgangskontrol",
- "editInternalResourceDialogAccessControlDescription": "Kontroller hvilke roller, brugere og maskinklienter som har adgang til denne ressource når den er forbundet til. Administratorer har altid adgang.",
- "editInternalResourceDialogPortRangeValidationError": "Portintervallet skal være \"*\" for alle porte eller en kommasepareret liste med porte og intervaller (f.eks. \"80,443,8000-9000\"). Porte skal være mellem 1 og 65535.",
- "internalResourceAuthDaemonStrategy": "SSH Auth Daemon Sted",
- "internalResourceAuthDaemonStrategyDescription": "Vælg, hvor SSH-autentificeringsdaemonen kører: på sitet (Newt) eller på en ekstern host.",
- "internalResourceAuthDaemonDescription": "SSH-godkendelsesdaemonen håndterer SSH-nøglesignering og PAM-autentificering for denne ressource. Vælg, om den kører på sitet (Newt) eller på en separat ekstern host. Se dokumentationen for mere.",
- "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
- "internalResourceAuthDaemonStrategyPlaceholder": "Vælg strategi",
- "internalResourceAuthDaemonStrategyLabel": "Sted",
- "internalResourceAuthDaemonSite": "På site",
- "internalResourceAuthDaemonSiteDescription": "Autentificeringsdaemonen kører på sitet (Newt).",
- "internalResourceAuthDaemonRemote": "Ekstern host",
- "internalResourceAuthDaemonRemoteDescription": "Autentificeringsdaemonen kører på en host, som ikke er sitet.",
- "internalResourceAuthDaemonPort": "Daemon Port (valgfrit)",
- "orgAuthWhatsThis": "Hvor kan jeg finne min organisations-ID?",
- "learnMore": "Læs mere",
- "backToHome": "Gå tilbage til start",
- "needToSignInToOrg": "Behøver du at bruge organisationens identitetsudbyder?",
- "maintenanceMode": "Vedligeholdelsesside",
- "maintenanceModeDescription": "Vis en vedligeholdelsesside til besøgende",
- "maintenanceModeType": "Vedligeholdelsestilstandstype",
- "showMaintenancePage": "Vis en vedligeholdelsesside til besøgende",
- "enableMaintenanceMode": "Aktivér vedligeholdelsestilstand",
- "enableMaintenanceModeDescription": "Når dette er aktiveret, vil besøgende se en vedligeholdelsesside i stedet for din ressource.",
- "automatic": "Automatisk",
- "automaticModeDescription": "Vis kun vedligeholdelsessiden, når alle serverens mål er nede eller usunde. Din ressource fortsætter med at fungere normalt, så længe mindst ét mål er sundt.",
- "forced": "Tvunget",
- "forcedModeDescription": "Vis altid vedligeholdelsessiden uafhængigt af serverens sundhed. Brug dette ved planlagt vedligeholdelse, når du vil forhindre al adgang.",
- "warning:": "Advarsel:",
- "forcedeModeWarning": "Al trafik vil blive dirigeret til vedligeholdelsessiden. Serverens ressourcer vil ikke modtage nogen forespørgsler.",
- "pageTitle": "Sidetittel",
- "maintenancePageContentSubsection": "Sideindhold",
- "maintenancePageContentSubsectionDescription": "Tilpas indholdet, der vises på vedligeholdelsessiden",
- "pageTitleDescription": "Hovedoverskriften, der vises på vedligeholdelsessiden",
- "maintenancePageMessage": "Vedligeholdelsesbesked",
- "maintenancePageMessagePlaceholder": "Vi kommer snart tilbage! Vores site gennemgår i øjeblikket planlagt vedligeholdelse.",
- "maintenancePageMessageDescription": "Detaljeret besked, der forklarer vedligeholdelsen",
- "maintenancePageTimeTitle": "Estimert ferdigstillelsestid (Valgfrit)",
- "privateMaintenanceScreenTitle": "Privat plassholder skærm",
- "privateMaintenanceScreenMessage": "Dette domænet bruges på en privat ressource. Opret forbindelse til ved at bruge Pangolin-klienten for at få adgang til denne ressource.",
- "privateMaintenanceScreenSteps": "Hvis du stadig ser denne meddelelse, når du er forbundet, peger browserens DNS-cache muligvis stadig på den gamle adresse. For at rette det: Luk og åbn denne fane eller browseren igen, og gå derefter tilbage til denne side.",
- "maintenanceTime": "f.eks. 2 timer, 1. november kl. 17:00",
- "maintenanceEstimatedTimeDescription": "Hvornår du forventer, at vedligeholdelsen er færdig",
- "editDomain": "Rediger domæne",
- "editDomainDescription": "Vælg et domæne for din ressource",
- "maintenanceModeDisabledTooltip": "Denne funktion kræver en gyldig licens for at aktiveres.",
- "maintenanceScreenTitle": "Tjenesten er midlertidigt utilgængelig",
- "maintenanceScreenMessage": "Vi oplever i øjeblikket tekniske problemer. Tjek venligst igen snart.",
- "maintenanceScreenEstimatedCompletion": "Estimert ferdigstillelse:",
- "createInternalResourceDialogDestinationRequired": "Destinationen er nødvendig",
- "available": "Tilgængelig",
- "disabledResourceDescription": "Når ressourcen er deaktiveret, vil den være utilgængelig for alle.",
- "archived": "Arkiveret",
- "noArchivedDevices": "Ingen arkiverede enheder fundet",
- "deviceArchived": "Enhed arkiveret",
- "deviceArchivedDescription": "Enheden er blevet arkiveret.",
- "errorArchivingDevice": "Fejl ved arkivering af enhed",
- "failedToArchiveDevice": "Kunne ikke arkivere enhed",
- "deviceQuestionArchive": "Er du sikker på, at du vil arkivere denne enhed?",
- "deviceMessageArchive": "Enheden bliver arkiveret og fjernet fra listen over aktive enheder.",
- "deviceArchiveConfirm": "Arkiver enhed",
- "archiveDevice": "Arkiver enhed",
- "archive": "Arkiv",
- "deviceUnarchived": "Enheden er fjernet fra arkivet",
- "deviceUnarchivedDescription": "Enheden er blevet fjernet fra arkivet.",
- "errorUnarchivingDevice": "Fejl ved arkivering af enhed",
- "failedToUnarchiveDevice": "Kunne ikke fjerne enheden fra arkivet",
- "unarchive": "Avarkiver",
- "archiveClient": "Arkiver klient",
- "archiveClientQuestion": "Er du sikker på at du vil arkivere denne klienten?",
- "archiveClientMessage": "Klienten arkiveres og fjernes fra listen over aktive klienter.",
- "archiveClientConfirm": "Arkiver klient",
- "blockClient": "Bloker kunde",
- "blockClientQuestion": "Er du sikker på at du vil blokere denne klienten?",
- "blockClientMessage": "Enheden bliver tvunget til at frakoble, hvis den er forbundet. Du kan fjerne blokeringen af enheden senere.",
- "blockClientConfirm": "Bloker kunde",
- "active": "Aktiv",
- "usernameOrEmail": "Brugernavn eller e-mail",
- "selectYourOrganization": "Vælg din organisation",
- "signInTo": "Log ind på",
- "signInWithPassword": "Fortsæt med adgangskode",
- "noAuthMethodsAvailable": "Ingen autentificeringsmetoder er tilgængelige for denne organisation.",
- "enterPassword": "Angiv dit adgangskode",
- "enterMfaCode": "Angiv koden fra din godkendelsesapp",
- "securityKeyRequired": "Venligst brug sikkerhedsnøglen til at loge på.",
- "needToUseAnotherAccount": "Behøver du at bruge en anden konto?",
- "loginLegalDisclaimer": "Ved at klike på knapperne nedenfor, anerkender du at du har læst, forstår, og accepterer Vilkår for brug og for Privatlivserklæring.",
- "termsOfService": "Vilkår for brug",
- "privacyPolicy": "Privatlivspolitik",
- "userNotFoundWithUsername": "Ingen bruger med det brugernavnet fundet.",
- "verify": "Bekræft",
- "signIn": "Log ind",
- "forgotPassword": "Glemt adgangskode?",
- "orgSignInTip": "Hvis du har loget ind før, kan du skrive ind brugernavnet eller e-mailadressen ovenfor for at autentificere med organisationens identitetstjeneste i stedet. Det er nemmere!",
- "continueAnyway": "Fortsæt likevel",
- "dontShowAgain": "Ikke vis tilbage",
- "orgSignInNotice": "Visste du?",
- "signupOrgNotice": "Prøver du at logge ind?",
- "signupOrgTip": "Prøver du at logge ind via din organisations identitetsudbyder?",
- "signupOrgLink": "Log ind eller tilmeld dig med organisationen din i stedet",
- "verifyEmailLogInWithDifferentAccount": "Brug en anden konto",
- "logIn": "Log ind",
- "deviceInformation": "Enhedsoplysninger",
- "deviceInformationDescription": "Oplysninger om enheden og agenten",
- "deviceSecurity": "Enhedssikkerhed",
- "deviceSecurityDescription": "Sikkerhedstilstandsinformation om udstyr",
- "platform": "Platform",
- "macosVersion": "macOS version",
- "windowsVersion": "Windows version",
- "iosVersion": "iOS-version",
- "androidVersion": "Android version",
- "osVersion": "OS version",
- "kernelVersion": "Kjerne version",
- "deviceModel": "Enhedsmodel",
- "serialNumber": "Serienummer",
- "hostname": "Hostnavn",
- "firstSeen": "Først sett",
- "lastSeen": "Sist sett",
- "biometricsEnabled": "Biometri aktiveret",
- "diskEncrypted": "Disk krypteret",
- "firewallEnabled": "Brannmur aktiveret",
- "autoUpdatesEnabled": "Automatiske opdateringer aktiveret",
- "tpmAvailable": "TPM tilgængelig",
- "windowsAntivirusEnabled": "Antivirus aktiveret",
- "macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
- "macosGatekeeperEnabled": "Gatekeeper",
- "macosFirewallStealthMode": "Brannmur Usynlig Tilstand",
- "linuxAppArmorEnabled": "Rustning",
- "linuxSELinuxEnabled": "SELinux",
- "deviceSettingsDescription": "Vis enhedsoplysninger og indstillinger",
- "devicePendingApprovalDescription": "Denne enhed venter på godkendelse",
- "deviceBlockedDescription": "Denne enhed er blokeret. Den kan ikke oprette forbindelse til nogen ressourcer, medmindre blokeringen fjernes.",
- "unblockClient": "Fjern blokering af klient",
- "unblockClientDescription": "Enheden er blevet blokeret",
- "unarchiveClient": "Fjern arkivering klient",
- "unarchiveClientDescription": "Enheden er arkiveret",
- "block": "Bloker",
- "unblock": "Fjern blokering af",
- "deviceActions": "Enhedshandlinger",
- "deviceActionsDescription": "Administrer enhedsstatus og adgang",
- "devicePendingApprovalBannerDescription": "Denne enhed venter på godkendelse. Den kan ikke oprette forbindelse til ressourcer, før den er godkendt.",
- "connected": "Tilsluttet",
- "disconnected": "Offline",
- "approvalsEmptyStateTitle": "Enhedsgodkendelser er ikke aktiveret",
- "approvalsEmptyStateDescription": "Aktivere godkendelser af enheder for at roller skal godkendes af admin før brugere kan opret forbindelse til nye enheder.",
- "approvalsEmptyStateStep1Title": "Gå til roller",
- "approvalsEmptyStateStep1Description": "Naviger til organisationens roller indstillinger for at konfigurere enhedsgodkendelser.",
- "approvalsEmptyStateStep2Title": "Aktivér enhedsgodkendelser",
- "approvalsEmptyStateStep2Description": "Rediger en rolle og aktivér muligheden 'Kræve enhedsgodkendelser'. Brugere med denne rolle vil have brug for administratorgodkendelse for nye enheder.",
- "approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når dette er aktiveret, vises ventende enhedsanmodninger her til gennemgang",
- "approvalsEmptyStateButtonText": "Administrer Roller",
- "domainErrorTitle": "Vi har problemer med at verificere dit domæne",
- "idpAdminAutoProvisionPoliciesTabHint": "Konfigurer rollegartlegging og organisationspolitikker på Autoprovisioneringsindstillinger fanen.",
- "streamingTitle": "Hændelsesstreaming",
- "streamingDescription": "Stream hændelser fra din organisation til eksterne destinationer i sanntid.",
- "streamingUnnamedDestination": "Plassering uden navn",
- "streamingNoUrlConfigured": "Ingen URL konfigureret",
- "streamingAddDestination": "Tilføj mål",
- "streamingHttpWebhookTitle": "HTTP Webhook",
- "streamingHttpWebhookDescription": "Send hændelser til alle HTTP-endpoints med fleksibel autentificering og maling.",
- "streamingS3Title": "Amazon S3",
- "streamingS3Description": "Strøm hændelser til en S3-kompatibel objektlager. Kommer snart.",
- "streamingDatadogTitle": "Datadog",
- "streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
- "streamingTypePickerDescription": "Vælg en måltype for at komme i gang.",
- "streamingLastSyncError": "Det opstod en fejl under seneste synkronisering",
- "streamingUnexpectedError": "En uventet fejl opstod.",
- "streamingFailedToUpdate": "Kunne ikke opdatere destination",
- "streamingDeletedSuccess": "Målet blev slettet",
- "streamingFailedToDelete": "Kunne ikke slette destination",
- "streamingDeleteTitle": "Slet mål",
- "streamingDeleteButtonText": "Slet mål",
- "streamingDeleteDialogAreYouSure": "Er du sikker på at du vil slette",
- "streamingDeleteDialogThisDestination": "denne destinationen",
- "streamingDeleteDialogPermanentlyRemoved": "? Alle konfigurationer vil blive slettet permanent.",
- "httpDestEditTitle": "Rediger mål",
- "httpDestAddTitle": "Tilføj HTTP-destination",
- "httpDestEditDescription": "Opdater konfigurationen for denne HTTP-hændelsesstreamingdestination.",
- "httpDestAddDescription": "Konfigurer et nyt HTTP-endpoint til at modtage organisationens hændelser.",
- "S3DestEditTitle": "Rediger destination",
- "S3DestAddTitle": "Tilføj S3 destination",
- "S3DestEditDescription": "Opdater konfigurationen for denne S3-hændelsesstreamingdestination.",
- "S3DestAddDescription": "Konfigurer en ny Amazon S3-bucket (eller S3-kompatibel bucket) til at modtage din organisations hændelser.",
- "s3DestTabSettings": "Indstillinger",
- "s3DestTabFormat": "Format",
- "s3DestNameLabel": "Navn",
- "s3DestNamePlaceholder": "Min S3-destination",
- "s3DestAccessKeyIdLabel": "AWS adgangsnøgle-ID",
- "s3DestSecretAccessKeyLabel": "AWS hemmelig adgangsnøgle",
- "s3DestSecretAccessKeyPlaceholder": "Din AWS hemmelighed access key",
- "s3DestRegionLabel": "AWS-region",
- "s3DestBucketLabel": "Bucket-navn",
- "s3DestPrefixLabel": "Nøglepræfiks (valgfrit)",
- "s3DestPrefixDescription": "Valgfrit sti-prefiks tilføjet hver objektnøgle. Objekter er gemt på {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
- "s3DestEndpointLabel": "Brugerdefineret endpoint (valgfrit)",
- "s3DestEndpointDescription": "Overstyr S3-endepunktet for S3-kompatibel lagring som MinIO eller Cloudflare R2. Lad stå tomt for standard AWS S3.",
- "s3DestGzipLabel": "Gzip-komprimering",
- "s3DestGzipDescription": "Komprimer hvert uploadede objekt med gzip. Reducerer lageromkostninger og uploadstørrelse.",
- "s3DestFormatTitle": "Filformat",
- "s3DestFormatDescription": "Hvordan hændelser er serialisert i hvert uploadede objekt.",
- "s3DestFormatJsonArrayDescription": "Hvert objekt er et JSON-array af hændelsesposter. Kompatibel med de fleste analyseværktøjer.",
- "s3DestFormatNdjsonDescription": "Hvert objekt indeholder en JSON-post per linje (nylinje-delt JSON). Kompatibel med Athena, BigQuery, og Spark.",
- "s3DestFormatCsvTitle": "CSV",
- "s3DestFormatCsvDescription": "Hvert objekt er en RFC-4180 CSV-fil med en overskriftsrække. Kolonnenavne er afledt af hændelsesdatafelterne.",
- "s3DestSaveChanges": "Gem ændringer",
- "s3DestCreateDestination": "Opret destination",
- "s3DestUpdatedSuccess": "Destination opdateret med succes",
- "s3DestCreatedSuccess": "Destination oprettet med succes",
- "s3DestUpdateFailed": "Kunne ikke opdatere destination",
- "s3DestCreateFailed": "Kunne ikke oprette destination",
- "datadogDestEditTitle": "Rediger destination",
- "datadogDestAddTitle": "Tilføj Datadog destination",
- "datadogDestEditDescription": "Opdater konfigurationen for denne Datadog-hændelsesstreamingdestination.",
- "datadogDestAddDescription": "Konfigurer et nyt Datadog-endpoint til at modtage organisationens hændelser.",
- "httpDestTabSettings": "Indstillinger",
- "httpDestTabHeaders": "Overskrifter",
- "httpDestTabBody": "Indhold",
- "httpDestTabLogs": "Logfiler",
- "httpDestNamePlaceholder": "Min HTTP destination",
- "httpDestUrlLabel": "Destinasjons URL",
- "httpDestUrlErrorHttpRequired": "URL-adressen skal bruge httpp eller HTTPS",
- "httpDestUrlErrorHttpsRequired": "HTTPS er nødvendig for distribution af cloud",
- "httpDestUrlErrorInvalid": "Indtast en gyldig webadresse (f.eks. https://eksempel.com/webhook)",
- "httpDestAuthTitle": "Autentificering",
- "httpDestAuthDescription": "Vælg, hvordan anmodninger til dit endpoint autentificeres.",
- "httpDestAuthNoneTitle": "Ingen godkendelse",
- "httpDestAuthNoneDescription": "Sender forespørgsler uden autorisasjonsoverskrift.",
- "httpDestAuthBearerTitle": "Bærer Symbol",
- "httpDestAuthBearerDescription": "Legger til en Autorisation: Bearer '' header til hver forespørgsel.",
- "httpDestAuthBearerPlaceholder": "Din API-nøgle eller token",
- "httpDestAuthBasicTitle": "Standard Auth",
- "httpDestAuthBasicDescription": "Legger til en Autorisation: Basic '' header. Give legitimationsoplysninger som brugernavn:adgangskode.",
- "httpDestAuthBasicPlaceholder": "brugernavn:adgangskode",
- "httpDestAuthCustomTitle": "Brugerdefineret header",
- "httpDestAuthCustomDescription": "Angiv et brugerdefineret HTTP headers navn og værdi for autentificering (f.eks X-API-Key).",
- "httpDestAuthCustomHeaderNamePlaceholder": "Headernavn (f.eks. X-API-Key)",
- "httpDestAuthCustomHeaderValuePlaceholder": "Header værdi",
- "httpDestCustomHeadersTitle": "Brugerdefinerede HTTP-headers",
- "httpDestCustomHeadersDescription": "Tilføj brugerdefinerede headers til hver udgående forespørgsel. Nyttig for statisk tokens eller en brugerdefineret indholdstype. Som standard bliver indholdstype: application/json sendt.",
- "httpDestNoHeadersConfigured": "Ingen brugerdefinerede headers konfigureret. Klik på \"Tilføj header\" for at tilføje en.",
- "httpDestHeaderNamePlaceholder": "Headernavn",
- "httpDestHeaderValuePlaceholder": "Værdi",
- "httpDestAddHeader": "Tilføj header",
- "httpDestBodyTemplateTitle": "Brugerdefineret hovedmal",
- "httpDestBodyTemplateDescription": "Kontroller JSON payloadstrukturen sendt til dit endpoint. Hvis deaktiveret, sendes et standard JSON-objekt for hver hændelse.",
- "httpDestEnableBodyTemplate": "Aktivér brugerdefineret meldingsmal",
- "httpDestBodyTemplateLabel": "Body-skabelon (JSON)",
- "httpDestBodyTemplateHint": "Brug designmal variabler for at referere til eventfelt i din betaling.",
- "httpDestPayloadFormatTitle": "Mål format",
- "httpDestPayloadFormatDescription": "Hvordan hændelser serialiseres i hver request body.",
- "httpDestFormatJsonArrayTitle": "JSON-liste",
- "httpDestFormatJsonArrayDescription": "Én forespørgsel pr. batch; indholdet er en JSON-liste. Kompatibel med de fleste generiske webhooks og Datadog.",
- "httpDestFormatNdjsonTitle": "NDJSON",
- "httpDestFormatNdjsonDescription": "Én forespørgsel pr. batch; indholdet er newline-delimited JSON — ét objekt pr. linje, uden ydre array. Kræves af Splunk HEC, Elastic/OpenSearch og Grafana Loki.",
- "httpDestFormatSingleTitle": "En hændelse per forespørgsel",
- "httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hændelse. Brug kun for endpoints der ikke kan håndtere batcher.",
- "httpDestLogTypesTitle": "Logtyper",
- "httpDestLogTypesDescription": "Vælg, hvilke logtyper der videresendes til dette mål. Kun aktiverede logtyper bliver streamet.",
- "httpDestAccessLogsTitle": "Adgangslogs",
- "httpDestAccessLogsDescription": "Adgangsforsøk for ressourcer, inklusive godkendte og afvist forespørgsler.",
- "httpDestActionLogsTitle": "Handlingsloger",
- "httpDestActionLogsDescription": "Administrative tiltak som utføres af brugere inden for organisationen.",
- "httpDestConnectionLogsTitle": "Forbindelseslogs",
- "httpDestConnectionLogsDescription": "Udstyrs- og tunnelforbindelseshændelser, inklusive forbindelser og frakobling.",
- "httpDestRequestLogsTitle": "HTTP-forespørgselslogs",
- "httpDestRequestLogsDescription": "HTTP-forespørgsel logs for bekræftede ressourcer, inklusive metode, sti og responskode.",
- "httpDestSaveChanges": "Gem ændringer",
- "httpDestCreateDestination": "Opret mål",
- "httpDestUpdatedSuccess": "Målet er opdateret",
- "httpDestCreatedSuccess": "Målet er oprettet",
- "httpDestUpdateFailed": "Kunne ikke opdatere destination",
- "httpDestCreateFailed": "Kan ikke oprette mål",
- "followRedirects": "Følg videresendinger",
- "followRedirectsDescription": "Følg automatisk HTTP-videresendinger for forespørgsler.",
- "alertingErrorWebhookUrl": "Indtast venligst en gyldig URL for webhooken.",
- "healthCheckStrategyHttp": "Validerer forbindelse og kontrollerer HTTP-responsstatus.",
- "healthCheckStrategyTcp": "Bekræfter kun TCP-forbindelse, uden at inspisere responsen.",
- "healthCheckStrategySnmp": "Udfører en SNMP get-forespørgsel for at tjekke sundheden til netværksenheder og infrastruktur.",
- "healthCheckStrategyIcmp": "Bruger ICMP ekko forespørgsler (ping) for at tjekke om en ressource er tilgængelig og responsiv.",
- "healthCheckTabStrategy": "Strategi",
- "healthCheckTabConnection": "Forbindelse",
- "healthCheckTabAdvanced": "Avanceret",
- "healthCheckStrategyNotAvailable": "Denne strategien er ikke tilgængeligt. Venligst kontakt salgsafdelingen for at aktivere denne funktion.",
- "uptime30d": "Oppetid (30 d)",
- "idpAddActionCreateNew": "Opret ny identitetsudbyder",
- "idpAddActionImportFromOrg": "Importer fra en anden organisation",
- "idpImportDialogTitle": "Importer identitetsudbyder",
- "idpImportDialogDescription": "Vælg en identitetsudbyder fra en organisation der du er admin. Den vil blive knyttet til denne organisation.",
- "idpImportSearchPlaceholder": "Søg efter organisations- eller leverandørnavn...",
- "idpImportEmpty": "Ingen identitetsudbydere fundet.",
- "idpImportedDescription": "Identitetsudbyderen blev importert med succes.",
- "idpDeleteGlobalQuestion": "Er du sikker på, at du vil slette denne identitetsudbyder permanent?",
- "idpDeleteGlobalDescription": "Dette vil slette identitetsudbyder permanent fra alle organisationer, den er tilknyttet.",
- "idpUnassociateTitle": "Frakobl identitetsudbyder",
- "idpUnassociateQuestion": "Er du sikker på, at du vil frakoble denne identitetsudbyder fra denne organisation?",
- "idpUnassociateDescription": "Alle brugere knyttet til denne identitetsudbyder vil blive fjernet fra denne organisation, men identitetsudbyderen vil stadig eksistere for andre tilknyttede organisationer.",
- "idpUnassociateConfirm": "Bekræft frakobling af identitetsudbyder",
- "idpConfirmDeleteAndRemoveMeFromOrg": "SLET OG FJERN MIG FRA ORGANISATIONEN",
- "idpUnassociateAndRemoveMeFromOrg": "FRAKOBL OG FJERN MIG FRA ORGANISATIONEN",
- "idpUnassociateWarning": "Dette kan ikke fortrydes for denne organisation.",
- "idpUnassociatedDescription": "Identitetsudbyderen er frakoblet fra denne organisation",
- "idpUnassociateMenu": "Frakobl",
- "idpDeleteAllOrgsMenu": "Slet",
- "publicIpEndpoint": "Slutpunkt",
- "lastTriggeredAt": "Senest udløst",
- "reject": "Afvis",
- "uptimeDaysAgo": "{count} dage siden",
- "uptimeToday": "I dag",
- "uptimeNoDataAvailable": "Ingen data tilgængelig",
- "uptimeSuffix": "oppetid",
- "uptimeDowntimeSuffix": "nedetid",
- "uptimeTooltipUptimeLabel": "Oppetid",
- "uptimeTooltipDowntimeLabel": "Nedetid",
- "uptimeOngoing": "pågående",
- "uptimeNoMonitoringData": "Ingen overvåkingsdata",
- "uptimeNoData": "Ingen data",
- "uptimeMiniBarDown": "Nede",
- "uptimeSectionTitle": "Oppetid",
- "uptimeSectionDescription": "Tilgængelighed de seneste {days} dage",
- "uptimeAddAlert": "Tilføj varsling",
- "uptimeViewAlerts": "Vis varsler",
- "uptimeCreateEmailAlert": "Opret e-mailnotifikation",
- "uptimeAlertDescriptionSite": "Få besked via e-mail, når dette site går offline eller kommer tilbage online.",
- "uptimeAlertDescriptionResource": "Få besked via e-mail, når denne ressource går offline eller kommer tilbage online.",
- "uptimeAlertNamePlaceholder": "Varslingsnavn",
- "uptimeAdditionalEmails": "Flere e-mails",
- "uptimeCreateAlert": "Opret varsling",
- "uptimeAlertNoRecipients": "Ingen modtagere",
- "uptimeAlertNoRecipientsDescription": "Venligst tilføj mindst én bruger, rolle, eller e-mail for at varsle.",
- "uptimeAlertCreated": "Notifikation oprettet",
- "uptimeAlertCreatedDescription": "Du vil blive varslet når dette ændrer status.",
- "uptimeAlertCreateFailed": "Kunne ikke oprette notifikation",
- "webhookUrlLabel": "URL",
- "webhookHeaderKeyPlaceholder": "Nøgle",
- "webhookHeaderValuePlaceholder": "Værdi",
- "alertLabel": "Notifikation",
- "domainPickerWildcardSubdomainNotAllowed": "Wildcard-underdomæner er ikke tilladt.",
- "domainPickerWildcardCertWarning": "Wildcard-ressourcer kan kræve ekstra konfiguration for at fungere korrekt.",
- "domainPickerWildcardCertWarningLink": "Læs mere",
- "health": "Sundhed",
- "domainPendingErrorTitle": "Verificeringsproblem",
- "memberPortalTitle": "Ressourcer",
- "memberPortalDescription": "Ressourcer du har adgang til i denne organisation",
- "memberPortalSortBy": "Sorter efter...",
- "memberPortalSortNameAsc": "Navn A-AT",
- "memberPortalSortNameDesc": "Navn AT-A",
- "memberPortalSortDomainAsc": "Domæne A-AT",
- "memberPortalSortDomainDesc": "Domæne AT-A",
- "memberPortalSortEnabledFirst": "Aktiveret først",
- "memberPortalSortDisabledFirst": "Deaktiveret først",
- "memberPortalRefresh": "Opdater",
- "memberPortalRefreshResources": "Opdater ressourcer",
- "memberPortalFailedToLoad": "Kunne ikke indlæse ind ressourcer",
- "memberPortalFailedToLoadDescription": "Kunne ikke indlæse ressourcer. Tjek venligst din forbindelse, og prøv igen.",
- "memberPortalUnableToLoad": "Kan ikke indlæse ind ressourcer",
- "memberPortalTryAgain": "Prøv igen",
- "memberPortalNoResourcesFound": "Ingen ressourcer fundet",
- "memberPortalNoResourcesAvailable": "Ingen ressourcer tilgængelig",
- "memberPortalNoResourcesMatchSearch": "Ingen ressourcer matcher med \"{query}\". Prøv at justere søgeordene dine eller fjern søgningen for at se alle ressourcer.",
- "memberPortalNoResourcesAccess": "Du har endnu ikke adgang til nogen ressourcer. Kontakt din administrator for at få adgang til de ressourcer, du har brug for.",
- "memberPortalClearSearch": "Fjern søg",
- "memberPortalPublicResources": "Offentlige ressourcer",
- "memberPortalPublicResourcesDescription": "Webapplikationer og -tjenester tilgængelige via browser",
- "memberPortalCopiedToClipboard": "Kopieret til udklipsholderen",
- "memberPortalCopiedUrlDescription": "Ressource-URL er kopieret til din udklipsholder.",
- "memberPortalOpenResource": "Åbn ressource",
- "memberPortalPrivateResources": "Private ressourcer",
- "memberPortalPrivateResourcesDescription": "Interne netværksressourcer tilgængelige via klient",
- "memberPortalResourceDetails": "Ressourcedetaljer",
- "memberPortalMode": "Tilstand",
- "memberPortalDestination": "Destination",
- "memberPortalAlias": "Navn",
- "memberPortalCopiedAliasDescription": "Ressourcealias er kopieret til din udklipsholder.",
- "memberPortalCopiedDestinationDescription": "Ressourcedestination er kopieret til din udklipsholder.",
- "memberPortalRequiresClientConnection": "Kræver klientforbindelse",
- "memberPortalAuthMethods": "Autentificeringsmetoder",
- "memberPortalSso": "Enkeltpåloging (SSO)",
- "memberPortalPasswordProtected": "Adgangskodebeskyttet",
- "memberPortalPinCode": "PIN-kode",
- "memberPortalEmailWhitelist": "E-mailwhitelist",
- "memberPortalResourceDisabled": "Ressource deaktiveret",
- "memberPortalShowingResources": "Viser {start}-{end} af {total} ressourcer",
- "resourceLauncherTitle": "Ressource Starter",
- "resourceSidebarLauncherTitle": "Launcher",
- "resourceLauncherDescription": "Se alle tilgængelige ressourcer og start dem fra ét centralt knudepunkt",
- "resourceLauncherSearchPlaceholder": "Søg i dine ressourcer...",
- "resourceLauncherDefaultView": "Standard",
- "resourceLauncherSaveView": "Gem Visning",
- "resourceLauncherSaveToCurrentView": "Gem til nuværende visning",
- "resourceLauncherSaveDefaultPersonal": "Gem for mig",
- "resourceLauncherResetView": "Nulstil Visning",
- "resourceLauncherResetSystemDefault": "Nulstil til systemstandard",
- "resourceLauncherSystemDefaultRestored": "Systemstandard gendannet",
- "resourceLauncherSystemDefaultRestoredDescription": "Standardvisningen er blevet nulstillet til de oprindelige indstillinger.",
- "resourceLauncherSaveAsNewView": "Gem som Ny Visning",
- "resourceLauncherSaveAsNewViewDescription": "Giv denne visning et navn for at gemme dine nuværende filtre og layout.",
- "resourceLauncherSaveForEveryone": "Gem for Alle",
- "resourceLauncherSaveForEveryoneDescription": "Del denne visning med alle organisationsmedlemmer. Når den er ikke markeret, er visningen kun synlig for dig.",
- "resourceLauncherMakePersonal": "Gør Personlig",
- "resourceLauncherFilter": "Filter",
- "resourceLauncherFilterWithCount": "Filter, {count} anvendt",
- "resourceLauncherSort": "Sortér",
- "resourceLauncherSortAscending": "Sortér stigende",
- "resourceLauncherSortDescending": "Sortér faldende",
- "resourceLauncherSettings": "Indstillinger",
- "resourceLauncherGroupBy": "Gruppér Efter",
- "resourceLauncherGroupBySite": "Websted",
- "resourceLauncherGroupByLabel": "Etikett",
- "resourceLauncherGroupByNone": "Ingen",
- "resourceLauncherLayout": "Layout",
- "resourceLauncherLayoutGrid": "Gitter",
- "resourceLauncherLayoutList": "Liste",
- "resourceLauncherShowLabels": "Vis Etiketter",
- "resourceLauncherShowSiteTags": "Vis Site Tags",
- "resourceLauncherShowRecents": "Vis Seneste",
- "resourceLauncherDeleteView": "Slet Visning",
- "resourceLauncherDeleteViewTitle": "Slet visning",
- "resourceLauncherDeleteViewQuestion": "Er du sikker på, at du vil slette denne launchervisning?",
- "resourceLauncherDeleteViewConfirm": "Slet visning",
- "resourceLauncherViewAsAdmin": "Vis som Admin",
- "resourceLauncherResourceDetailsDescription": "Forbindelsesoplysninger og status for denne ressource.",
- "resourceLauncherResourceDetails": "Ressourcedetaljer",
- "resourceLauncherAuthMethodsDescription": "Autentificeringsmetoder aktiveret for denne ressource.",
- "resourceLauncherPrivateClientRequired": "Forbind med en klient på din enhed for at få privat adgang til denne ressource.",
- "resourceLauncherPrivateClientRequiredTitle": "Klientforbindelse påkrævet",
- "resourceLauncherDownloadClient": "Download klient",
- "resourceLauncherFailedToLoadDetails": "Kunne ikke indlæse ressourcedetaljer. Du har muligvis ikke længere adgang til denne ressource.",
- "resourceLauncherNoPortRestrictions": "Ingen portbegrænsninger",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Uden Etiket",
- "resourceLauncherNoSite": "Ingen Site",
- "resourceLauncherNoResourcesInGroup": "Ingen ressourcer i denne gruppe",
- "resourceLauncherEmptyStateTitle": "Ingen ressourcer tilgængelige",
- "resourceLauncherEmptyStateDescription": "Du har endnu ikke adgang til nogen ressourcer. Kontakt din administrator for at anmode om adgang.",
- "resourceLauncherEmptyStateNoResultsTitle": "Ingen ressourcer fundet",
- "resourceLauncherEmptyStateNoResultsDescription": "Ingen ressourcer matcher din nuværende søgning eller filtre. Prøv at justere dem for at finde det, du leder efter.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Ingen ressourcer matcher \"{query}\". Prøv at justere din søgning eller rydde filtre for at se alle ressourcer.",
- "resourceLauncherSearchFirstTitle": "Søg eller filtrer for at browse",
- "resourceLauncherSearchFirstDescription": "Du har adgang til mange ressourcer. Brug søgning eller filtrer efter site eller label for at finde det, du har brug for.",
- "resourceLauncherSiteGroupingDisabled": "Site-gruppering er ikke tilgængelig i denne skala. Filtrer efter site for at gruppere et mindre sæt.",
- "resourceLauncherLabelGroupingDisabled": "Label-gruppering er ikke tilgængelig i denne skala.",
- "resourceLauncherCompactModeHint": "Viser en forenklet liste for hurtigere browsing. Brug søgning eller filtre for at indsnævre resultater.",
- "resourceLauncherCompactGroupingHint": "Anvend site- eller label-filters for at aktivere gruppering.",
- "resourceLauncherCopiedToClipboard": "Kopieret til udklipsholderen",
- "resourceLauncherCopiedAccessDescription": "Ressourcetilgangen er kopieret til din udklipsholder.",
- "resourceLauncherViewNamePlaceholder": "Vis navn",
- "resourceLauncherViewNameLabel": "Vis Navn",
- "resourceLauncherViewSaved": "Vis Gemt",
- "resourceLauncherViewSavedDescription": "Din launcher-visning er blevet gemt.",
- "resourceLauncherViewSaveFailed": "Kunne ikke gemme visning",
- "resourceLauncherViewSaveFailedDescription": "Kunne ikke gemme launcher-visningen. Prøv venligst igen.",
- "resourceLauncherViewDeleted": "Visning slået væk",
- "resourceLauncherViewDeletedDescription": "Launcher-visningen er blevet slettet.",
- "resourceLauncherViewDeleteFailed": "Kunne ikke slette visning",
- "resourceLauncherViewDeleteFailedDescription": "Kan ikke slette launcher-visningen. Prøv venligst igen.",
- "memberPortalPrevious": "Forrige",
- "memberPortalNext": "Næste",
- "httpSettings": "HTTP Indstillinger",
- "tcpSettings": "TCP Indstillinger",
- "udpSettings": "UDP Indstillinger",
- "sshTitle": "SSH",
- "sshConnectingDescription": "Oprette en sikker forbindelse…",
- "sshConnecting": "Opretter forbindelse til…",
- "sshInitializing": "Initialiserer…",
- "sshSignInTitle": "Log ind på SSH",
- "sshSignInDescription": "Indtast dine SSH-legitimationsoplysninger for at oprette forbindelse til",
- "sshPasswordTab": "Adgangskode",
- "sshPrivateKeyTab": "Privat Nøgle",
- "sshPrivateKeyField": "Privat Nøgle",
- "sshPrivateKeyDisclaimer": "Din private nøgle gemmes ikke og er ikke synlig for Pangolin. Alternativt kan du bruge kortlivede certifikater til sømløs autentificering med din eksisterende Pangolin-identitet.",
- "sshLearnMore": "Læs mere",
- "sshPrivateKeyFile": "Privat nøglefil",
- "sshAuthenticate": "Opret forbindelse til",
- "sshTerminate": "Avslutt",
- "sshPoweredBy": "Drevet af",
- "sshErrorNoTarget": "Ingen mål spesifisert",
- "sshErrorWebSocket": "WebSocket-forbindelse mislykkedes",
- "sshErrorAuthFailed": "Autentificering mislykkedes",
- "sshErrorConnectionClosed": "Forbindelse afsluttet før autentificering blev fuldført",
- "sitePangolinSshDescription": "Tillad SSH-adgang til ressourcer på dette site. Dette kan ændres senere.",
- "browserGatewayNoResourceForDomain": "Ingen ressourcer fundet for dette domænet",
- "browserGatewayNoTarget": "Ingen mål",
- "browserGatewayConnect": "Opret forbindelse til",
- "browserGatewayCtrlAltDel": "Ctrl+Alt+Del",
- "sshErrorSignKeyFailed": "Kunne ikke signere SSH-nøgle for PAM-loginautentificering. Er du logget ind som bruger?",
- "sshTerminalError": "Fejl: {error}",
- "sshConnectionClosedCode": "Forbindelsen blev lukket (kode {code})",
- "sshPrivateKeyPlaceholder": "-----BEGYNN OPENSSH PRIVAT NØGLE-----",
- "sshPrivateKeyRequired": "Privat nøgle er påkrævet",
- "vncTitle": "VNC",
- "vncSignInDescription": "Indtast dine VNC-legitimationsoplysninger for at oprette forbindelse",
- "vncUsernameOptional": "Brugernavn (valgfrit)",
- "vncPasswordOptional": "Adgangskode (valgfrit)",
- "vncNoResourceTarget": "Intet ressourcemål tilgængeligt",
- "vncFailedToLoadNovnc": "Kunne ikke indlæse noVNC",
- "vncAuthFailedStatus": "Status {status}",
- "vncPasteClipboard": "Indsæt udklipsholder",
- "rdpTitle": "RDP",
- "rdpSignInTitle": "Log ind på Fjernskrivebord",
- "rdpSignInDescription": "Indtast Windows-legitimationsoplysninger for at oprette forbindelse til",
- "rdpLoadingModule": "Indlæser modul...",
- "rdpFailedToLoadModule": "Kunne ikke indlæse RDP-modul",
- "rdpNotReady": "Ikke klar",
- "rdpModuleInitializing": "RDP-modulen er stadig under initialisering",
- "rdpDownloadingFiles": "Downloader {count} fil(er) fra fjern…",
- "rdpDownloadFailed": "Download mislykkedes: {fileName}",
- "rdpUploaded": "Uploadet: {fileName}",
- "rdpNoConnectionTarget": "Intet forbindelsesmål tilgængeligt",
- "rdpConnectionFailed": "Forbindelsen mislykkedes",
- "rdpFit": "Tilpass",
- "rdpFull": "Fuldt",
- "rdpReal": "Ekte",
- "rdpMeta": "Meta",
- "rdpUploadFiles": "Upload filer",
- "rdpFilesReadyToPaste": "Filer klare til at limes ind",
- "rdpFilesReadyToPasteDescription": "{count} fil(er) kopieret til fjernudklipsholderen — trykk Ctrl+V på fjernskrivebordet for at indsætte.",
- "rdpUploadFailed": "Uploaden mislykkedes",
- "rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
- "sessionToolbarShow": "Vis værktøjslinje",
- "sessionToolbarHide": "Skjul værktøjslinje"
-}
diff --git a/messages/de-DE.json b/messages/de-DE.json
index dc7dbb808..87afc5d68 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -66,15 +66,9 @@
"local": "Lokal",
"edit": "Bearbeiten",
"siteConfirmDelete": "Löschen des Standorts bestätigen",
- "siteConfirmDeleteAndResources": "Löschen von Standort und Ressourcen bestätigen",
"siteDelete": "Standort löschen",
- "siteDeleteAndResources": "Standort und Ressourcen löschen",
"siteMessageRemove": "Sobald der Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle mit dem Standort verbundenen Ziele werden ebenfalls entfernt.",
- "siteMessageRemoveAndResources": "Dies wird dauerhaft alle öffentlichen und privaten Ressourcen, die mit diesem Standort verknüpft sind, löschen, selbst wenn eine Ressource auch mit anderen Standorten verbunden ist.",
"siteQuestionRemove": "Sind Sie sicher, dass Sie den Standort aus der Organisation entfernen möchten?",
- "siteQuestionRemoveAndResources": "Sind Sie sicher, dass Sie diesen Standort und alle zugehörigen Ressourcen löschen möchten?",
- "sitesTableDeleteSite": "Standort löschen",
- "sitesTableDeleteSiteAndResources": "Standort und Ressourcen löschen",
"siteManageSites": "Standorte verwalten",
"siteDescription": "Erstellen und Verwalten von Standorten, um die Verbindung zu privaten Netzwerken zu ermöglichen",
"sitesBannerTitle": "Verbinde ein beliebiges Netzwerk",
@@ -123,16 +117,6 @@
"siteUpdated": "Standort aktualisiert",
"siteUpdatedDescription": "Der Standort wurde aktualisiert.",
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
- "siteRestartTitle": "Standort neu starten",
- "siteRestartDescription": "Starten Sie den WireGuard-Tunnel für diesen Standort neu. Dies wird die Konnektivität kurzzeitig unterbrechen.",
- "siteRestartBody": "Verwenden Sie dies, wenn der Standort-Tunnel nicht ordnungsgemäß funktioniert und Sie eine erneute Verbindung erzwingen möchten, ohne den Host neu zu starten.",
- "siteRestartButton": "Standort neu starten",
- "siteRestartDialogMessage": "Sind Sie sicher, dass Sie den WireGuard-Tunnel für {name} neu starten möchten? Der Standort wird kurzzeitig die Konnektivität verlieren.",
- "siteRestartWarning": "Der Standort wird kurzzeitig getrennt, während der Tunnel neu gestartet wird.",
- "siteRestarted": "Standort neu gestartet",
- "siteRestartedDescription": "Der WireGuard-Tunnel wurde neu gestartet.",
- "siteErrorRestart": "Fehler beim Neustart des Standorts",
- "siteErrorRestartDescription": "Ein Fehler ist aufgetreten, während der Standort neu gestartet wurde.",
"siteSettingDescription": "Standorteinstellungen konfigurieren",
"siteResourcesTab": "Ressourcen",
"siteResourcesNoneOnSite": "Dieser Standort hat noch keine öffentlichen oder privaten Ressourcen",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy-Anfragen über rohe TCP/UDP mit Portnummer. Benötigt Sites, um sich mit einem entfernten Knoten zu verbinden.",
"resourceCreate": "Ressource erstellen",
"resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen",
- "resourcePublicCreate": "Öffentliche Ressource erstellen",
- "resourcePublicCreateDescription": "Befolgen Sie die unten aufgeführten Schritte, um eine neue öffentliche Ressource zu erstellen, auf die über einen Webbrowser zugegriffen werden kann",
"resourceCreateGeneralDescription": "Konfigurieren Sie die Grundeinstellungen der Ressource, einschließlich Name und Typ",
"resourceSeeAll": "Alle Ressourcen anzeigen",
"resourceCreateGeneral": "Allgemein",
@@ -615,8 +597,7 @@
"idpNameInternal": "Intern",
"emailInvalid": "Ungültige E-Mail-Adresse",
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
- "accessRoleSelectPlease": "Ein Benutzer muss mindestens einer Rolle zugeordnet sein.",
- "accessRoleRequired": "Rolle erforderlich",
+ "accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
"removeOwnAdminRoleConfirmTitle": "Möchten Sie Ihren Administratorzugriff entfernen?",
"removeOwnAdminRoleConfirmDescription": "Nach dem Speichern haben Sie keine Administratorrechte mehr in dieser Organisation. Ein anderer Administrator kann den Zugriff bei Bedarf wiederherstellen.",
"removeOwnAdminRoleConfirmButton": "Meinen Administratorzugriff entfernen",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Grundlegende Header-Authentifizierung",
"policyAuthHeaderAuthDescription": "Überprüfen Sie einen benutzerdefinierten HTTP-Headernamen und -wert bei jeder Anfrage",
"policyAuthHeaderAuthSummary": "Header konfiguriert",
- "policyAuthHeaderName": "Benutzername",
- "policyAuthHeaderValue": "Passwort",
+ "policyAuthHeaderName": "Header-Name",
+ "policyAuthHeaderValue": "Erwarteter Wert",
"policyAuthSetPasscode": "Passcode setzen",
"policyAuthSetPincode": "PIN-Code festlegen",
"policyAuthSetEmailWhitelist": "E-Mail-Whitelist festlegen",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Blueprint anwenden",
"actionListBlueprints": "Blaupausen anzeigen",
"actionGetBlueprint": "Erhalte Blaupause",
- "actionCreateOrgWideLauncherView": "Organisationen-Weiter-Startansicht erstellen",
"setupToken": "Setup-Token",
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
"setupTokenRequired": "Setup-Token ist erforderlich",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Ressourcen-PIN festlegen",
"actionSetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist festlegen",
"actionGetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist abrufen",
- "actionGetResourcePolicy": "Ressourcenrichtlinie abrufen",
- "actionUpdateResourcePolicy": "Ressourcenrichtlinie aktualisieren",
- "actionSetResourcePolicyUsers": "Ressourcenrichtlinienbenutzer festlegen",
- "actionSetResourcePolicyRoles": "Rollen der Ressourcenrichtlinie festlegen",
- "actionSetResourcePolicyPassword": "Passwort der Ressourcenrichtlinie festlegen",
- "actionSetResourcePolicyPincode": "Ressourcenrichtlinie-PIN festlegen",
- "actionSetResourcePolicyHeaderAuth": "Ressourcenrichtlinie für Header-Authentifizierung festlegen",
- "actionSetResourcePolicyWhitelist": "E-Mail-Whitelist der Ressourcenrichtlinie festlegen",
- "actionSetResourcePolicyRules": "Ressourcenrichtlinienregeln festlegen",
"actionCreateTarget": "Ziel erstellen",
"actionDeleteTarget": "Ziel löschen",
"actionGetTarget": "Ziel abrufen",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Zugriffstoken generieren",
"actionDeleteAccessToken": "Zugriffstoken löschen",
"actionListAccessTokens": "Zugriffstoken auflisten",
- "actionCreateResourceSessionToken": "Ressourcensitzungstoken erstellen",
"actionCreateResourceRule": "Ressourcenregel erstellen",
"actionDeleteResourceRule": "Ressourcenregel löschen",
"actionListResourceRules": "Ressourcenregeln auflisten",
@@ -1520,30 +1490,6 @@
"navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
"navbarDocsLink": "Dokumentation",
- "commandPaletteTitle": "Befehlsfeld",
- "commandPaletteDescription": "Nach Seiten, Organisationen, Ressourcen und Aktionen suchen",
- "commandPaletteSearchPlaceholder": "Seiten, Ressourcen, Aktionen suchen...",
- "commandPaletteNoResults": "Keine Ergebnisse gefunden.",
- "commandPaletteSearching": "Suche...",
- "commandPaletteNavigation": "Navigation",
- "commandPaletteOrganizations": "Organisationen",
- "commandPaletteSites": "Seiten",
- "commandPaletteResources": "Ressourcen",
- "commandPaletteUsers": "Benutzer",
- "commandPaletteClients": "Maschinen-Clients",
- "commandPaletteActions": "Aktionen",
- "commandPaletteCreateSite": "Seite erstellen",
- "commandPaletteCreateProxyResource": "Öffentliche Ressource erstellen",
- "commandPaletteCreatePrivateResource": "Private Ressource erstellen",
- "commandPaletteCreateUser": "Benutzer erstellen",
- "commandPaletteCreateApiKey": "API-Schlüssel erstellen",
- "commandPaletteCreateMachineClient": "Maschinen-Client erstellen",
- "commandPaletteCreateAlertRule": "Alarmregel erstellen",
- "commandPaletteCreateIdentityProvider": "Identitätsanbieter erstellen",
- "commandPaletteToggleTheme": "Thema umschalten",
- "commandPaletteChooseOrganization": "Organisation auswählen",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Strg K",
"otpErrorEnable": "2FA konnte nicht aktiviert werden",
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
"sidebarLogsAnalytics": "Analytik",
- "commandSites": "Seiten",
- "commandActionModeInfo": "Geben Sie \">\" ein, um den Aktionsmodus zu öffnen",
- "commandResources": "Ressourcen",
- "commandProxyResources": "Öffentliche Ressourcen",
- "commandClientResources": "Private Ressourcen",
- "commandClients": "Clients",
- "commandUserDevices": "Benutzergeräte",
- "commandMachineClients": "Maschinen-Clients",
- "commandDomains": "Domänen",
- "commandRemoteExitNodes": "Fernknoten",
- "commandTeam": "Team",
- "commandUsers": "Benutzer",
- "commandRoles": "Rollen",
- "commandInvitations": "Einladungen",
- "commandPolicies": "Geteilte Richtlinien",
- "commandResourcePolicies": "Öffentliche Ressourcenrichtlinien",
- "commandIdentityProviders": "Identitätsanbieter",
- "commandApprovals": "Genehmigungsanfragen",
- "commandShareableLinks": "Teilbare Links",
- "commandOrganization": "Organisation",
- "commandLogsAndAnalytics": "Logs & Analysen",
- "commandLogsAnalytics": "Analysen",
- "commandLogsRequest": "HTTP-Anforderungsprotokolle",
- "commandLogsAccess": "Zugriffsprotokolle",
- "commandLogsAction": "Administrator-Aktionsprotokolle",
- "commandLogsConnection": "Netzwerkprotokolle",
- "commandLogsStreaming": "Ereignis-Streaming",
- "commandManagement": "Verwaltung",
- "commandAlerting": "Alarmierung",
- "commandProvisioning": "Bereitstellung",
- "commandBluePrints": "Blaupausen",
- "commandApiKeys": "API-Schlüssel",
- "commandBillingAndLicenses": "Abrechnung & Lizenzen",
- "commandBilling": "Abrechnung",
- "commandEnterpriseLicenses": "Lizenzen",
- "commandSettings": "Einstellungen",
- "commandLauncher": "Launcher",
- "commandResourceLauncher": "Ressourcen-Launcher",
- "commandSearchResults": "Suchergebnisse",
"alertingTitle": "Benachrichtigung",
"alertingDescription": "Quellen, Auslöser und Aktionen für Benachrichtigungen festlegen",
"alertingRules": "Benachrichtigungsregeln",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domänen",
"billingOrganizations": "Orden",
"billingRemoteExitNodes": "Entfernte Knoten",
- "billingPublicResources": "Öffentliche Ressourcen",
- "billingPrivateResources": "Private Ressourcen",
- "billingMachineClients": "Maschinen-Clients",
"billingNoLimitConfigured": "Kein Limit konfiguriert",
"billingEstimatedPeriod": "Geschätzter Abrechnungszeitraum",
"billingIncludedUsage": "Inklusive Nutzung",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Wie viele Benutzer Sie verwenden können",
"billingDomainInfo": "Wie viele Domains Sie verwenden können",
"billingRemoteExitNodesInfo": "Wie viele entfernte Knoten Sie verwenden können",
- "billingPublicResourcesInfo": "Wie viele öffentliche Ressourcen Sie nutzen können",
- "billingPrivateResourcesInfo": "Wie viele private Ressourcen Sie nutzen können",
- "billingMachineClientsInfo": "Wie viele Maschinen-Clients Sie nutzen können",
"billingLicenseKeys": "Lizenzschlüssel",
"billingLicenseKeysDescription": "Verwalten Sie Ihre Lizenzschlüssel Abonnements",
"billingLicenseSubscription": "Lizenzabonnement",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Subnetz",
"addressDescription": "Die interne Adresse des Clients. Muss in das Subnetz der Organisation fallen.",
"selectSites": "Standorte auswählen",
- "selectLabels": "Etiketten auswählen",
"sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.",
"clientInstallOlm": "Olm installieren",
"clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Schließen",
"createInternalResourceDialogCreateClientResource": "Private Ressource erstellen",
"createInternalResourceDialogCreateClientResourceDescription": "Erstelle eine neue Ressource, die nur für Clients zugänglich ist, die mit der Organisation verbunden sind",
- "privateResourceGeneralDescription": "Konfigurieren Sie den Namen, die Kennung und andere allgemeine Ressourceneinstellungen.",
- "privateResourceCreatePageSeeAll": "Alle privaten Ressourcen anzeigen",
- "privateResourceAllowIcmpPing": "ICMP-Ping zulassen",
- "privateResourceNetworkAccess": "Netzwerkzugriff",
- "privateResourceNetworkAccessDescription": "TCP/UDP-Portzugriff kontrollieren und ICMP-Ping für diese Ressource zulassen.",
- "hostSettings": "Host-Einstellungen",
- "cidrSettings": "CIDR-Einstellungen",
"createInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Standort",
"selectSite": "Standort auswählen...",
"multiSitesSelectorSitesCount": "{count, plural, one {# Standort} other {# Standorte}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# Etikett} other {# Etiketten}}",
"noSitesFound": "Keine Standorte gefunden.",
"createInternalResourceDialogProtocol": "Protokoll",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Der CIDR-Bereich der Ressource im Netzwerk der Website.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Ein optionaler interner DNS-Alias für diese Ressource.",
- "internalResourceAliasLocalWarning": "Aliasse, die auf .local enden, können aufgrund von mDNS in einigen Netzwerken zu Auflösungsproblemen führen.",
"internalResourceDownstreamSchemeRequired": "Schema ist für HTTP-Ressourcen erforderlich",
"internalResourceHttpPortRequired": "Zielport ist für HTTP-Ressourcen erforderlich",
"siteConfiguration": "Konfiguration",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Entfernte Knoten",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Geheimnis",
- "remoteExitNodeNetworkingTitle": "Netzwerkeinstellungen",
- "remoteExitNodeNetworkingDescription": "Konfigurieren Sie, wie dieser Remote Exit Node den Datenverkehr leitet und welche Standorte bevorzugt über ihn verbinden. Erweiterte Funktionen zur Verwendung mit Backhaul-Netzwerkkonfigurationen.",
- "remoteExitNodeNetworkingSave": "Einstellungen speichern",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Netzwerkeinstellungen gespeichert",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Netzwerkeinstellungen wurden erfolgreich aktualisiert.",
- "remoteExitNodeNetworkingSaveError": "Fehler beim Speichern der Netzwerkeinstellungen",
- "remoteExitNodeNetworkingSubnetsTitle": "Remote-Subnetze",
- "remoteExitNodeNetworkingSubnetsDescription": "Definieren Sie die CIDR-Bereiche, an die dieser Remote Exit Node den Datenverkehr weiterleitet. Geben Sie einen gültigen CIDR (z. B. 10.0.0.0/8) ein und drücken Sie die Eingabetaste, um hinzuzufügen.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Fügen Sie einen CIDR-Bereich hinzu (z.B. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Fehler beim Laden der Subnetze",
- "remoteExitNodeNetworkingLabelsTitle": "Präferenzetiketten",
- "remoteExitNodeNetworkingLabelsDescription": "Standorte mit diesen Etiketten werden gezwungen, über diesen Remote Exit Node zu verbinden.",
- "remoteExitNodeNetworkingLabelsButtonText": "Etiketten auswählen...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Etiketten suchen...",
- "remoteExitNodeNetworkingLabelsLoadError": "Fehler beim Laden der Etiketten",
"remoteExitNodeCreate": {
"title": "Erstelle Remote Node",
"description": "Erstelle einen neues selbst gehostetes Relay und ihre Proxyserver Nodes",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Für diese Organisation sind keine Knoten verfügbar. Erstellen Sie zuerst einen Knoten, um lokale Standorte zu verwenden.",
"exitNode": "Exit-Node",
"country": "Land",
- "countryIsNot": "Land ist nicht",
"rulesMatchCountry": "Derzeit basierend auf der Quell-IP",
"region": "Region",
"selectRegion": "Region wählen...",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC Provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnetz",
- "utilitySubnet": "Nutzsubnetz",
"subnetDescription": "Das Subnetz für die Netzwerkkonfiguration dieser Organisation.",
"customDomain": "Eigene Domain",
"authPage": "Authentifizierungs-Seiten",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Die Zwei-Faktor-Authentifizierung ist erforderlich. Bitte melden Sie sich erneut über {dashboardUrl}/auth/login an. Dann kommen Sie hierher zurück.",
"additionalSecurityRequired": "Zusätzliche Sicherheit erforderlich",
"organizationRequiresAdditionalSteps": "Diese Organisation erfordert zusätzliche Sicherheitsschritte, bevor Sie auf Ressourcen zugreifen können.",
- "sessionExpired": "Sitzung abgelaufen",
- "sessionExpiredReauthRequired": "Ihre Sitzung ist gemäß der Sicherheitsrichtlinie Ihrer Organisation abgelaufen. Bitte authentifizieren Sie sich erneut, um fortzufahren.",
- "reauthenticate": "Neu anmelden",
"completeTheseSteps": "Schließe diese Schritte ab",
"enableTwoFactorAuthentication": "Zwei-Faktor-Authentifizierung aktivieren",
"completeSecuritySteps": "Schließe Sicherheitsschritte ab",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt",
"loadingDNSRecords": "Lade DNS-Einträge...",
"olmUpdateAvailableInfo": "Eine aktualisierte Version von Olm ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für die beste Erfahrung.",
- "updateAvailableInfo": "Eine aktualisierte Version ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
"client": "Client",
"proxyProtocol": "Proxy-Protokoll-Einstellungen",
"proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP-Dienste zu erhalten.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Nutzer hinzufügen",
"editInternalResourceDialogAddClients": "Clients hinzufügen",
"editInternalResourceDialogDestinationLabel": "Ziel",
- "editInternalResourceDialogDestinationDescription": "Konfigurieren Sie, wie Clients diese Ressource erreichen.",
+ "editInternalResourceDialogDestinationDescription": "Geben Sie die Zieladresse für die interne Ressource an. Dies kann ein Hostname, eine IP-Adresse oder ein CIDR-Bereich sein, abhängig vom gewählten Modus. Legen Sie optional einen internen DNS-Alias für eine vereinfachte Identifizierung fest.",
"internalResourceFormMultiSiteRoutingHelp": "Durch die Auswahl mehrerer Seiten wird ein ausfallsicheres Routing und Failover für hohe Verfügbarkeit ermöglicht.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Mehr erfahren",
"editInternalResourceDialogPortRestrictionsDescription": "Den Zugriff auf bestimmte TCP/UDP-Ports beschränken oder alle Ports erlauben/blockieren.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "E-Mail-Whitelist",
"memberPortalResourceDisabled": "Ressource deaktiviert",
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
- "resourceLauncherTitle": "Ressourcenstarter",
- "resourceSidebarLauncherTitle": "Launcher",
- "resourceLauncherDescription": "Alle verfügbaren Ressourcen anzeigen und von einem zentralen Hub aus starten",
- "resourceLauncherSearchPlaceholder": "Suche deine Ressourcen...",
- "resourceLauncherDefaultView": "Standard",
- "resourceLauncherSaveView": "Ansicht speichern",
- "resourceLauncherSaveToCurrentView": "In aktueller Ansicht speichern",
- "resourceLauncherSaveDefaultPersonal": "Für mich speichern",
- "resourceLauncherResetView": "Ansicht zurücksetzen",
- "resourceLauncherResetSystemDefault": "Auf Systemeinstellung zurücksetzen",
- "resourceLauncherSystemDefaultRestored": "Systemstandard wurde wiederhergestellt",
- "resourceLauncherSystemDefaultRestoredDescription": "Die Standardansicht wurde auf die ursprünglichen Einstellungen zurückgesetzt.",
- "resourceLauncherSaveAsNewView": "Als neue Ansicht speichern",
- "resourceLauncherSaveAsNewViewDescription": "Geben Sie dieser Ansicht einen Namen, um Ihre aktuellen Filter und das Layout zu speichern.",
- "resourceLauncherSaveForEveryone": "Für alle speichern",
- "resourceLauncherSaveForEveryoneDescription": "Teilen Sie diese Ansicht mit allen Organisationsmitgliedern. Wenn nicht aktiviert, ist die Ansicht nur für Sie sichtbar.",
- "resourceLauncherMakePersonal": "Persönlich machen",
- "resourceLauncherFilter": "Filter",
- "resourceLauncherFilterWithCount": "Filter, {count} angewendet",
- "resourceLauncherSort": "Sortieren",
- "resourceLauncherSortAscending": "Aufsteigend sortieren",
- "resourceLauncherSortDescending": "Absteigend sortieren",
- "resourceLauncherSettings": "Einstellungen",
- "resourceLauncherGroupBy": "Gruppieren nach",
- "resourceLauncherGroupBySite": "Standort",
- "resourceLauncherGroupByLabel": "Etikett",
- "resourceLauncherGroupByNone": "Keine",
- "resourceLauncherLayout": "Layout",
- "resourceLauncherLayoutGrid": "Raster",
- "resourceLauncherLayoutList": "Liste",
- "resourceLauncherShowLabels": "Etiketten anzeigen",
- "resourceLauncherShowSiteTags": "Standort-Tags anzeigen",
- "resourceLauncherShowRecents": "Kürzlich anzeigen",
- "resourceLauncherDeleteView": "Ansicht löschen",
- "resourceLauncherDeleteViewTitle": "Ansicht löschen",
- "resourceLauncherDeleteViewQuestion": "Sind Sie sicher, dass Sie diese Launcher-Ansicht löschen möchten?",
- "resourceLauncherDeleteViewConfirm": "Ansicht löschen",
- "resourceLauncherViewAsAdmin": "Ansicht als Administrator anzeigen",
- "resourceLauncherResourceDetailsDescription": "Verbindungsinformationen und Status für diese Ressource.",
- "resourceLauncherResourceDetails": "Ressourcendetails",
- "resourceLauncherAuthMethodsDescription": "Aktivierte Authentifizierungsmethoden für diese Ressource.",
- "resourceLauncherPrivateClientRequired": "Verbinde dich mit einem Client auf deinem Gerät, um privat auf diese Ressource zuzugreifen.",
- "resourceLauncherPrivateClientRequiredTitle": "Client-Verbindung erforderlich",
- "resourceLauncherDownloadClient": "Client herunterladen",
- "resourceLauncherFailedToLoadDetails": "Ressourcendetails konnten nicht geladen werden. Möglicherweise haben Sie keinen Zugriff mehr auf diese Ressource.",
- "resourceLauncherNoPortRestrictions": "Keine Portbeschränkungen",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Nicht etikettiert",
- "resourceLauncherNoSite": "Kein Standort",
- "resourceLauncherNoResourcesInGroup": "Keine Ressourcen in dieser Gruppe",
- "resourceLauncherEmptyStateTitle": "Keine Ressourcen verfügbar",
- "resourceLauncherEmptyStateDescription": "Sie haben noch keinen Zugriff auf Ressourcen. Kontaktieren Sie Ihren Administrator, um Zugriff anzufordern.",
- "resourceLauncherEmptyStateNoResultsTitle": "Keine Ressourcen gefunden",
- "resourceLauncherEmptyStateNoResultsDescription": "Keine Ressourcen entsprechen Ihrer aktuellen Suche oder den Filtern. Versuchen Sie, diese anzupassen, um zu finden, wonach Sie suchen.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Keine Ressourcen entsprechen \"{query}\". Versuchen Sie, Ihre Suche anzupassen oder die Filter zu löschen, um alle Ressourcen anzuzeigen.",
- "resourceLauncherSearchFirstTitle": "Suchen oder Filtern zum Durchsuchen",
- "resourceLauncherSearchFirstDescription": "Sie haben Zugriff auf viele Ressourcen. Verwenden Sie die Suche oder filtern Sie nach Websites oder Labels, um das zu finden, was Sie brauchen.",
- "resourceLauncherSiteGroupingDisabled": "Website-Gruppierung ist in diesem Maßstab nicht verfügbar. Filtern Sie nach Standort, um eine kleinere Gruppe zu bilden.",
- "resourceLauncherLabelGroupingDisabled": "Label-Gruppierung ist in diesem Maßstab nicht verfügbar.",
- "resourceLauncherCompactModeHint": "Vereinfachte Liste für schnelleres Durchsuchen anzeigen. Verwenden Sie Suche oder Filter, um die Ergebnisse einzugrenzen.",
- "resourceLauncherCompactGroupingHint": "Treffen Sie Standort- oder Labelfilter an, um Gruppierungen zu aktivieren.",
- "resourceLauncherCopiedToClipboard": "In die Zwischenablage kopiert",
- "resourceLauncherCopiedAccessDescription": "Der Ressourcenzugriff wurde in Ihre Zwischenablage kopiert.",
- "resourceLauncherViewNamePlaceholder": "Ansichtsname",
- "resourceLauncherViewNameLabel": "Ansichtsname",
- "resourceLauncherViewSaved": "Ansicht gespeichert",
- "resourceLauncherViewSavedDescription": "Ihre Startansicht wurde gespeichert.",
- "resourceLauncherViewSaveFailed": "Fehler beim Speichern der Ansicht",
- "resourceLauncherViewSaveFailedDescription": "Die Startansicht konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.",
- "resourceLauncherViewDeleted": "Ansicht gelöscht",
- "resourceLauncherViewDeletedDescription": "Die Startansicht wurde gelöscht.",
- "resourceLauncherViewDeleteFailed": "Fehler beim Löschen der Ansicht",
- "resourceLauncherViewDeleteFailedDescription": "Die Startansicht konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
"memberPortalPrevious": "Vorherige",
"memberPortalNext": "Nächste",
"httpSettings": "HTTP-Einstellungen",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Privater Schlüssel ist erforderlich",
"vncTitle": "VNC",
- "vncSignInDescription": "Geben Sie Ihre VNC-Zugangsdaten ein, um sich zu verbinden",
- "vncUsernameOptional": "Benutzername (optional)",
+ "vncSignInDescription": "Geben Sie Ihr VNC-Passwort ein, um sich zu verbinden",
"vncPasswordOptional": "Passwort (optional)",
"vncNoResourceTarget": "Kein Ressourcen-Ziel verfügbar",
"vncFailedToLoadNovnc": "Fehler beim Laden von noVNC",
diff --git a/messages/en-US.json b/messages/en-US.json
index 2a2c83b18..5937595b5 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -66,15 +66,9 @@
"local": "Local",
"edit": "Edit",
"siteConfirmDelete": "Confirm Delete Site",
- "siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
"siteDelete": "Delete Site",
- "siteDeleteAndResources": "Delete Site and Resources",
- "siteMessageRemove": "Once removed the site will no longer be accessible. Targets associated with this site will be removed, but resources will remain.",
- "siteMessageRemoveAndResources": "This will permanently delete all public and private resources linked to this site, even if a resource is also associated with other sites.",
+ "siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
- "siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
- "sitesTableDeleteSite": "Delete Site",
- "sitesTableDeleteSiteAndResources": "Delete Site and Resources",
"siteManageSites": "Manage Sites",
"siteDescription": "Create and manage sites to enable connectivity to private networks",
"sitesBannerTitle": "Connect Any Network",
@@ -123,16 +117,6 @@
"siteUpdated": "Site updated",
"siteUpdatedDescription": "The site has been updated.",
"siteGeneralDescription": "Configure the general settings for this site",
- "siteRestartTitle": "Restart Site",
- "siteRestartDescription": "Restart the WireGuard tunnel for this site. This will briefly interrupt connectivity.",
- "siteRestartBody": "Use this if the site tunnel is not functioning correctly and you want to force a reconnect without restarting the host.",
- "siteRestartButton": "Restart Site",
- "siteRestartDialogMessage": "Are you sure you want to restart the WireGuard tunnel for {name}? The site will briefly lose connectivity.",
- "siteRestartWarning": "The site will briefly disconnect while the tunnel restarts.",
- "siteRestarted": "Site restarted",
- "siteRestartedDescription": "The WireGuard tunnel has been restarted.",
- "siteErrorRestart": "Failed to restart site",
- "siteErrorRestartDescription": "An error occurred while restarting the site.",
"siteSettingDescription": "Configure the settings on the site",
"siteResourcesTab": "Resources",
"siteResourcesNoneOnSite": "This site has no public or private resources yet.",
@@ -220,7 +204,7 @@
"proxyResourceTitle": "Manage Public Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"publicResourcesBannerTitle": "Web-based Public Access",
- "publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet through a web browser and include identity and context-aware access policies. Unlike private resources, they do not require client-side software.",
+ "publicResourcesBannerDescription": "Public resources are HTTPS proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
"clientResourceTitle": "Manage Private Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.",
"resourceCreate": "Create Resource",
"resourceCreateDescription": "Follow the steps below to create a new resource",
- "resourcePublicCreate": "Create Public Resource",
- "resourcePublicCreateDescription": "Follow the steps below to create a new public resource that is accessible through a web browser",
"resourceCreateGeneralDescription": "Configure the basic resource settings including the name and the type",
"resourceSeeAll": "See All Resources",
"resourceCreateGeneral": "General",
@@ -615,8 +597,7 @@
"idpNameInternal": "Internal",
"emailInvalid": "Invalid email address",
"inviteValidityDuration": "Please select a duration",
- "accessRoleSelectPlease": "A user must belong to at least one role.",
- "accessRoleRequired": "Role required",
+ "accessRoleSelectPlease": "Please select a role",
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Basic Header Auth",
"policyAuthHeaderAuthDescription": "Validate a custom HTTP header name and value on each request",
"policyAuthHeaderAuthSummary": "Header configured",
- "policyAuthHeaderName": "Username",
- "policyAuthHeaderValue": "Password",
+ "policyAuthHeaderName": "Header name",
+ "policyAuthHeaderValue": "Expected value",
"policyAuthSetPasscode": "Set Passcode",
"policyAuthSetPincode": "Set PIN Code",
"policyAuthSetEmailWhitelist": "Set Email Whitelist",
@@ -1397,7 +1378,6 @@
"createOrgUser": "Create Org User",
"actionUpdateOrg": "Update Organization",
"actionRemoveInvitation": "Remove Invitation",
- "actionRemoveUserRole": "Remove User Role",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
@@ -1415,7 +1395,6 @@
"actionApplyBlueprint": "Apply Blueprint",
"actionListBlueprints": "List Blueprints",
"actionGetBlueprint": "Get Blueprint",
- "actionCreateOrgWideLauncherView": "Create Org-Wide Launcher View",
"setupToken": "Setup Token",
"setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required",
@@ -1435,15 +1414,6 @@
"actionSetResourcePincode": "Set Resource Pincode",
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
- "actionGetResourcePolicy": "Get Resource Policy",
- "actionUpdateResourcePolicy": "Update Resource Policy",
- "actionSetResourcePolicyUsers": "Set Resource Policy Users",
- "actionSetResourcePolicyRoles": "Set Resource Policy Roles",
- "actionSetResourcePolicyPassword": "Set Resource Policy Password",
- "actionSetResourcePolicyPincode": "Set Resource Policy Pincode",
- "actionSetResourcePolicyHeaderAuth": "Set Resource Policy Header Authentication",
- "actionSetResourcePolicyWhitelist": "Set Resource Policy Email Whitelist",
- "actionSetResourcePolicyRules": "Set Resource Policy Rules",
"actionCreateTarget": "Create Target",
"actionDeleteTarget": "Delete Target",
"actionGetTarget": "Get Target",
@@ -1463,7 +1433,6 @@
"actionGenerateAccessToken": "Generate Access Token",
"actionDeleteAccessToken": "Delete Access Token",
"actionListAccessTokens": "List Access Tokens",
- "actionCreateResourceSessionToken": "Create Resource Session Token",
"actionCreateResourceRule": "Create Resource Rule",
"actionDeleteResourceRule": "Delete Resource Rule",
"actionListResourceRules": "List Resource Rules",
@@ -1521,30 +1490,6 @@
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
"navbarDocsLink": "Documentation",
- "commandPaletteTitle": "Command Palette",
- "commandPaletteDescription": "Search for pages, organizations, resources, and actions",
- "commandPaletteSearchPlaceholder": "Search pages, resources, actions...",
- "commandPaletteNoResults": "No results found.",
- "commandPaletteSearching": "Searching...",
- "commandPaletteNavigation": "Navigation",
- "commandPaletteOrganizations": "Organizations",
- "commandPaletteSites": "Sites",
- "commandPaletteResources": "Resources",
- "commandPaletteUsers": "Users",
- "commandPaletteClients": "Machine Clients",
- "commandPaletteActions": "Actions",
- "commandPaletteCreateSite": "Create Site",
- "commandPaletteCreateProxyResource": "Create Public Resource",
- "commandPaletteCreatePrivateResource": "Create Private Resource",
- "commandPaletteCreateUser": "Create User",
- "commandPaletteCreateApiKey": "Create API Key",
- "commandPaletteCreateMachineClient": "Create Machine Client",
- "commandPaletteCreateAlertRule": "Create Alert Rule",
- "commandPaletteCreateIdentityProvider": "Create Identity Provider",
- "commandPaletteToggleTheme": "Toggle Theme",
- "commandPaletteChooseOrganization": "Choose Organization",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Unable to enable 2FA",
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
"otpSetupCheckCode": "Please enter a 6-digit code",
@@ -1622,45 +1567,6 @@
"sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
- "commandSites": "Sites",
- "commandActionModeInfo": "Type \">\" To Open Action Mode",
- "commandResources": "Resources",
- "commandProxyResources": "Public Resources",
- "commandClientResources": "Private Resources",
- "commandClients": "Clients",
- "commandUserDevices": "User Devices",
- "commandMachineClients": "Machine Clients",
- "commandDomains": "Domains",
- "commandRemoteExitNodes": "Remote Nodes",
- "commandTeam": "Team",
- "commandUsers": "Users",
- "commandRoles": "Roles",
- "commandInvitations": "Invitations",
- "commandPolicies": "Shared Policies",
- "commandResourcePolicies": "Public Resources Policies",
- "commandIdentityProviders": "Identity Providers",
- "commandApprovals": "Approval Requests",
- "commandShareableLinks": "Shareable Links",
- "commandOrganization": "Organization",
- "commandLogsAndAnalytics": "Logs & Analytics",
- "commandLogsAnalytics": "Analytics",
- "commandLogsRequest": "HTTP Request Logs",
- "commandLogsAccess": "Authentication Logs",
- "commandLogsAction": "Admin Action Logs",
- "commandLogsConnection": "Network Logs",
- "commandLogsStreaming": "Event Streaming",
- "commandManagement": "Management",
- "commandAlerting": "Alerting",
- "commandProvisioning": "Provisioning",
- "commandBluePrints": "Blueprints",
- "commandApiKeys": "API Keys",
- "commandBillingAndLicenses": "Billing & Licenses",
- "commandBilling": "Billing",
- "commandEnterpriseLicenses": "Licenses",
- "commandSettings": "Settings",
- "commandLauncher": "Launcher",
- "commandResourceLauncher": "Resource Launcher",
- "commandSearchResults": "Search Results",
"alertingTitle": "Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications",
"alertingRules": "Alert rules",
@@ -1732,7 +1638,7 @@
"alertingActionType": "Action type",
"alertingNotifyUsers": "Users",
"alertingNotifyRoles": "Roles",
- "alertingNotifyEmails": "Email Addresses",
+ "alertingNotifyEmails": "Email addresses",
"alertingEmailPlaceholder": "Add email and press Enter",
"alertingWebhookMethod": "HTTP method",
"alertingWebhookSecret": "Signing secret (optional)",
@@ -1992,9 +1898,6 @@
"billingDomains": "Domains",
"billingOrganizations": "Orgs",
"billingRemoteExitNodes": "Remote Nodes",
- "billingPublicResources": "Public Resources",
- "billingPrivateResources": "Private Resources",
- "billingMachineClients": "Machine Clients",
"billingNoLimitConfigured": "No limit configured",
"billingEstimatedPeriod": "Estimated Billing Period",
"billingIncludedUsage": "Included Usage",
@@ -2023,9 +1926,6 @@
"billingUsersInfo": "How many users you can use",
"billingDomainInfo": "How many domains you can use",
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
- "billingPublicResourcesInfo": "How many public resources you can use",
- "billingPrivateResourcesInfo": "How many private resources you can use",
- "billingMachineClientsInfo": "How many machine clients you can use",
"billingLicenseKeys": "License Keys",
"billingLicenseKeysDescription": "Manage your license key subscriptions",
"billingLicenseSubscription": "License Subscription",
@@ -2171,7 +2071,6 @@
"subnetPlaceholder": "Subnet",
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
"selectSites": "Select sites",
- "selectLabels": "Select labels",
"sitesDescription": "The client will have connectivity to the selected sites",
"clientInstallOlm": "Install Machine Client",
"clientInstallOlmDescription": "Install the machine client for your system",
@@ -2272,10 +2171,10 @@
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
"sshSudo": "Allow sudo",
"sshSudoCommands": "Sudo Commands",
- "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
+ "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, separated by commas, spaces, or new lines. Absolute paths must be used.",
"sshCreateHomeDir": "Create Home Directory",
"sshUnixGroups": "Unix Groups",
- "sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
+ "sshUnixGroupsDescription": "Unix groups to add the user to on the target host, separated by commas, spaces, or new lines.",
"roleTextFieldPlaceholder": "Enter values, or drop a .txt or .csv file",
"roleTextImportTitle": "Import from File",
"roleTextImportDescription": "Importing {fileName} into {fieldLabel}.",
@@ -2394,19 +2293,11 @@
"createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Private Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
- "privateResourceGeneralDescription": "Configure the name, identifier, and other general resource settings.",
- "privateResourceCreatePageSeeAll": "See All Private Resources",
- "privateResourceAllowIcmpPing": "Allow ICMP Ping",
- "privateResourceNetworkAccess": "Network Access",
- "privateResourceNetworkAccessDescription": "Control TCP/UDP port access and whether ICMP ping is allowed for this resource.",
- "hostSettings": "Host Settings",
- "cidrSettings": "CIDR Settings",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
"selectSite": "Select site...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# label} other {# labels}}",
"noSitesFound": "No sites found.",
"createInternalResourceDialogProtocol": "Protocol",
"createInternalResourceDialogTcp": "TCP",
@@ -2447,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
- "internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
"internalResourceDownstreamSchemeRequired": "Scheme is required for HTTP resources",
"internalResourceHttpPortRequired": "Destination port is required for HTTP resources",
"siteConfiguration": "Configuration",
@@ -2481,21 +2371,6 @@
"sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secret",
- "remoteExitNodeNetworkingTitle": "Network Settings",
- "remoteExitNodeNetworkingDescription": "Configure how this remote exit node routes traffic and which sites prefer to connect through it. Advanced features to be used with backhaul networking configurations.",
- "remoteExitNodeNetworkingSave": "Save Settings",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Network settings saved",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Network settings have been updated successfully.",
- "remoteExitNodeNetworkingSaveError": "Failed to save network settings",
- "remoteExitNodeNetworkingSubnetsTitle": "Remote Subnets",
- "remoteExitNodeNetworkingSubnetsDescription": "Define the CIDR ranges that this remote exit node will route traffic to. Type a valid CIDR (e.g. 10.0.0.0/8) and press Enter to add.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
- "remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
- "remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
- "remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
- "remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
"remoteExitNodeCreate": {
"title": "Create Remote Node",
"description": "Create a new self-hosted remote relay and proxy server node",
@@ -2549,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "No nodes are available for this organization. Create a node first to use local sites.",
"exitNode": "Exit Node",
"country": "Country",
- "countryIsNot": "Country Is Not",
"rulesMatchCountry": "Currently based on source IP",
"region": "Region",
"selectRegion": "Select region",
@@ -2675,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnet",
- "utilitySubnet": "Utility Subnet",
"subnetDescription": "The subnet for this organization's network configuration.",
"customDomain": "Custom Domain",
"authPage": "Authentication Pages",
@@ -2759,9 +2632,6 @@
"twoFactorSetupRequired": "Two-factor authentication setup is required. Please log in again via {dashboardUrl}/auth/login complete this step. Then, come back here.",
"additionalSecurityRequired": "Additional Security Required",
"organizationRequiresAdditionalSteps": "This organization requires additional security steps before you can access resources.",
- "sessionExpired": "Session Expired",
- "sessionExpiredReauthRequired": "Your session has expired per your organization's security policy. Please re-authenticate to continue.",
- "reauthenticate": "Re-authenticate",
"completeTheseSteps": "Complete these steps",
"enableTwoFactorAuthentication": "Enable two-factor authentication",
"completeSecuritySteps": "Complete Security Steps",
@@ -3097,7 +2967,6 @@
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
"loadingDNSRecords": "Loading DNS records...",
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
- "updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
"client": "Client",
"proxyProtocol": "Proxy Protocol Settings",
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
@@ -3271,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Add Users",
"editInternalResourceDialogAddClients": "Add Clients",
"editInternalResourceDialogDestinationLabel": "Destination",
- "editInternalResourceDialogDestinationDescription": "Configure how clients reach this resource.",
+ "editInternalResourceDialogDestinationDescription": "Choose where this resource runs and how clients reach it. Selecting multiple sites will create a high availability resource that can be accessed from any of the selected sites.",
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
@@ -3664,80 +3533,6 @@
"memberPortalEmailWhitelist": "Email Whitelist",
"memberPortalResourceDisabled": "Resource Disabled",
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
- "resourceLauncherTitle": "Resource Launcher",
- "resourceSidebarLauncherTitle": "Launcher",
- "resourceLauncherDescription": "View all available resources and launch them from one central hub",
- "resourceLauncherSearchPlaceholder": "Search your resources...",
- "resourceLauncherDefaultView": "Default",
- "resourceLauncherSaveView": "Save View",
- "resourceLauncherSaveToCurrentView": "Save to Current View",
- "resourceLauncherSaveDefaultPersonal": "Save for Me",
- "resourceLauncherResetView": "Reset View",
- "resourceLauncherResetSystemDefault": "Reset to System Default",
- "resourceLauncherSystemDefaultRestored": "System default restored",
- "resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
- "resourceLauncherSaveAsNewView": "Save as New View",
- "resourceLauncherSaveAsNewViewDescription": "Give this view a name to save your current filters and layout.",
- "resourceLauncherSaveForEveryone": "Save for Everyone",
- "resourceLauncherSaveForEveryoneDescription": "Share this view with all organization members. When unchecked, the view is only visible to you.",
- "resourceLauncherMakePersonal": "Make Personal",
- "resourceLauncherFilter": "Filter",
- "resourceLauncherFilterWithCount": "Filter, {count} applied",
- "resourceLauncherSort": "Sort",
- "resourceLauncherSortAscending": "Sort ascending",
- "resourceLauncherSortDescending": "Sort descending",
- "resourceLauncherSettings": "Settings",
- "resourceLauncherGroupBy": "Group By",
- "resourceLauncherGroupBySite": "Site",
- "resourceLauncherGroupByLabel": "Label",
- "resourceLauncherGroupByNone": "None",
- "resourceLauncherLayout": "Layout",
- "resourceLauncherLayoutGrid": "Grid",
- "resourceLauncherLayoutList": "List",
- "resourceLauncherShowLabels": "Show Labels",
- "resourceLauncherShowSiteTags": "Show Site Tags",
- "resourceLauncherShowRecents": "Show Recents",
- "resourceLauncherDeleteView": "Delete View",
- "resourceLauncherDeleteViewTitle": "Delete View",
- "resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
- "resourceLauncherDeleteViewConfirm": "Delete View",
- "resourceLauncherViewAsAdmin": "View as Admin",
- "resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
- "resourceLauncherResourceDetails": "Resource Details",
- "resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
- "resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
- "resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
- "resourceLauncherDownloadClient": "Download client",
- "resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
- "resourceLauncherNoPortRestrictions": "No port restrictions",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Unlabeled",
- "resourceLauncherNoSite": "No Site",
- "resourceLauncherNoResourcesInGroup": "No resources in this group",
- "resourceLauncherEmptyStateTitle": "No Resources Available",
- "resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.",
- "resourceLauncherEmptyStateNoResultsTitle": "No Resources Found",
- "resourceLauncherEmptyStateNoResultsDescription": "No resources match your current search or filters. Try adjusting them to find what you are looking for.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "No resources match \"{query}\". Try adjusting your search or clearing filters to see all resources.",
- "resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
- "resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
- "resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
- "resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
- "resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
- "resourceLauncherCompactGroupingHint": "Apply site or label filters to enable grouping.",
- "resourceLauncherCopiedToClipboard": "Copied to clipboard",
- "resourceLauncherCopiedAccessDescription": "Resource access has been copied to your clipboard.",
- "resourceLauncherViewNamePlaceholder": "View name",
- "resourceLauncherViewNameLabel": "View Name",
- "resourceLauncherViewSaved": "View saved",
- "resourceLauncherViewSavedDescription": "Your launcher view has been saved.",
- "resourceLauncherViewSaveFailed": "Failed to save view",
- "resourceLauncherViewSaveFailedDescription": "Could not save the launcher view. Please try again.",
- "resourceLauncherViewDeleted": "View deleted",
- "resourceLauncherViewDeletedDescription": "The launcher view has been deleted.",
- "resourceLauncherViewDeleteFailed": "Failed to delete view",
- "resourceLauncherViewDeleteFailedDescription": "Could not delete the launcher view. Please try again.",
"memberPortalPrevious": "Previous",
"memberPortalNext": "Next",
"httpSettings": "HTTP Settings",
@@ -3773,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Private key is required",
"vncTitle": "VNC",
- "vncSignInDescription": "Enter your VNC credentials to connect",
- "vncUsernameOptional": "Username (optional)",
+ "vncSignInDescription": "Enter your VNC password to connect",
"vncPasswordOptional": "Password (optional)",
"vncNoResourceTarget": "No resource target is available",
"vncFailedToLoadNovnc": "Failed to load noVNC",
diff --git a/messages/es-ES.json b/messages/es-ES.json
index 9bc4c83a1..19a6ab6e1 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -66,15 +66,9 @@
"local": "Local",
"edit": "Editar",
"siteConfirmDelete": "Confirmar Borrar Sitio",
- "siteConfirmDeleteAndResources": "Confirmar eliminación del sitio y recursos",
"siteDelete": "Eliminar sitio",
- "siteDeleteAndResources": "Eliminar sitio y recursos",
"siteMessageRemove": "Una vez eliminado, el sitio ya no será accesible. Todos los objetivos asociados con el sitio también serán eliminados.",
- "siteMessageRemoveAndResources": "Esto eliminará permanentemente todos los recursos públicos y privados vinculados a este sitio, incluso si un recurso también está asociado con otros sitios.",
"siteQuestionRemove": "¿Está seguro que desea eliminar el sitio de la organización?",
- "siteQuestionRemoveAndResources": "¿Está seguro de que desea eliminar este sitio y todos los recursos asociados?",
- "sitesTableDeleteSite": "Eliminar sitio",
- "sitesTableDeleteSiteAndResources": "Eliminar sitio y recursos",
"siteManageSites": "Administrar Sitios",
"siteDescription": "Crear y administrar sitios para permitir la conectividad a redes privadas",
"sitesBannerTitle": "Conectar cualquier red",
@@ -123,16 +117,6 @@
"siteUpdated": "Sitio actualizado",
"siteUpdatedDescription": "El sitio ha sido actualizado.",
"siteGeneralDescription": "Configurar la configuración general de este sitio",
- "siteRestartTitle": "Reiniciar Sitio",
- "siteRestartDescription": "Reinicia el túnel WireGuard para este sitio. Esto interrumpirá brevemente la conectividad.",
- "siteRestartBody": "Utiliza esto si el túnel del sitio no está funcionando correctamente y quieres forzar una reconexión sin reiniciar el host.",
- "siteRestartButton": "Reiniciar Sitio",
- "siteRestartDialogMessage": "¿Estás seguro de que deseas reiniciar el túnel WireGuard para {name}? El sitio perderá conectividad brevemente.",
- "siteRestartWarning": "El sitio se desconectará brevemente mientras se reinicia el túnel.",
- "siteRestarted": "Sitio reiniciado",
- "siteRestartedDescription": "El túnel WireGuard ha sido reiniciado.",
- "siteErrorRestart": "Error al reiniciar el sitio",
- "siteErrorRestartDescription": "Se ha producido un error al reiniciar el sitio.",
"siteSettingDescription": "Configurar los ajustes en el sitio",
"siteResourcesTab": "Recursos",
"siteResourcesNoneOnSite": "Este sitio aún no tiene recursos públicos o privados.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Las peticiones de proxy sobre TCP/UDP crudas usando un número de puerto. Requiere que los sitios se conecten a un nodo remoto.",
"resourceCreate": "Crear Recurso",
"resourceCreateDescription": "Siga los siguientes pasos para crear un nuevo recurso",
- "resourcePublicCreate": "Crear recurso público",
- "resourcePublicCreateDescription": "Siga los pasos a continuación para crear un nuevo recurso público accesible a través de un navegador web",
"resourceCreateGeneralDescription": "Configurar la configuración básica del recurso, incluido el nombre y el tipo",
"resourceSeeAll": "Ver todos los recursos",
"resourceCreateGeneral": "General",
@@ -615,8 +597,7 @@
"idpNameInternal": "Interno",
"emailInvalid": "Dirección de correo inválida",
"inviteValidityDuration": "Por favor, seleccione una duración",
- "accessRoleSelectPlease": "Un usuario debe pertenecer al menos a un rol.",
- "accessRoleRequired": "Rol requerido",
+ "accessRoleSelectPlease": "Por favor, seleccione un rol",
"removeOwnAdminRoleConfirmTitle": "¿Eliminar su acceso de administrador?",
"removeOwnAdminRoleConfirmDescription": "Ya no tendrá permisos de administrador en esta organización después de guardar. Otro administrador puede restaurar el acceso si es necesario.",
"removeOwnAdminRoleConfirmButton": "Eliminar Mi Acceso de Administrador",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Autenticación Básica del Encabezado",
"policyAuthHeaderAuthDescription": "Valida un nombre y valor de encabezado HTTP personalizado en cada petición",
"policyAuthHeaderAuthSummary": "Encabezado configurado",
- "policyAuthHeaderName": "Usuario",
- "policyAuthHeaderValue": "Contraseña",
+ "policyAuthHeaderName": "Nombre del encabezado",
+ "policyAuthHeaderValue": "Valor esperado",
"policyAuthSetPasscode": "Establecer Código de Acceso",
"policyAuthSetPincode": "Establecer Código PIN",
"policyAuthSetEmailWhitelist": "Establecer Lista Blanca de Correo",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Aplicar plano",
"actionListBlueprints": "Listar blueprints",
"actionGetBlueprint": "Obtener blueprint",
- "actionCreateOrgWideLauncherView": "Crear Vista de Lanzador para toda la Organización",
"setupToken": "Configuración de token",
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
"setupTokenRequired": "Se requiere el token de configuración",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Establecer Pincode del recurso",
"actionSetResourceEmailWhitelist": "Establecer lista blanca de correo de recursos",
"actionGetResourceEmailWhitelist": "Obtener correo electrónico de recursos",
- "actionGetResourcePolicy": "Obtener política de recursos",
- "actionUpdateResourcePolicy": "Actualizar política de recursos",
- "actionSetResourcePolicyUsers": "Definir política de recursos para usuarios",
- "actionSetResourcePolicyRoles": "Definir roles de política de recursos",
- "actionSetResourcePolicyPassword": "Definir contraseña de política de recursos",
- "actionSetResourcePolicyPincode": "Definir Pincode de política de recursos",
- "actionSetResourcePolicyHeaderAuth": "Definir autenticación de encabezado de política de recursos",
- "actionSetResourcePolicyWhitelist": "Definir lista blanca de correos de política de recursos",
- "actionSetResourcePolicyRules": "Definir reglas de política de recursos",
"actionCreateTarget": "Crear destino",
"actionDeleteTarget": "Eliminar destino",
"actionGetTarget": "Obtener objetivo",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Generar token de acceso",
"actionDeleteAccessToken": "Eliminar token de acceso",
"actionListAccessTokens": "Lista de Tokens de Acceso",
- "actionCreateResourceSessionToken": "Crear Token de Sesión de Recurso",
"actionCreateResourceRule": "Crear Regla de Recursos",
"actionDeleteResourceRule": "Eliminar Regla de Recurso",
"actionListResourceRules": "Lista de Reglas de Recursos",
@@ -1520,30 +1490,6 @@
"navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación",
"navbarDocsLink": "Documentación",
- "commandPaletteTitle": "Paleta de Comandos",
- "commandPaletteDescription": "Buscar páginas, organizaciones, recursos y acciones",
- "commandPaletteSearchPlaceholder": "Buscar páginas, recursos, acciones...",
- "commandPaletteNoResults": "No se han encontrado resultados.",
- "commandPaletteSearching": "Buscando...",
- "commandPaletteNavigation": "Navegación",
- "commandPaletteOrganizations": "Organizaciones",
- "commandPaletteSites": "Sitios",
- "commandPaletteResources": "Recursos",
- "commandPaletteUsers": "Usuarios",
- "commandPaletteClients": "Clientes de Máquina",
- "commandPaletteActions": "Acciones",
- "commandPaletteCreateSite": "Crear Sitio",
- "commandPaletteCreateProxyResource": "Crear recurso público",
- "commandPaletteCreatePrivateResource": "Crear recurso privado",
- "commandPaletteCreateUser": "Crear Usuario",
- "commandPaletteCreateApiKey": "Crear Clave API",
- "commandPaletteCreateMachineClient": "Crear Cliente de Máquina",
- "commandPaletteCreateAlertRule": "Crear Regla de Alerta",
- "commandPaletteCreateIdentityProvider": "Crear proveedor de identidad",
- "commandPaletteToggleTheme": "Cambiar tema",
- "commandPaletteChooseOrganization": "Elegir organización",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "No se puede habilitar 2FA",
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Gestión",
"sidebarBillingAndLicenses": "Facturación y licencias",
"sidebarLogsAnalytics": "Analíticas",
- "commandSites": "Sitios",
- "commandActionModeInfo": "Escriba \">\" Para abrir el modo de acción",
- "commandResources": "Recursos",
- "commandProxyResources": "Recursos Públicos",
- "commandClientResources": "Recursos Privados",
- "commandClients": "Clientes",
- "commandUserDevices": "Dispositivos de Usuario",
- "commandMachineClients": "Clientes de Máquina",
- "commandDomains": "Dominios",
- "commandRemoteExitNodes": "Nodos Remotos",
- "commandTeam": "Equipo",
- "commandUsers": "Usuarios",
- "commandRoles": "Roles",
- "commandInvitations": "Invitaciones",
- "commandPolicies": "Políticas Compartidas",
- "commandResourcePolicies": "Políticas de Recursos Públicos",
- "commandIdentityProviders": "Proveedores de identidad",
- "commandApprovals": "Solicitudes de Aprobación",
- "commandShareableLinks": "Enlaces compartibles",
- "commandOrganization": "Organización",
- "commandLogsAndAnalytics": "Registros y Análisis",
- "commandLogsAnalytics": "Análisis",
- "commandLogsRequest": "Registros de Solicitud HTTP",
- "commandLogsAccess": "Registros de acceso",
- "commandLogsAction": "Registros de acción de administrador",
- "commandLogsConnection": "Registros de conexión",
- "commandLogsStreaming": "Transmisión de Eventos",
- "commandManagement": "Gestión",
- "commandAlerting": "Alertas",
- "commandProvisioning": "Aprovisionamiento",
- "commandBluePrints": "Planos",
- "commandApiKeys": "Claves API",
- "commandBillingAndLicenses": "Facturación y Licencias",
- "commandBilling": "Facturación",
- "commandEnterpriseLicenses": "Licencias",
- "commandSettings": "Ajustes",
- "commandLauncher": "Lanzador",
- "commandResourceLauncher": "Lanzador de Recursos",
- "commandSearchResults": "Resultados de búsqueda",
"alertingTitle": "Alertas",
"alertingDescription": "Definir fuentes, disparadores y acciones para notificaciones",
"alertingRules": "Reglas de alerta",
@@ -1991,9 +1898,6 @@
"billingDomains": "Dominios",
"billingOrganizations": "Orgánico",
"billingRemoteExitNodes": "Nodos remotos",
- "billingPublicResources": "Recursos Públicos",
- "billingPrivateResources": "Recursos Privados",
- "billingMachineClients": "Clientes de Máquina",
"billingNoLimitConfigured": "No se ha configurado ningún límite",
"billingEstimatedPeriod": "Período de facturación estimado",
"billingIncludedUsage": "Uso incluido",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Cuántos usuarios puedes usar",
"billingDomainInfo": "Cuántos dominios puedes usar",
"billingRemoteExitNodesInfo": "Cuántos nodos remotos puedes usar",
- "billingPublicResourcesInfo": "Cuántos recursos públicos puedes usar",
- "billingPrivateResourcesInfo": "Cuántos recursos privados puedes usar",
- "billingMachineClientsInfo": "Cuántos clientes de máquina puedes usar",
"billingLicenseKeys": "Claves de licencia",
"billingLicenseKeysDescription": "Administrar las suscripciones de su clave de licencia",
"billingLicenseSubscription": "Suscripción de licencia",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Subred",
"addressDescription": "La dirección interna del cliente. Debe estar dentro de la subred de la organización.",
"selectSites": "Seleccionar sitios",
- "selectLabels": "Seleccionar etiquetas",
"sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados",
"clientInstallOlm": "Instalar Olm",
"clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Cerrar",
"createInternalResourceDialogCreateClientResource": "Crear recurso privado",
"createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que sólo será accesible a los clientes conectados a la organización",
- "privateResourceGeneralDescription": "Configura el nombre, identificador y otros ajustes generales del recurso.",
- "privateResourceCreatePageSeeAll": "Ver todos los recursos privados",
- "privateResourceAllowIcmpPing": "Permitir ping ICMP",
- "privateResourceNetworkAccess": "Acceso de red",
- "privateResourceNetworkAccessDescription": "Controlar el acceso a puertos TCP/UDP y si se permite ping ICMP para este recurso.",
- "hostSettings": "Configuraciones del anfitrión",
- "cidrSettings": "Configuraciones CIDR",
"createInternalResourceDialogResourceProperties": "Propiedades del recurso",
"createInternalResourceDialogName": "Nombre",
"createInternalResourceDialogSite": "Sitio",
"selectSite": "Seleccionar sitio...",
"multiSitesSelectorSitesCount": "{count, plural, one {# sitio} other {# sitios}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# etiqueta} other {# etiquetas}}",
"noSitesFound": "Sitios no encontrados.",
"createInternalResourceDialogProtocol": "Protocolo",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "El rango CIDR del recurso en la red del sitio.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Un alias DNS interno opcional para este recurso.",
- "internalResourceAliasLocalWarning": "Los alias que terminan en .local pueden causar problemas de resolución debido a mDNS en algunas redes.",
"internalResourceDownstreamSchemeRequired": "Se requiere el método para recursos HTTP",
"internalResourceHttpPortRequired": "Se requiere el puerto de destino para recursos HTTP",
"siteConfiguration": "Configuración",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Nodos remotos",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secreto",
- "remoteExitNodeNetworkingTitle": "Ajustes de Red",
- "remoteExitNodeNetworkingDescription": "Configura cómo este nodo de salida remoto dirige el tráfico y qué sitios prefieren conectarse a través de él. Características avanzadas para usar con configuraciones de red de retroceso.",
- "remoteExitNodeNetworkingSave": "Guardar Ajustes",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Ajustes de red guardados",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Los ajustes de red han sido actualizados exitosamente.",
- "remoteExitNodeNetworkingSaveError": "Error al guardar los ajustes de red",
- "remoteExitNodeNetworkingSubnetsTitle": "Subredes Remotas",
- "remoteExitNodeNetworkingSubnetsDescription": "Define los rangos CIDR a los que este nodo de salida remoto dirigirá el tráfico. Escribe un CIDR válido (e.g. 10.0.0.0/8) y presiona Enter para añadir.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Añadir un rango CIDR (e.g. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Error al cargar las subredes",
- "remoteExitNodeNetworkingLabelsTitle": "Etiquetas de Preferencias",
- "remoteExitNodeNetworkingLabelsDescription": "Los sitios con estas etiquetas se verán obligados a conectarse a través de este nodo de salida remoto.",
- "remoteExitNodeNetworkingLabelsButtonText": "Seleccionar etiquetas...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Buscar etiquetas...",
- "remoteExitNodeNetworkingLabelsLoadError": "Error al cargar las etiquetas",
"remoteExitNodeCreate": {
"title": "Crear nodo remoto",
"description": "Crea un nuevo nodo de retransmisión y proxy server autogestionado",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "No hay nodos disponibles para esta organización. Crea un nodo primero para usar sitios locales.",
"exitNode": "Nodo de Salida",
"country": "País",
- "countryIsNot": "El país no es",
"rulesMatchCountry": "Actualmente basado en IP de origen",
"region": "Región",
"selectRegion": "Seleccionar región",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Proveedor OAuth2/OIDC de Google",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subred",
- "utilitySubnet": "Subred de Utilidad",
"subnetDescription": "La subred para la configuración de red de esta organización.",
"customDomain": "Dominio personalizado",
"authPage": "Páginas de autenticación",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "La configuración de autenticación de doble factor es requerida. Por favor, inicia sesión de nuevo a través de {dashboardUrl}/auth/login completa este paso. Luego, vuelve aquí.",
"additionalSecurityRequired": "Seguridad adicional requerida",
"organizationRequiresAdditionalSteps": "Esta organización requiere pasos de seguridad adicionales antes de poder acceder a los recursos.",
- "sessionExpired": "Sesión Expirada",
- "sessionExpiredReauthRequired": "Su sesión ha expirado según la política de seguridad de su organización. Por favor, vuelva a autenticarse para continuar.",
- "reauthenticate": "Volver a autenticar",
"completeTheseSteps": "Completa estos pasos",
"enableTwoFactorAuthentication": "Habilitar autenticación de doble factor",
"completeSecuritySteps": "Pasos de seguridad completos",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Falta el ID de organización o dominio",
"loadingDNSRecords": "Cargando registros DNS...",
"olmUpdateAvailableInfo": "Una versión actualizada de Olm está disponible. Por favor, actualice a la última versión para obtener la mejor experiencia.",
- "updateAvailableInfo": "Hay una versión actualizada disponible. Actualice a la última versión para obtener la mejor experiencia.",
"client": "Cliente",
"proxyProtocol": "Configuración del Protocolo Proxy",
"proxyProtocolDescription": "Configurar el protocolo de proxy para preservar las direcciones IP del cliente para los servicios TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Agregar usuarios",
"editInternalResourceDialogAddClients": "Agregar clientes",
"editInternalResourceDialogDestinationLabel": "Destino",
- "editInternalResourceDialogDestinationDescription": "Configura cómo los clientes acceden a este recurso.",
+ "editInternalResourceDialogDestinationDescription": "Especifique la dirección de destino para el recurso interno. Puede ser un nombre de host, dirección IP o rango CIDR dependiendo del modo seleccionado. Opcionalmente establezca un alias DNS interno para una identificación más fácil.",
"internalResourceFormMultiSiteRoutingHelp": "Seleccionar múltiples sitios habilita el enrutamiento resistente y la conmutación por error para alta disponibilidad.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Más información",
"editInternalResourceDialogPortRestrictionsDescription": "Restringir el acceso a puertos TCP/UDP específicos o permitir/bloquear todos los puertos.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Lista Blanca de Correo",
"memberPortalResourceDisabled": "Recurso Deshabilitado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
- "resourceLauncherTitle": "Lanzador de Recursos",
- "resourceSidebarLauncherTitle": "Lanzador",
- "resourceLauncherDescription": "Ver todos los recursos disponibles y lanzarlos desde un único centro",
- "resourceLauncherSearchPlaceholder": "Buscar tus recursos...",
- "resourceLauncherDefaultView": "Predeterminado",
- "resourceLauncherSaveView": "Guardar Vista",
- "resourceLauncherSaveToCurrentView": "Guardar en la Vista Actual",
- "resourceLauncherSaveDefaultPersonal": "Guardar para mí",
- "resourceLauncherResetView": "Restablecer Vista",
- "resourceLauncherResetSystemDefault": "Restaurar a la configuración predeterminada del sistema",
- "resourceLauncherSystemDefaultRestored": "Configuración del sistema restaurada",
- "resourceLauncherSystemDefaultRestoredDescription": "La vista predeterminada se ha restablecido a la configuración original.",
- "resourceLauncherSaveAsNewView": "Guardar como Nueva Vista",
- "resourceLauncherSaveAsNewViewDescription": "Ponle un nombre a esta vista para guardar tus filtros y diseño actuales.",
- "resourceLauncherSaveForEveryone": "Guardar para Todos",
- "resourceLauncherSaveForEveryoneDescription": "Comparte esta vista con todos los miembros de la organización. Si está desmarcado, la vista solo es visible para ti.",
- "resourceLauncherMakePersonal": "Hacer Personal",
- "resourceLauncherFilter": "Filtro",
- "resourceLauncherFilterWithCount": "Filtro, {count} aplicado",
- "resourceLauncherSort": "Ordenar",
- "resourceLauncherSortAscending": "Ordenar Ascendente",
- "resourceLauncherSortDescending": "Ordenar Descendente",
- "resourceLauncherSettings": "Ajustes",
- "resourceLauncherGroupBy": "Agrupar Por",
- "resourceLauncherGroupBySite": "Sitio",
- "resourceLauncherGroupByLabel": "Etiqueta",
- "resourceLauncherGroupByNone": "Ninguna",
- "resourceLauncherLayout": "Disposición",
- "resourceLauncherLayoutGrid": "Cuadrícula",
- "resourceLauncherLayoutList": "Lista",
- "resourceLauncherShowLabels": "Mostrar Etiquetas",
- "resourceLauncherShowSiteTags": "Mostrar Etiquetas del Sitio",
- "resourceLauncherShowRecents": "Mostrar Recientes",
- "resourceLauncherDeleteView": "Eliminar Vista",
- "resourceLauncherDeleteViewTitle": "Eliminar Vista",
- "resourceLauncherDeleteViewQuestion": "¿Está seguro de que desea eliminar esta vista de lanzador?",
- "resourceLauncherDeleteViewConfirm": "Eliminar Vista",
- "resourceLauncherViewAsAdmin": "Ver como Administrador",
- "resourceLauncherResourceDetailsDescription": "Información de conexión y estado de este recurso.",
- "resourceLauncherResourceDetails": "Detalles del recurso",
- "resourceLauncherAuthMethodsDescription": "Métodos de autenticación habilitados para este recurso.",
- "resourceLauncherPrivateClientRequired": "Conéctese con un cliente en su dispositivo para acceder a este recurso en privado.",
- "resourceLauncherPrivateClientRequiredTitle": "Se requiere conexión del cliente",
- "resourceLauncherDownloadClient": "Descargar cliente",
- "resourceLauncherFailedToLoadDetails": "No se pudieron cargar los detalles del recurso. Es posible que ya no tengas acceso a este recurso.",
- "resourceLauncherNoPortRestrictions": "Sin restricciones de puertos",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Sin Etiqueta",
- "resourceLauncherNoSite": "Sin Sitio",
- "resourceLauncherNoResourcesInGroup": "No hay recursos en este grupo",
- "resourceLauncherEmptyStateTitle": "No hay Recursos Disponibles",
- "resourceLauncherEmptyStateDescription": "Todavía no tienes acceso a ningún recurso. Contacta a tu administrador para solicitar acceso.",
- "resourceLauncherEmptyStateNoResultsTitle": "No se Encontraron Recursos",
- "resourceLauncherEmptyStateNoResultsDescription": "No hay recursos que coincidan con tu búsqueda o filtros actuales. Intenta ajustarlos para encontrar lo que buscas.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "No hay recursos que coincidan con \"{query}\". Intenta ajustar tu búsqueda o borrar filtros para ver todos los recursos.",
- "resourceLauncherSearchFirstTitle": "Buscar o filtrar para navegar",
- "resourceLauncherSearchFirstDescription": "Tiene acceso a muchos recursos. Use la búsqueda o filtre por sitio o etiqueta para encontrar lo que necesita.",
- "resourceLauncherSiteGroupingDisabled": "La agrupación por sitios no está disponible en esta escala. Filtre por sitio para agrupar un conjunto más pequeño.",
- "resourceLauncherLabelGroupingDisabled": "La agrupación por etiquetas no está disponible en esta escala.",
- "resourceLauncherCompactModeHint": "Mostrando una lista simplificada para una navegación más rápida. Use la búsqueda o los filtros para limitar los resultados.",
- "resourceLauncherCompactGroupingHint": "Aplique filtros de sitio o etiqueta para habilitar la agrupación.",
- "resourceLauncherCopiedToClipboard": "Copiado al portapapeles",
- "resourceLauncherCopiedAccessDescription": "El acceso al recurso ha sido copiado a tu portapapeles.",
- "resourceLauncherViewNamePlaceholder": "Nombre de la Vista",
- "resourceLauncherViewNameLabel": "Nombre de la Vista",
- "resourceLauncherViewSaved": "Vista guardada",
- "resourceLauncherViewSavedDescription": "Tu vista del lanzador ha sido guardada.",
- "resourceLauncherViewSaveFailed": "Error al guardar la vista",
- "resourceLauncherViewSaveFailedDescription": "No se pudo guardar la vista del lanzador. Por favor, intenta de nuevo.",
- "resourceLauncherViewDeleted": "Vista eliminada",
- "resourceLauncherViewDeletedDescription": "La vista del lanzador ha sido eliminada.",
- "resourceLauncherViewDeleteFailed": "Error al eliminar la vista",
- "resourceLauncherViewDeleteFailedDescription": "No se pudo eliminar la vista del lanzador. Por favor, intenta de nuevo.",
"memberPortalPrevious": "Anterior",
"memberPortalNext": "Siguiente",
"httpSettings": "Configuración HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----COMIENZO DE LA CLAVE PRIVADA OPENSSH-----",
"sshPrivateKeyRequired": "Se requiere clave privada",
"vncTitle": "VNC",
- "vncSignInDescription": "Introduce tus credenciales VNC para conectarte",
- "vncUsernameOptional": "Nombre de usuario (opcional)",
+ "vncSignInDescription": "Introduce tu contraseña VNC para conectar",
"vncPasswordOptional": "Contraseña (opcional)",
"vncNoResourceTarget": "No hay objetivo de recurso disponible",
"vncFailedToLoadNovnc": "Error al cargar noVNC",
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index 4084ceae8..c2abdbe02 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -66,15 +66,9 @@
"local": "Locale",
"edit": "Modifier",
"siteConfirmDelete": "Confirmer la suppression du nœud",
- "siteConfirmDeleteAndResources": "Confirmer la suppression du site et des ressources",
"siteDelete": "Supprimer le nœud",
- "siteDeleteAndResources": "Supprimer le site et les ressources",
"siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.",
- "siteMessageRemoveAndResources": "Cela supprimera définitivement toutes les ressources publiques et privées liées à ce site, même si une ressource est également associée à d'autres sites.",
"siteQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce nœud de l'organisation ?",
- "siteQuestionRemoveAndResources": "Êtes-vous sûr de vouloir supprimer ce site et toutes les ressources associées?",
- "sitesTableDeleteSite": "Supprimer le site",
- "sitesTableDeleteSiteAndResources": "Supprimer le site et les ressources",
"siteManageSites": "Gérer les nœuds",
"siteDescription": "Créer et gérer des sites pour activer la connectivité aux réseaux privés",
"sitesBannerTitle": "Se connecter à n'importe quel réseau",
@@ -123,16 +117,6 @@
"siteUpdated": "Nœud mis à jour",
"siteUpdatedDescription": "Le nœud a été mis à jour.",
"siteGeneralDescription": "Configurer les paramètres par défaut de ce nœud",
- "siteRestartTitle": "Redémarrer Site",
- "siteRestartDescription": "Redémarrer le tunnel WireGuard pour ce site. Cela interrompra brièvement la connectivité.",
- "siteRestartBody": "Utilisez cela si le tunnel du site ne fonctionne pas correctement et que vous souhaitez forcer une reconnexion sans redémarrer l'hôte.",
- "siteRestartButton": "Redémarrer Site",
- "siteRestartDialogMessage": "Êtes-vous sûr de vouloir redémarrer le tunnel WireGuard pour {name}? Le site perdra brièvement sa connectivité.",
- "siteRestartWarning": "Le site sera brièvement déconnecté pendant le redémarrage du tunnel.",
- "siteRestarted": "Site redémarré",
- "siteRestartedDescription": "Le tunnel WireGuard a été redémarré.",
- "siteErrorRestart": "Échec du redémarrage du site",
- "siteErrorRestartDescription": "Une erreur s'est produite lors du redémarrage du site.",
"siteSettingDescription": "Configurer les paramètres du site",
"siteResourcesTab": "Ressources",
"siteResourcesNoneOnSite": "Ce site n'a pas encore de ressources publiques ou privées.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Requêtes de proxy sur TCP/UDP brute en utilisant un numéro de port. Nécessite des sites pour se connecter à un noeud distant.",
"resourceCreate": "Créer une ressource",
"resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource",
- "resourcePublicCreate": "Créer une ressource publique",
- "resourcePublicCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource publique accessible via un navigateur web",
"resourceCreateGeneralDescription": "Configurer les paramètres de ressource de base, y compris le nom et le type",
"resourceSeeAll": "Voir toutes les ressources",
"resourceCreateGeneral": "Général",
@@ -615,8 +597,7 @@
"idpNameInternal": "Interne",
"emailInvalid": "Adresse e-mail invalide",
"inviteValidityDuration": "Veuillez sélectionner une durée",
- "accessRoleSelectPlease": "Un utilisateur doit appartenir à au moins un rôle.",
- "accessRoleRequired": "Rôle requis",
+ "accessRoleSelectPlease": "Veuillez sélectionner un rôle",
"removeOwnAdminRoleConfirmTitle": "Retirer votre accès administrateur ?",
"removeOwnAdminRoleConfirmDescription": "Vous n'aurez plus de droits d'administrateur dans cette organisation après avoir enregistré. Un autre administrateur pourra restaurer cet accès si nécessaire.",
"removeOwnAdminRoleConfirmButton": "Retirer mon accès administrateur",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Authentification de l'en-tête de base",
"policyAuthHeaderAuthDescription": "Validez un nom et une valeur d'en-tête HTTP personnalisé à chaque requête",
"policyAuthHeaderAuthSummary": "En-tête configuré",
- "policyAuthHeaderName": "Nom d'utilisateur",
- "policyAuthHeaderValue": "Mot de passe",
+ "policyAuthHeaderName": "Nom de l'en-tête",
+ "policyAuthHeaderValue": "Valeur attendue",
"policyAuthSetPasscode": "Définir le code confidentiel",
"policyAuthSetPincode": "Définir le code PIN",
"policyAuthSetEmailWhitelist": "Définir la liste blanche des e-mails",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Appliquer la Config",
"actionListBlueprints": "Lister les plans",
"actionGetBlueprint": "Obtenez un plan",
- "actionCreateOrgWideLauncherView": "Créer une vue de lancement au niveau de l'organisation",
"setupToken": "Jeton de configuration",
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Définir le code PIN de la ressource",
"actionSetResourceEmailWhitelist": "Définir la liste blanche des emails de la ressource",
"actionGetResourceEmailWhitelist": "Obtenir la liste blanche des emails de la ressource",
- "actionGetResourcePolicy": "Obtenir une politique de ressources",
- "actionUpdateResourcePolicy": "Mettre à jour la politique de ressources",
- "actionSetResourcePolicyUsers": "Définir les utilisateurs de la politique de ressources",
- "actionSetResourcePolicyRoles": "Définir les rôles de la politique de ressources",
- "actionSetResourcePolicyPassword": "Définir le mot de passe de la politique de ressources",
- "actionSetResourcePolicyPincode": "Définir le code PIN de la politique de ressources",
- "actionSetResourcePolicyHeaderAuth": "Définir l'authentification des en-têtes de la politique de ressources",
- "actionSetResourcePolicyWhitelist": "Définir la liste blanche des emails de la politique de ressources",
- "actionSetResourcePolicyRules": "Définir les règles de la politique de ressources",
"actionCreateTarget": "Créer une cible",
"actionDeleteTarget": "Supprimer une cible",
"actionGetTarget": "Obtenir une cible",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Générer un jeton d'accès",
"actionDeleteAccessToken": "Supprimer un jeton d'accès",
"actionListAccessTokens": "Lister les jetons d'accès",
- "actionCreateResourceSessionToken": "Créer un jeton de session de ressource",
"actionCreateResourceRule": "Créer une règle de ressource",
"actionDeleteResourceRule": "Supprimer une règle de ressource",
"actionListResourceRules": "Lister les règles de ressource",
@@ -1520,30 +1490,6 @@
"navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application",
"navbarDocsLink": "Documentation",
- "commandPaletteTitle": "Palette de commandes",
- "commandPaletteDescription": "Rechercher des pages, organisations, ressources et actions",
- "commandPaletteSearchPlaceholder": "Rechercher des pages, ressources, actions...",
- "commandPaletteNoResults": "Aucun résultat trouvé.",
- "commandPaletteSearching": "Recherche en cours...",
- "commandPaletteNavigation": "Navigation",
- "commandPaletteOrganizations": "Organisations",
- "commandPaletteSites": "Sites",
- "commandPaletteResources": "Ressource",
- "commandPaletteUsers": "Utilisateurs",
- "commandPaletteClients": "Liste des clients",
- "commandPaletteActions": "Actions",
- "commandPaletteCreateSite": "Créer un site",
- "commandPaletteCreateProxyResource": "Créer une ressource publique",
- "commandPaletteCreatePrivateResource": "Créer une ressource privée",
- "commandPaletteCreateUser": "Créer un utilisateur",
- "commandPaletteCreateApiKey": "Créer une clé API",
- "commandPaletteCreateMachineClient": "Créer un client machine",
- "commandPaletteCreateAlertRule": "Créer une règle d'alerte",
- "commandPaletteCreateIdentityProvider": "Créer un fournisseur d'identité",
- "commandPaletteToggleTheme": "Changer de thème",
- "commandPaletteChooseOrganization": "Choisir une organisation",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Impossible d'activer l'A2F",
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Gestion",
"sidebarBillingAndLicenses": "Facturation & Licences",
"sidebarLogsAnalytics": "Analyses",
- "commandSites": "Nœuds",
- "commandActionModeInfo": "Tapez \">\" Pour ouvrir le mode action",
- "commandResources": "Ressource",
- "commandProxyResources": "Ressources publiques",
- "commandClientResources": "Ressources privées",
- "commandClients": "Clients",
- "commandUserDevices": "Appareils utilisateur",
- "commandMachineClients": "Clients machine",
- "commandDomains": "Domaines",
- "commandRemoteExitNodes": "Nœuds distants",
- "commandTeam": "Équipe",
- "commandUsers": "Utilisateurs",
- "commandRoles": "Rôles",
- "commandInvitations": "Invitations",
- "commandPolicies": "Politiques partagées",
- "commandResourcePolicies": "Politiques de ressources publiques",
- "commandIdentityProviders": "Fournisseurs d'identité",
- "commandApprovals": "Demandes d'approbation",
- "commandShareableLinks": "Liens partageables",
- "commandOrganization": "Organisation",
- "commandLogsAndAnalytics": "Journaux & Analyses",
- "commandLogsAnalytics": "Analyses",
- "commandLogsRequest": "Journaux des requêtes HTTP",
- "commandLogsAccess": "Journaux d'authentification",
- "commandLogsAction": "Journaux des actions administratives",
- "commandLogsConnection": "Journaux de connexion",
- "commandLogsStreaming": "Diffusion d'événements",
- "commandManagement": "Gestion",
- "commandAlerting": "Alerte",
- "commandProvisioning": "Provisionnement",
- "commandBluePrints": "Configs",
- "commandApiKeys": "Clés d'API",
- "commandBillingAndLicenses": "Facturation & Licences",
- "commandBilling": "Facturation",
- "commandEnterpriseLicenses": "Licence",
- "commandSettings": "Réglages",
- "commandLauncher": "Lanceur",
- "commandResourceLauncher": "Lanceur de ressources",
- "commandSearchResults": "Résultats de recherche",
"alertingTitle": "Alertes",
"alertingDescription": "Définissez des sources, des déclencheurs et des actions pour les notifications",
"alertingRules": "Règles d'alerte",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domaines",
"billingOrganizations": "Organes",
"billingRemoteExitNodes": "Nœuds distants",
- "billingPublicResources": "Ressources publiques",
- "billingPrivateResources": "Ressources privées",
- "billingMachineClients": "Clients machine",
"billingNoLimitConfigured": "Aucune limite configurée",
"billingEstimatedPeriod": "Période de facturation estimée",
"billingIncludedUsage": "Utilisation incluse",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Combien d'utilisateurs vous pouvez utiliser",
"billingDomainInfo": "Combien de domaines vous pouvez utiliser",
"billingRemoteExitNodesInfo": "Combien de nœuds distants vous pouvez utiliser",
- "billingPublicResourcesInfo": "Combien de ressources publiques pouvez-vous utiliser",
- "billingPrivateResourcesInfo": "Combien de ressources privées pouvez-vous utiliser",
- "billingMachineClientsInfo": "Combien de clients machine pouvez-vous utiliser",
"billingLicenseKeys": "Clés de licence",
"billingLicenseKeysDescription": "Gérer vos abonnements à la clé de licence",
"billingLicenseSubscription": "Abonnement à la licence",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Sous-réseau",
"addressDescription": "L'adresse interne du client. Doit être dans le sous-réseau de l'organisation.",
"selectSites": "Sélectionner des sites",
- "selectLabels": "Sélectionner des étiquettes",
"sitesDescription": "Le client aura une connectivité vers les sites sélectionnés",
"clientInstallOlm": "Installer Olm",
"clientInstallOlmDescription": "Faites fonctionner Olm sur votre système",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Fermer",
"createInternalResourceDialogCreateClientResource": "Créer une ressource privée",
"createInternalResourceDialogCreateClientResourceDescription": "Créer une nouvelle ressource qui ne sera accessible qu'aux clients connectés à l'organisation",
- "privateResourceGeneralDescription": "Configurez le nom, l'identifiant et d'autres paramètres généraux de la ressource.",
- "privateResourceCreatePageSeeAll": "Voir toutes les ressources privées",
- "privateResourceAllowIcmpPing": "Autoriser le ping ICMP",
- "privateResourceNetworkAccess": "Accès au réseau",
- "privateResourceNetworkAccessDescription": "Contrôler l'accès aux ports TCP/UDP et si le ping ICMP est autorisé pour cette ressource.",
- "hostSettings": "Paramètres de l'hôte",
- "cidrSettings": "Paramètres CIDR",
"createInternalResourceDialogResourceProperties": "Propriétés de la ressource",
"createInternalResourceDialogName": "Nom",
"createInternalResourceDialogSite": "Site",
"selectSite": "Sélectionner un site...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# étiquette} other {# étiquettes}}",
"noSitesFound": "Aucun site trouvé.",
"createInternalResourceDialogProtocol": "Protocole",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "La gamme CIDR de la ressource sur le réseau du site.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Un alias DNS interne optionnel pour cette ressource.",
- "internalResourceAliasLocalWarning": "Les alias se terminant par .local peuvent causer des problèmes de résolution dus au mDNS sur certains réseaux.",
"internalResourceDownstreamSchemeRequired": "Un schéma est requis pour les ressources HTTP",
"internalResourceHttpPortRequired": "Le port de destination est requis pour les ressources HTTP",
"siteConfiguration": "Configuration",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Nœuds distants",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Clé secrète",
- "remoteExitNodeNetworkingTitle": "Paramètres du réseau",
- "remoteExitNodeNetworkingDescription": "Configurez comment ce nœud de sortie distant acheminera le trafic et quels sites préfèrent se connecter via ce dernier. Fonctions avancées à utiliser avec les configurations réseau de retour.",
- "remoteExitNodeNetworkingSave": "Enregistrer les paramètres",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Paramètres du réseau enregistrés",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Les paramètres du réseau ont été mis à jour avec succès.",
- "remoteExitNodeNetworkingSaveError": "Échec de l'enregistrement des paramètres du réseau",
- "remoteExitNodeNetworkingSubnetsTitle": "Sous-réseaux distants",
- "remoteExitNodeNetworkingSubnetsDescription": "Définissez les plages CIDR que ce nœud de sortie distant acheminera. Saisissez un CIDR valide (par exemple 10.0.0.0/8) et appuyez sur Entrée pour ajouter.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Ajouter une plage CIDR (par exemple 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Échec du chargement des sous-réseaux",
- "remoteExitNodeNetworkingLabelsTitle": "Étiquettes de préférences",
- "remoteExitNodeNetworkingLabelsDescription": "Les sites avec ces étiquettes devront se connecter via ce nœud de sortie distant.",
- "remoteExitNodeNetworkingLabelsButtonText": "Sélectionner des étiquettes...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Chercher des étiquettes...",
- "remoteExitNodeNetworkingLabelsLoadError": "Échec du chargement des étiquettes",
"remoteExitNodeCreate": {
"title": "Créer un nœud distant",
"description": "Créez un nouveau nœud de relais et de serveur proxy distant auto-hébergé",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Aucun noeud n'est disponible pour cette organisation. Créez d'abord un noeud pour utiliser des sites locaux.",
"exitNode": "Nœud de sortie",
"country": "Pays",
- "countryIsNot": "Le pays n'est pas",
"rulesMatchCountry": "Actuellement basé sur l'IP source",
"region": "Région",
"selectRegion": "Sélectionner une région",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Fournisseur Google OAuth2/OIDC",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Sous-réseau",
- "utilitySubnet": "Routeur utilitaire",
"subnetDescription": "Le sous-réseau de la configuration réseau de cette organisation.",
"customDomain": "Domaine personnalisé",
"authPage": "Pages d'authentification",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "La configuration d'authentification à deux facteurs est requise. Veuillez vous reconnecter via {dashboardUrl}/auth/login terminé cette étape. Puis revenez ici.",
"additionalSecurityRequired": "Sécurité supplémentaire requise",
"organizationRequiresAdditionalSteps": "Cette organisation nécessite des étapes de sécurité supplémentaires avant de pouvoir accéder aux ressources.",
- "sessionExpired": "Session expirée",
- "sessionExpiredReauthRequired": "Votre session a expiré selon la politique de sécurité de votre organisation. Veuillez vous ré-authentifier pour continuer.",
- "reauthenticate": "Se ré-authentifier",
"completeTheseSteps": "Compléter ces étapes",
"enableTwoFactorAuthentication": "Activer l'authentification à deux facteurs",
"completeSecuritySteps": "Compléter les étapes de sécurité",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "L'organisation ou l'identifiant de domaine est manquant",
"loadingDNSRecords": "Chargement des enregistrements DNS...",
"olmUpdateAvailableInfo": "Une version mise à jour de Olm est disponible. Veuillez mettre à jour vers la dernière version pour la meilleure expérience.",
- "updateAvailableInfo": "Une version mise à jour est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
"client": "Client",
"proxyProtocol": "Paramètres du protocole proxy",
"proxyProtocolDescription": "Configurer le protocole Proxy pour préserver les adresses IP du client pour les services TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Ajouter des utilisateurs",
"editInternalResourceDialogAddClients": "Ajouter des clients",
"editInternalResourceDialogDestinationLabel": "Destination",
- "editInternalResourceDialogDestinationDescription": "Configurez comment les clients accèdent à cette ressource.",
+ "editInternalResourceDialogDestinationDescription": "Indiquez l'adresse de destination pour la ressource interne. Cela peut être un nom d'hôte, une adresse IP ou une plage CIDR selon le mode sélectionné. Définissez éventuellement un alias DNS interne pour une identification plus facile.",
"internalResourceFormMultiSiteRoutingHelp": "La sélection de plusieurs sites permet un routage résilient et un basculement pour une haute disponibilité.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "En savoir plus",
"editInternalResourceDialogPortRestrictionsDescription": "Restreindre l'accès à des ports TCP/UDP spécifiques ou autoriser/bloquer tous les ports.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Liste blanche des e-mails",
"memberPortalResourceDisabled": "Ressource désactivée",
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
- "resourceLauncherTitle": "Lanceur de ressources",
- "resourceSidebarLauncherTitle": "Lanceur",
- "resourceLauncherDescription": "Afficher toutes les ressources disponibles et les lancer depuis un hub central",
- "resourceLauncherSearchPlaceholder": "Chercher vos ressources...",
- "resourceLauncherDefaultView": "Par défaut",
- "resourceLauncherSaveView": "Enregistrer la vue",
- "resourceLauncherSaveToCurrentView": "Enregistrer dans la vue actuelle",
- "resourceLauncherSaveDefaultPersonal": "Sauvegarder pour moi",
- "resourceLauncherResetView": "Réinitialiser la vue",
- "resourceLauncherResetSystemDefault": "Réinitialiser aux paramètres système par défaut",
- "resourceLauncherSystemDefaultRestored": "Paramètre système par défaut restauré",
- "resourceLauncherSystemDefaultRestoredDescription": "La vue par défaut a été réinitialisée aux paramètres d'origine.",
- "resourceLauncherSaveAsNewView": "Enregistrer comme nouvelle vue",
- "resourceLauncherSaveAsNewViewDescription": "Donnez un nom à cette vue pour enregistrer vos filtres et mise en page actuels.",
- "resourceLauncherSaveForEveryone": "Enregistrer pour tout le monde",
- "resourceLauncherSaveForEveryoneDescription": "Partagez cette vue avec tous les membres de l'organisation. Lorsque décochée, la vue est visible uniquement par vous.",
- "resourceLauncherMakePersonal": "Rendre personnel",
- "resourceLauncherFilter": "Filtrer",
- "resourceLauncherFilterWithCount": "Filtrer, {count} appliqué",
- "resourceLauncherSort": "Trier",
- "resourceLauncherSortAscending": "Trier par ordre croissant",
- "resourceLauncherSortDescending": "Trier par ordre décroissant",
- "resourceLauncherSettings": "Réglages",
- "resourceLauncherGroupBy": "Grouper par",
- "resourceLauncherGroupBySite": "Nœud",
- "resourceLauncherGroupByLabel": "Étiquette",
- "resourceLauncherGroupByNone": "Aucun",
- "resourceLauncherLayout": "Mise en page",
- "resourceLauncherLayoutGrid": "Grille",
- "resourceLauncherLayoutList": "Liste",
- "resourceLauncherShowLabels": "Afficher les étiquettes",
- "resourceLauncherShowSiteTags": "Afficher les tags de site",
- "resourceLauncherShowRecents": "Afficher les récents",
- "resourceLauncherDeleteView": "Supprimer la vue",
- "resourceLauncherDeleteViewTitle": "Supprimer la vue",
- "resourceLauncherDeleteViewQuestion": "Êtes-vous sûr de vouloir supprimer cette vue de lancement ?",
- "resourceLauncherDeleteViewConfirm": "Supprimer la vue",
- "resourceLauncherViewAsAdmin": "Voir en tant qu'admin",
- "resourceLauncherResourceDetailsDescription": "Informations de connexion et statut pour cette ressource.",
- "resourceLauncherResourceDetails": "Détails de la ressource",
- "resourceLauncherAuthMethodsDescription": "Méthodes d'authentification activées pour cette ressource.",
- "resourceLauncherPrivateClientRequired": "Connectez-vous avec un client sur votre appareil pour accéder à cette ressource en privé.",
- "resourceLauncherPrivateClientRequiredTitle": "Connexion client requise",
- "resourceLauncherDownloadClient": "Télécharger le client",
- "resourceLauncherFailedToLoadDetails": "Impossible de charger les détails de la ressource. Vous n'avez peut-être plus accès à cette ressource.",
- "resourceLauncherNoPortRestrictions": "Aucune restriction de port",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Non étiqueté",
- "resourceLauncherNoSite": "Aucun nœud",
- "resourceLauncherNoResourcesInGroup": "Aucune ressource dans ce groupe",
- "resourceLauncherEmptyStateTitle": "Aucune ressource disponible",
- "resourceLauncherEmptyStateDescription": "Vous n'avez pas encore accès à des ressources. Contactez votre administrateur pour demander l'accès.",
- "resourceLauncherEmptyStateNoResultsTitle": "Aucune ressource trouvée",
- "resourceLauncherEmptyStateNoResultsDescription": "Aucune ressource ne correspond à votre recherche ou filtre actuel. Essayez de les ajuster pour trouver ce que vous cherchez.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Aucune ressource ne correspond à \"{query}\". Essayez d'ajuster votre recherche ou de supprimer les filtres pour voir toutes les ressources.",
- "resourceLauncherSearchFirstTitle": "Rechercher ou filtrer pour parcourir",
- "resourceLauncherSearchFirstDescription": "Vous avez accès à de nombreuses ressources. Utilisez la recherche ou filtrez par site ou étiquette pour trouver ce dont vous avez besoin.",
- "resourceLauncherSiteGroupingDisabled": "Le regroupement par site n'est pas disponible à cette échelle. Filtrez par site pour regrouper un ensemble plus restreint.",
- "resourceLauncherLabelGroupingDisabled": "Le regroupement par étiquette n'est pas disponible à cette échelle.",
- "resourceLauncherCompactModeHint": "Affichage d'une liste simplifiée pour une navigation plus rapide. Utilisez la recherche ou les filtres pour affiner les résultats.",
- "resourceLauncherCompactGroupingHint": "Appliquer des filtres par site ou étiquette pour activer le regroupement.",
- "resourceLauncherCopiedToClipboard": "Copié dans le presse-papiers",
- "resourceLauncherCopiedAccessDescription": "L'accès à la ressource a été copié dans votre presse-papiers.",
- "resourceLauncherViewNamePlaceholder": "Nom de la vue",
- "resourceLauncherViewNameLabel": "Nom de la vue",
- "resourceLauncherViewSaved": "Vue enregistrée",
- "resourceLauncherViewSavedDescription": "Votre vue de lancement a été enregistrée.",
- "resourceLauncherViewSaveFailed": "Échec de l'enregistrement de la vue",
- "resourceLauncherViewSaveFailedDescription": "Impossible d'enregistrer la vue de lancement. Veuillez réessayer.",
- "resourceLauncherViewDeleted": "Vue supprimée",
- "resourceLauncherViewDeletedDescription": "La vue de lancement a été supprimée.",
- "resourceLauncherViewDeleteFailed": "Impossible de supprimer la vue",
- "resourceLauncherViewDeleteFailedDescription": "Impossible de supprimer la vue de lancement. Veuillez réessayer.",
"memberPortalPrevious": "Précédent",
"memberPortalNext": "Suivant",
"httpSettings": "Paramètres HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Une clé privée est requise",
"vncTitle": "VNC",
- "vncSignInDescription": "Entrez vos identifiants VNC pour vous connecter",
- "vncUsernameOptional": "Nom d'utilisateur (optionnel)",
+ "vncSignInDescription": "Entrez votre mot de passe VNC pour vous connecter",
"vncPasswordOptional": "Mot de passe (facultatif)",
"vncNoResourceTarget": "Aucune cible de ressource disponible",
"vncFailedToLoadNovnc": "Échec du chargement de noVNC",
diff --git a/messages/it-IT.json b/messages/it-IT.json
index ebb398334..7623e8f64 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -66,15 +66,9 @@
"local": "Locale",
"edit": "Modifica",
"siteConfirmDelete": "Conferma Eliminazione Sito",
- "siteConfirmDeleteAndResources": "Conferma Eliminazione Sito e Risorse",
"siteDelete": "Elimina Sito",
- "siteDeleteAndResources": "Elimina Sito e Risorse",
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.",
- "siteMessageRemoveAndResources": "Questo eliminerà permanentemente tutte le risorse pubbliche e private collegate a questo sito, anche se una risorsa è anche associata ad altri siti.",
"siteQuestionRemove": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
- "siteQuestionRemoveAndResources": "Sei sicuro di voler eliminare questo sito e tutte le risorse associate?",
- "sitesTableDeleteSite": "Elimina Sito",
- "sitesTableDeleteSiteAndResources": "Elimina Sito e Risorse",
"siteManageSites": "Gestisci Siti",
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
"sitesBannerTitle": "Connetti Qualsiasi Rete",
@@ -123,16 +117,6 @@
"siteUpdated": "Sito aggiornato",
"siteUpdatedDescription": "Il sito è stato aggiornato.",
"siteGeneralDescription": "Configura le impostazioni generali per questo sito",
- "siteRestartTitle": "Riavvia Sito",
- "siteRestartDescription": "Riavvia il tunnel WireGuard per questo sito. Questo interromperà brevemente la connettività.",
- "siteRestartBody": "Usalo se il tunnel del sito non funziona correttamente e vuoi forzare un riconnessione senza riavviare l'host.",
- "siteRestartButton": "Riavvia Sito",
- "siteRestartDialogMessage": "Sei sicuro di voler riavviare il tunnel WireGuard per {name}? Il sito perderà brevemente la connettività.",
- "siteRestartWarning": "Il sito si disconnette brevemente mentre il tunnel si riavvia.",
- "siteRestarted": "Sito riavviato",
- "siteRestartedDescription": "Il tunnel WireGuard è stato riavviato.",
- "siteErrorRestart": "Impossibile riavviare il sito",
- "siteErrorRestartDescription": "Si è verificato un errore durante il riavvio del sito.",
"siteSettingDescription": "Configura le impostazioni del sito",
"siteResourcesTab": "Risorse",
"siteResourcesNoneOnSite": "Questo sito non ha ancora risorse pubbliche o private.",
@@ -266,8 +250,6 @@
"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",
- "resourcePublicCreate": "Crea Risorsa Pubblica",
- "resourcePublicCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa pubblica accessibile tramite un browser web",
"resourceCreateGeneralDescription": "Configura le impostazioni generali delle risorse, inclusi il nome e il tipo",
"resourceSeeAll": "Vedi Tutte Le Risorse",
"resourceCreateGeneral": "Generale",
@@ -615,8 +597,7 @@
"idpNameInternal": "Interno",
"emailInvalid": "Indirizzo email non valido",
"inviteValidityDuration": "Seleziona una durata",
- "accessRoleSelectPlease": "Un utente deve appartenere ad almeno un ruolo.",
- "accessRoleRequired": "Ruolo richiesto",
+ "accessRoleSelectPlease": "Seleziona un ruolo",
"removeOwnAdminRoleConfirmTitle": "Rimuovere il tuo accesso amministrativo?",
"removeOwnAdminRoleConfirmDescription": "Non avrai più i permessi di amministratore in questa organizzazione dopo il salvataggio. Un altro amministratore può ripristinare l'accesso se necessario.",
"removeOwnAdminRoleConfirmButton": "Rimuovere il Mio Accesso Amministrativo",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Autenticazione Header Base",
"policyAuthHeaderAuthDescription": "Convalida un nome e un valore di intestazione HTTP personalizzato su ogni richiesta",
"policyAuthHeaderAuthSummary": "Intestazione configurata",
- "policyAuthHeaderName": "Nome utente",
- "policyAuthHeaderValue": "Password",
+ "policyAuthHeaderName": "Nome dell'intestazione",
+ "policyAuthHeaderValue": "Valore atteso",
"policyAuthSetPasscode": "Imposta Codice di Accesso",
"policyAuthSetPincode": "Imposta Codice PIN",
"policyAuthSetEmailWhitelist": "Imposta Lista Autorizzazioni Email",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Applica Progetto",
"actionListBlueprints": "Elenco Blueprints",
"actionGetBlueprint": "Ottieni Blueprint",
- "actionCreateOrgWideLauncherView": "Crea Visualizzazione Lanscia Org-Wide",
"setupToken": "Configura Token",
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
"setupTokenRequired": "Il token di configurazione è richiesto",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Imposta Codice PIN Risorsa",
"actionSetResourceEmailWhitelist": "Imposta Lista Autorizzazioni Email Risorsa",
"actionGetResourceEmailWhitelist": "Ottieni Lista Autorizzazioni Email Risorsa",
- "actionGetResourcePolicy": "Ottieni la Politica della Risorsa",
- "actionUpdateResourcePolicy": "Aggiorna la Politica della Risorsa",
- "actionSetResourcePolicyUsers": "Imposta Utenti della Politica Risorsa",
- "actionSetResourcePolicyRoles": "Imposta Ruoli della Politica Risorsa",
- "actionSetResourcePolicyPassword": "Imposta Password della Politica Risorsa",
- "actionSetResourcePolicyPincode": "Imposta Codice PIN della Politica Risorsa",
- "actionSetResourcePolicyHeaderAuth": "Imposta Autenticazione Header della Politica Risorsa",
- "actionSetResourcePolicyWhitelist": "Imposta Lista Autorizzazioni Email della Politica Risorsa",
- "actionSetResourcePolicyRules": "Imposta Regole della Politica Risorsa",
"actionCreateTarget": "Crea Target",
"actionDeleteTarget": "Elimina Target",
"actionGetTarget": "Ottieni Target",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Genera Token di Accesso",
"actionDeleteAccessToken": "Elimina Token di Accesso",
"actionListAccessTokens": "Elenca Token di Accesso",
- "actionCreateResourceSessionToken": "Crea Token di Sessione della Risorsa",
"actionCreateResourceRule": "Crea Regola Risorsa",
"actionDeleteResourceRule": "Elimina Regola Risorsa",
"actionListResourceRules": "Elenca Regole Risorsa",
@@ -1520,30 +1490,6 @@
"navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione",
"navbarDocsLink": "Documentazione",
- "commandPaletteTitle": "Tavolozza dei Comandi",
- "commandPaletteDescription": "Cerca pagine, organizzazioni, risorse e azioni",
- "commandPaletteSearchPlaceholder": "Cerca pagine, risorse, azioni...",
- "commandPaletteNoResults": "Nessun risultato trovato.",
- "commandPaletteSearching": "Ricerca...",
- "commandPaletteNavigation": "Navigazione",
- "commandPaletteOrganizations": "Organizzazioni",
- "commandPaletteSites": "Siti",
- "commandPaletteResources": "Risorse",
- "commandPaletteUsers": "Utenti",
- "commandPaletteClients": "Client Macchina",
- "commandPaletteActions": "Azioni",
- "commandPaletteCreateSite": "Crea Sito",
- "commandPaletteCreateProxyResource": "Crea Risorsa Pubblica",
- "commandPaletteCreatePrivateResource": "Crea Risorsa Privata",
- "commandPaletteCreateUser": "Crea Utente",
- "commandPaletteCreateApiKey": "Crea Chiave API",
- "commandPaletteCreateMachineClient": "Crea Client Macchina",
- "commandPaletteCreateAlertRule": "Crea Regola di Avviso",
- "commandPaletteCreateIdentityProvider": "Crea Provider di Identità",
- "commandPaletteToggleTheme": "Attiva/Disattiva Tema",
- "commandPaletteChooseOrganization": "Scegli Organizzazione",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Impossibile abilitare 2FA",
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
"otpSetupCheckCode": "Inserisci un codice a 6 cifre",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Gestione",
"sidebarBillingAndLicenses": "Fatturazione E Licenze",
"sidebarLogsAnalytics": "Analisi",
- "commandSites": "Siti",
- "commandActionModeInfo": "Digita \">\" Per Aprire la Modalità Azione",
- "commandResources": "Risorse",
- "commandProxyResources": "Risorse Pubbliche",
- "commandClientResources": "Risorse Private",
- "commandClients": "Client",
- "commandUserDevices": "Dispositivi Utente",
- "commandMachineClients": "Client Macchina",
- "commandDomains": "Domini",
- "commandRemoteExitNodes": "Nodi Remoti",
- "commandTeam": "Squadra",
- "commandUsers": "Utenti",
- "commandRoles": "Ruoli",
- "commandInvitations": "Inviti",
- "commandPolicies": "Politiche Condivise",
- "commandResourcePolicies": "Politiche delle Risorse Pubbliche",
- "commandIdentityProviders": "Fornitori di Identità",
- "commandApprovals": "Richieste di Approvazione",
- "commandShareableLinks": "Link Condivisibili",
- "commandOrganization": "Organizzazione",
- "commandLogsAndAnalytics": "Log & Analisi",
- "commandLogsAnalytics": "Analisi",
- "commandLogsRequest": "Log di Richieste HTTP",
- "commandLogsAccess": "Log di Autenticazione",
- "commandLogsAction": "Log delle Azioni Amministrative",
- "commandLogsConnection": "Log di Connessione",
- "commandLogsStreaming": "Streaming di Eventi",
- "commandManagement": "Gestione",
- "commandAlerting": "Avvisi",
- "commandProvisioning": "Provisioning",
- "commandBluePrints": "Modelli",
- "commandApiKeys": "Chiavi API",
- "commandBillingAndLicenses": "Fatturazione & Licenze",
- "commandBilling": "Fatturazione",
- "commandEnterpriseLicenses": "Licenze",
- "commandSettings": "Impostazioni",
- "commandLauncher": "Avvio",
- "commandResourceLauncher": "Launcher delle Risorse",
- "commandSearchResults": "Risultati della Ricerca",
"alertingTitle": "Allerta",
"alertingDescription": "Definisci fonti, trigger e azioni per le notifiche",
"alertingRules": "Regole di allerta",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domini",
"billingOrganizations": "Organi",
"billingRemoteExitNodes": "Nodi Remoti",
- "billingPublicResources": "Risorse Pubbliche",
- "billingPrivateResources": "Risorse Private",
- "billingMachineClients": "Client Macchina",
"billingNoLimitConfigured": "Nessun limite configurato",
"billingEstimatedPeriod": "Periodo di Fatturazione Stimato",
"billingIncludedUsage": "Utilizzo Incluso",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Quanti utenti puoi usare",
"billingDomainInfo": "Quanti domini puoi usare",
"billingRemoteExitNodesInfo": "Quanti nodi remoti puoi usare",
- "billingPublicResourcesInfo": "Quante risorse pubbliche puoi utilizzare",
- "billingPrivateResourcesInfo": "Quante risorse private puoi utilizzare",
- "billingMachineClientsInfo": "Quanti client macchina puoi utilizzare",
"billingLicenseKeys": "Chiavi di Licenza",
"billingLicenseKeysDescription": "Gestisci le sottoscrizioni alla chiave di licenza",
"billingLicenseSubscription": "Abbonamento Licenza",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Sottorete",
"addressDescription": "L'indirizzo interno del client. Deve rientrare nella sottorete dell'organizzazione.",
"selectSites": "Seleziona siti",
- "selectLabels": "Seleziona etichette",
"sitesDescription": "Il cliente avrà connettività ai siti selezionati",
"clientInstallOlm": "Installa Olm",
"clientInstallOlmDescription": "Avvia Olm sul tuo sistema",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Chiudi",
"createInternalResourceDialogCreateClientResource": "Crea Risorsa Privata",
"createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile solo ai client connessi all'organizzazione",
- "privateResourceGeneralDescription": "Configura il nome, l'identificativo e altre impostazioni generali delle risorse.",
- "privateResourceCreatePageSeeAll": "Vedi Tutte le Risorse Private",
- "privateResourceAllowIcmpPing": "Consenti ICMP Ping",
- "privateResourceNetworkAccess": "Accesso alla Rete",
- "privateResourceNetworkAccessDescription": "Controlla l'accesso alle porte TCP/UDP e se ICMP ping è consentito per questa risorsa.",
- "hostSettings": "Impostazioni dell'Host",
- "cidrSettings": "Impostazioni CIDR",
"createInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
"createInternalResourceDialogName": "Nome",
"createInternalResourceDialogSite": "Sito",
"selectSite": "Seleziona sito...",
"multiSitesSelectorSitesCount": "{count, plural, one {# sito} other {# siti}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# etichetta} other {# etichette}}",
"noSitesFound": "Nessun sito trovato.",
"createInternalResourceDialogProtocol": "Protocollo",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "La gamma CIDR della risorsa sulla rete del sito.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.",
- "internalResourceAliasLocalWarning": "Gli alias che terminano in .local possono causare problemi di risoluzione a causa di mDNS su alcune reti.",
"internalResourceDownstreamSchemeRequired": "Il metodo è richiesto per risorse HTTP",
"internalResourceHttpPortRequired": "Porta di destinazione richiesta per risorse HTTP",
"siteConfiguration": "Configurazione",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Nodi Remoti",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Segreto",
- "remoteExitNodeNetworkingTitle": "Impostazioni di Rete",
- "remoteExitNodeNetworkingDescription": "Configura come questo nodo di uscita remoto indirizza il traffico e quali siti preferiscono connettersi tramite esso. Caratteristiche avanzate da utilizzare con le configurazioni di rete backhaul.",
- "remoteExitNodeNetworkingSave": "Salva Impostazioni",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Impostazioni di rete salvate",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Le impostazioni di rete sono state aggiornate con successo.",
- "remoteExitNodeNetworkingSaveError": "Impossibile salvare le impostazioni di rete",
- "remoteExitNodeNetworkingSubnetsTitle": "Sottoreti Remote",
- "remoteExitNodeNetworkingSubnetsDescription": "Definisci gli intervalli CIDR che questo nodo di uscita remota inoltrerà il traffico. Digita un CIDR valido (ad esempio 10.0.0.0/8) e premi Invio per aggiungerlo.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Aggiungi un intervallo CIDR (ad esempio 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Caricamento sottoreti fallito",
- "remoteExitNodeNetworkingLabelsTitle": "Etichette Preferenze",
- "remoteExitNodeNetworkingLabelsDescription": "I siti con queste etichette saranno collegati attraverso questo nodo di uscita remoto.",
- "remoteExitNodeNetworkingLabelsButtonText": "Seleziona etichette...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Cerca etichette...",
- "remoteExitNodeNetworkingLabelsLoadError": "Caricamento etichette fallito",
"remoteExitNodeCreate": {
"title": "Crea Nodo Remoto",
"description": "Crea un nuovo nodo server proxy e relay remoto ospitato in proprio",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Non ci sono nodi disponibili per questa organizzazione. Crea un nodo prima per usare i siti locali.",
"exitNode": "Nodo di Uscita",
"country": "Paese",
- "countryIsNot": "Paese Non È",
"rulesMatchCountry": "Attualmente basato sull'IP di origine",
"region": "Regione",
"selectRegion": "Seleziona regione",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Sottorete",
- "utilitySubnet": "Sottorete di utilità",
"subnetDescription": "La sottorete per la configurazione di rete di questa organizzazione.",
"customDomain": "Dominio Personalizzato",
"authPage": "Pagine di Autenticazione",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "È richiesta la configurazione di autenticazione a due fattori. Effettua nuovamente l'accesso tramite {dashboardUrl}/auth/login completa questo passaggio. Quindi, torna qui.",
"additionalSecurityRequired": "Necessaria Sicurezza Aggiuntiva",
"organizationRequiresAdditionalSteps": "Questa organizzazione richiede ulteriori passi di sicurezza prima di poter accedere alle risorse.",
- "sessionExpired": "Sessione Scaduta",
- "sessionExpiredReauthRequired": "La tua sessione è scaduta in conformità con la politica di sicurezza della tua organizzazione. Effettua nuovamente l'autenticazione per continuare.",
- "reauthenticate": "Riautenticazione",
"completeTheseSteps": "Completa questi passaggi",
"enableTwoFactorAuthentication": "Abilita autenticazione a due fattori",
"completeSecuritySteps": "Passi Di Sicurezza Completa",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Manca l'ID dell'organizzazione o del dominio",
"loadingDNSRecords": "Caricamento record DNS...",
"olmUpdateAvailableInfo": "È disponibile una versione aggiornata di Olm. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
- "updateAvailableInfo": "È disponibile una versione aggiornata. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
"client": "Client",
"proxyProtocol": "Impostazioni Protocollo Proxy",
"proxyProtocolDescription": "Configurare il protocollo proxy per preservare gli indirizzi IP client per i servizi TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Aggiungi Utenti",
"editInternalResourceDialogAddClients": "Aggiungi Clienti",
"editInternalResourceDialogDestinationLabel": "Destinazione",
- "editInternalResourceDialogDestinationDescription": "Configura come i client raggiungono questa risorsa.",
+ "editInternalResourceDialogDestinationDescription": "Specifica l'indirizzo di destinazione per la risorsa interna. Può essere un hostname, indirizzo IP o un intervallo CIDR a seconda della modalità selezionata. Opzionalmente imposta un alias DNS interno per una più facile identificazione.",
"internalResourceFormMultiSiteRoutingHelp": "Selezionare più siti consente un routing resiliente e Failover per alta disponibilità.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Scopri di più",
"editInternalResourceDialogPortRestrictionsDescription": "Limita l'accesso a porte TCP/UDP specifiche o consenti/blocca tutte le porte.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Lista Autorizzazioni Email",
"memberPortalResourceDisabled": "Risorsa Disabilitata",
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
- "resourceLauncherTitle": "Lanscia Risorse",
- "resourceSidebarLauncherTitle": "Avvio",
- "resourceLauncherDescription": "Visualizza tutte le risorse disponibili e avviale da un unico hub centrale",
- "resourceLauncherSearchPlaceholder": "Cerca le tue risorse...",
- "resourceLauncherDefaultView": "Predefinito",
- "resourceLauncherSaveView": "Salva Visualizzazione",
- "resourceLauncherSaveToCurrentView": "Salva alla Visualizzazione Corrente",
- "resourceLauncherSaveDefaultPersonal": "Salva per Me",
- "resourceLauncherResetView": "Reimposta Visualizzazione",
- "resourceLauncherResetSystemDefault": "Ripristina le Impostazioni di Sistema",
- "resourceLauncherSystemDefaultRestored": "Ripristinato il default di sistema",
- "resourceLauncherSystemDefaultRestoredDescription": "La vista predefinita è stata reimpostata alle impostazioni originali.",
- "resourceLauncherSaveAsNewView": "Salva come Nuova Visualizzazione",
- "resourceLauncherSaveAsNewViewDescription": "Dai un nome a questa visualizzazione per salvare i tuoi filtri e layout attuali.",
- "resourceLauncherSaveForEveryone": "Salva per Tutti",
- "resourceLauncherSaveForEveryoneDescription": "Condividi questa visualizzazione con tutti i membri dell'organizzazione. Quando non è selezionata, la visualizzazione è visibile solo a te.",
- "resourceLauncherMakePersonal": "Rendi Personale",
- "resourceLauncherFilter": "Filtro",
- "resourceLauncherFilterWithCount": "Filtro, {count} applicato/i",
- "resourceLauncherSort": "Ordina",
- "resourceLauncherSortAscending": "Ordina in ordine crescente",
- "resourceLauncherSortDescending": "Ordina in ordine decrescente",
- "resourceLauncherSettings": "Impostazioni",
- "resourceLauncherGroupBy": "Raggruppa per",
- "resourceLauncherGroupBySite": "Sito",
- "resourceLauncherGroupByLabel": "Etichetta",
- "resourceLauncherGroupByNone": "Nessuno",
- "resourceLauncherLayout": "Layout",
- "resourceLauncherLayoutGrid": "Griglia",
- "resourceLauncherLayoutList": "Lista",
- "resourceLauncherShowLabels": "Mostra Etichette",
- "resourceLauncherShowSiteTags": "Mostra Tag di Sito",
- "resourceLauncherShowRecents": "Mostra Recenti",
- "resourceLauncherDeleteView": "Elimina Visualizzazione",
- "resourceLauncherDeleteViewTitle": "Elimina Vista",
- "resourceLauncherDeleteViewQuestion": "Sei sicuro di voler eliminare questa vista del launcher?",
- "resourceLauncherDeleteViewConfirm": "Elimina Vista",
- "resourceLauncherViewAsAdmin": "Visualizza come Admin",
- "resourceLauncherResourceDetailsDescription": "Informazioni e stato della connessione per questa risorsa.",
- "resourceLauncherResourceDetails": "Dettagli Risorsa",
- "resourceLauncherAuthMethodsDescription": "Metodi di autenticazione abilitati per questa risorsa.",
- "resourceLauncherPrivateClientRequired": "Connettiti con un client sul tuo dispositivo per accedere a questa risorsa privatamente.",
- "resourceLauncherPrivateClientRequiredTitle": "Connessione Client Richiesta",
- "resourceLauncherDownloadClient": "Scarica client",
- "resourceLauncherFailedToLoadDetails": "Impossibile caricare i dettagli della risorsa. Potresti non avere più accesso a questa risorsa.",
- "resourceLauncherNoPortRestrictions": "Nessuna restrizione di porta",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Non Etichettato",
- "resourceLauncherNoSite": "Nessun Sito",
- "resourceLauncherNoResourcesInGroup": "Nessuna risorsa in questo gruppo",
- "resourceLauncherEmptyStateTitle": "Non ci sono risorse disponibili",
- "resourceLauncherEmptyStateDescription": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per richiedere l'accesso.",
- "resourceLauncherEmptyStateNoResultsTitle": "Nessuna risorsa trovata",
- "resourceLauncherEmptyStateNoResultsDescription": "Nessuna risorsa corrisponde alla tua ricerca o ai tuoi filtri attuali. Prova a modificarli per trovare ciò che stai cercando.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Nessuna risorsa corrisponde a \"{query}\". Prova a modificare la tua ricerca o a cancellare i filtri per vedere tutte le risorse.",
- "resourceLauncherSearchFirstTitle": "Cerca o Filtra per Navigare",
- "resourceLauncherSearchFirstDescription": "Hai accesso a molte risorse. Usa la ricerca o filtra per sito o etichetta per trovare ciò di cui hai bisogno.",
- "resourceLauncherSiteGroupingDisabled": "Raggruppamento per sito non disponibile su questa scala. Filtra per sito per raggruppare un set più piccolo.",
- "resourceLauncherLabelGroupingDisabled": "Raggruppamento per etichetta non disponibile su questa scala.",
- "resourceLauncherCompactModeHint": "Mostrando un elenco semplificato per una navigazione più veloce. Usa la ricerca o i filtri per restringere i risultati.",
- "resourceLauncherCompactGroupingHint": "Applica filtri di sito o etichetta per abilitare il raggruppamento.",
- "resourceLauncherCopiedToClipboard": "Copiato negli appunti",
- "resourceLauncherCopiedAccessDescription": "L'accesso alla risorsa è stato copiato nei tuoi appunti.",
- "resourceLauncherViewNamePlaceholder": "Nome Visualizzazione",
- "resourceLauncherViewNameLabel": "Nome Visualizzazione",
- "resourceLauncherViewSaved": "Visualizzazione salvata",
- "resourceLauncherViewSavedDescription": "La tua visualizzazione del lanscia è stata salvata.",
- "resourceLauncherViewSaveFailed": "Impossibile salvare la visualizzazione",
- "resourceLauncherViewSaveFailedDescription": "Impossibile salvare la visualizzazione del lanscia. Per favore riprova.",
- "resourceLauncherViewDeleted": "Visualizzazione eliminata",
- "resourceLauncherViewDeletedDescription": "La visualizzazione del lanscia è stata eliminata.",
- "resourceLauncherViewDeleteFailed": "Impossibile eliminare la visualizzazione",
- "resourceLauncherViewDeleteFailedDescription": "Non è stato possibile eliminare la visualizzazione del lanscia. Per favore riprova.",
"memberPortalPrevious": "Precedente",
"memberPortalNext": "Successivo",
"httpSettings": "Impostazioni HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "È richiesta una chiave privata",
"vncTitle": "VNC",
- "vncSignInDescription": "Inserisci le tue credenziali VNC per connetterti",
- "vncUsernameOptional": "Nome utente (facoltativo)",
+ "vncSignInDescription": "Inserisci la tua password VNC per connetterti",
"vncPasswordOptional": "Password (opzionale)",
"vncNoResourceTarget": "Nessun bersaglio di risorsa disponibile",
"vncFailedToLoadNovnc": "Impossibile caricare noVNC",
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index d8c929ead..f3829dd16 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -66,15 +66,9 @@
"local": "로컬",
"edit": "편집",
"siteConfirmDelete": "사이트 삭제 확인",
- "siteConfirmDeleteAndResources": "사이트 및 리소스 삭제 확인",
"siteDelete": "사이트 삭제",
- "siteDeleteAndResources": "사이트 및 리소스 삭제",
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
- "siteMessageRemoveAndResources": "이 사이트와 연결된 모든 공용 및 개인 리소스는 다른 사이트에도 연결되어 있더라도 영구적으로 삭제됩니다.",
"siteQuestionRemove": "조직에서 사이트를 제거하시겠습니까?",
- "siteQuestionRemoveAndResources": "이 사이트와 모든 관련 리소스를 삭제하시겠습니까?",
- "sitesTableDeleteSite": "사이트 삭제",
- "sitesTableDeleteSiteAndResources": "사이트 및 리소스 삭제",
"siteManageSites": "사이트 관리",
"siteDescription": "프라이빗 네트워크로의 연결을 활성화하려면 사이트를 생성하고 관리하세요.",
"sitesBannerTitle": "모든 네트워크 연결",
@@ -123,16 +117,6 @@
"siteUpdated": "사이트가 업데이트되었습니다",
"siteUpdatedDescription": "사이트가 업데이트되었습니다.",
"siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.",
- "siteRestartTitle": "사이트 다시 시작",
- "siteRestartDescription": "이 사이트의 WireGuard 터널을 다시 시작합니다. 일시적으로 연결이 중단될 수 있습니다.",
- "siteRestartBody": "사이트 터널이 제대로 작동하지 않을 경우, 호스트를 재시작하지 않고 다시 연결을 강제하려면 이 옵션을 사용하세요.",
- "siteRestartButton": "사이트 다시 시작",
- "siteRestartDialogMessage": "{name}의 WireGuard 터널을 재시작하시겠습니까? 이 작업으로 인해 사이트의 연결이 일시적으로 중단될 수 있습니다.",
- "siteRestartWarning": "터널을 재시작하는 동안 사이트가 일시적으로 연결이 끊깁니다.",
- "siteRestarted": "사이트가 재시작되었습니다",
- "siteRestartedDescription": "WireGuard 터널이 재시작되었습니다.",
- "siteErrorRestart": "사이트 재시작 실패",
- "siteErrorRestartDescription": "사이트를 재시작하는 중 오류가 발생했습니다.",
"siteSettingDescription": "사이트에서 설정을 구성하세요.",
"siteResourcesTab": "리소스",
"siteResourcesNoneOnSite": "이 사이트에는 아직 공용 또는 개인 리소스가 없습니다.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "포트 번호를 사용하여 원격 노드에 연결해야 합니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.",
"resourceCreate": "리소스 생성",
"resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.",
- "resourcePublicCreate": "공용 리소스 생성",
- "resourcePublicCreateDescription": "웹 브라우저를 통해 접근할 수 있는 새로운 공용 리소스를 생성하려면 아래 단계를 따르십시오",
"resourceCreateGeneralDescription": "이름 및 유형을 포함한 기본 리소스 설정 구성",
"resourceSeeAll": "모든 리소스 보기",
"resourceCreateGeneral": "일반",
@@ -615,8 +597,7 @@
"idpNameInternal": "내부",
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
"inviteValidityDuration": "지속 시간을 선택하십시오.",
- "accessRoleSelectPlease": "사용자는 적어도 하나의 역할에 속해야 합니다.",
- "accessRoleRequired": "역할 필요",
+ "accessRoleSelectPlease": "역할을 선택하세요",
"removeOwnAdminRoleConfirmTitle": "관리자 권한을 제거하시겠습니까?",
"removeOwnAdminRoleConfirmDescription": "저장 후 이 조직에 대한 관리자 권한이 없어집니다. 필요한 경우 다른 관리자가 접근 권한을 복구할 수 있습니다.",
"removeOwnAdminRoleConfirmButton": "내 관리자 권한 제거",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "기본 헤더 인증",
"policyAuthHeaderAuthDescription": "각 요청에서 맞춤 HTTP 헤더 이름 및 값을 검증",
"policyAuthHeaderAuthSummary": "헤더 구성됨",
- "policyAuthHeaderName": "사용자 이름",
- "policyAuthHeaderValue": "비밀번호",
+ "policyAuthHeaderName": "헤더 이름",
+ "policyAuthHeaderValue": "예상 값",
"policyAuthSetPasscode": "패스코드 설정",
"policyAuthSetPincode": "PIN 코드 설정",
"policyAuthSetEmailWhitelist": "이메일 화이트리스트 설정",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "청사진 적용",
"actionListBlueprints": "청사진 목록",
"actionGetBlueprint": "청사진 가져오기",
- "actionCreateOrgWideLauncherView": "조직 전체 런처 보기 생성",
"setupToken": "설정 토큰",
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "리소스 핀코드 설정",
"actionSetResourceEmailWhitelist": "리소스 이메일 화이트리스트 설정",
"actionGetResourceEmailWhitelist": "리소스 이메일 화이트리스트 가져오기",
- "actionGetResourcePolicy": "리소스 정책 가져오기",
- "actionUpdateResourcePolicy": "리소스 정책 업데이트",
- "actionSetResourcePolicyUsers": "리소스 정책 사용자 설정",
- "actionSetResourcePolicyRoles": "리소스 정책 역할 설정",
- "actionSetResourcePolicyPassword": "리소스 정책 비밀번호 설정",
- "actionSetResourcePolicyPincode": "리소스 정책 핀코드 설정",
- "actionSetResourcePolicyHeaderAuth": "리소스 정책 헤더 인증 설정",
- "actionSetResourcePolicyWhitelist": "리소스 정책 이메일 화이트리스트 설정",
- "actionSetResourcePolicyRules": "리소스 정책 규칙 설정",
"actionCreateTarget": "대상 만들기",
"actionDeleteTarget": "대상 삭제",
"actionGetTarget": "대상 가져오기",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "액세스 토큰 생성",
"actionDeleteAccessToken": "액세스 토큰 삭제",
"actionListAccessTokens": "액세스 토큰 목록",
- "actionCreateResourceSessionToken": "리소스 세션 토큰 생성",
"actionCreateResourceRule": "리소스 규칙 생성",
"actionDeleteResourceRule": "리소스 규칙 삭제",
"actionListResourceRules": "리소스 규칙 목록",
@@ -1520,30 +1490,6 @@
"navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
"navbarDocsLink": "문서",
- "commandPaletteTitle": "명령 팔레트",
- "commandPaletteDescription": "페이지, 조직, 리소스 및 작업을 검색합니다",
- "commandPaletteSearchPlaceholder": "페이지, 리소스, 작업 검색...",
- "commandPaletteNoResults": "결과를 찾을 수 없습니다.",
- "commandPaletteSearching": "검색 중...",
- "commandPaletteNavigation": "탐색",
- "commandPaletteOrganizations": "조직",
- "commandPaletteSites": "사이트",
- "commandPaletteResources": "리소스",
- "commandPaletteUsers": "사용자",
- "commandPaletteClients": "머신 클라이언트",
- "commandPaletteActions": "작업",
- "commandPaletteCreateSite": "사이트 생성",
- "commandPaletteCreateProxyResource": "공용 리소스 생성",
- "commandPaletteCreatePrivateResource": "개인 리소스 생성",
- "commandPaletteCreateUser": "사용자 생성",
- "commandPaletteCreateApiKey": "API 키 생성",
- "commandPaletteCreateMachineClient": "머신 클라이언트 생성",
- "commandPaletteCreateAlertRule": "경고 규칙 생성",
- "commandPaletteCreateIdentityProvider": "신원 공급자 생성",
- "commandPaletteToggleTheme": "테마 전환",
- "commandPaletteChooseOrganization": "조직 선택",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
"otpSetupCheckCode": "6자리 코드를 입력하세요",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "관리",
"sidebarBillingAndLicenses": "결제 및 라이선스",
"sidebarLogsAnalytics": "분석",
- "commandSites": "사이트",
- "commandActionModeInfo": "동작 모드 열기 위해 \">\" 입력",
- "commandResources": "리소스",
- "commandProxyResources": "공용 리소스",
- "commandClientResources": "개인 리소스",
- "commandClients": "클라이언트",
- "commandUserDevices": "사용자 기기",
- "commandMachineClients": "머신 클라이언트",
- "commandDomains": "도메인",
- "commandRemoteExitNodes": "원격 노드",
- "commandTeam": "팀",
- "commandUsers": "사용자",
- "commandRoles": "역할",
- "commandInvitations": "초대",
- "commandPolicies": "공유 정책",
- "commandResourcePolicies": "공용 리소스 정책",
- "commandIdentityProviders": "신원 공급자",
- "commandApprovals": "승인 요청",
- "commandShareableLinks": "공유 가능한 링크",
- "commandOrganization": "조직",
- "commandLogsAndAnalytics": "로그 및 분석",
- "commandLogsAnalytics": "분석",
- "commandLogsRequest": "HTTP 요청 로그",
- "commandLogsAccess": "인증 로그",
- "commandLogsAction": "관리자 작업 로그",
- "commandLogsConnection": "네트워크 로그",
- "commandLogsStreaming": "이벤트 스트리밍",
- "commandManagement": "관리",
- "commandAlerting": "경보",
- "commandProvisioning": "프로비저닝",
- "commandBluePrints": "블루밍",
- "commandApiKeys": "API 키",
- "commandBillingAndLicenses": "청구 및 라이선스",
- "commandBilling": "청구",
- "commandEnterpriseLicenses": "라이선스",
- "commandSettings": "설정",
- "commandLauncher": "런처",
- "commandResourceLauncher": "리소스 런처",
- "commandSearchResults": "검색 결과",
"alertingTitle": "알림",
"alertingDescription": "알림에 대한 소스, 트리거 및 작업 정의",
"alertingRules": "알림 규칙",
@@ -1991,9 +1898,6 @@
"billingDomains": "도메인",
"billingOrganizations": "조직",
"billingRemoteExitNodes": "원격 노드",
- "billingPublicResources": "공용 리소스",
- "billingPrivateResources": "개인 리소스",
- "billingMachineClients": "머신 클라이언트",
"billingNoLimitConfigured": "구성된 한도가 없습니다.",
"billingEstimatedPeriod": "예상 청구 기간",
"billingIncludedUsage": "포함 사용량",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "사용할 수 있는 사용자 수",
"billingDomainInfo": "사용할 수 있는 도메인 수",
"billingRemoteExitNodesInfo": "사용할 수 있는 원격 노드 수",
- "billingPublicResourcesInfo": "사용할 수 있는 공용 리소스 수",
- "billingPrivateResourcesInfo": "사용할 수 있는 개인 리소스 수",
- "billingMachineClientsInfo": "사용할 수 있는 머신 클라이언트 수",
"billingLicenseKeys": "라이센스 키",
"billingLicenseKeysDescription": "라이센스 키 구독을 관리하세요",
"billingLicenseSubscription": "라이센스 구독",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "서브넷",
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
"selectSites": "사이트 선택",
- "selectLabels": "레이블 선택",
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
"clientInstallOlm": "Olm 설치",
"clientInstallOlmDescription": "시스템에서 Olm을 실행하기",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "닫기",
"createInternalResourceDialogCreateClientResource": "사이트 리소스 생성",
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다",
- "privateResourceGeneralDescription": "이름, 식별자, 기타 일반 리소스 설정을 구성합니다.",
- "privateResourceCreatePageSeeAll": "모든 개인 리소스 보기",
- "privateResourceAllowIcmpPing": "ICMP 핑 허용",
- "privateResourceNetworkAccess": "네트워크 접근",
- "privateResourceNetworkAccessDescription": "이 리소스에 대한 TCP/UDP 포트 접근과 ICMP 핑이 허용되는지 여부를 제어합니다.",
- "hostSettings": "호스트 설정",
- "cidrSettings": "CIDR 설정",
"createInternalResourceDialogResourceProperties": "리소스 속성",
"createInternalResourceDialogName": "이름",
"createInternalResourceDialogSite": "사이트",
"selectSite": "사이트 선택...",
"multiSitesSelectorSitesCount": "{count, plural, other {# 사이트}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# 레이블} other {# 레이블}}",
"noSitesFound": "사이트를 찾을 수 없습니다.",
"createInternalResourceDialogProtocol": "프로토콜",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
"createInternalResourceDialogAlias": "별칭",
"createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
- "internalResourceAliasLocalWarning": ".local로 끝나는 별칭은 일부 네트워크에서 mDNS로 인해 해결 문제가 발생할 수 있습니다.",
"internalResourceDownstreamSchemeRequired": "HTTP 리소스에 스킴이 필요합니다",
"internalResourceHttpPortRequired": "HTTP 리소스에 목적지 포트가 필요합니다",
"siteConfiguration": "설정",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "원격 노드",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "비밀",
- "remoteExitNodeNetworkingTitle": "네트워크 설정",
- "remoteExitNodeNetworkingDescription": "이 원격 출구 노드의 트래픽 라우팅 방법과 어떤 사이트가 이를 통해 연결하는지 구성합니다. 백홀 네트워킹 구성을 사용한 고급 기능입니다.",
- "remoteExitNodeNetworkingSave": "설정 저장",
- "remoteExitNodeNetworkingSaveSuccessTitle": "네트워크 설정이 저장되었습니다",
- "remoteExitNodeNetworkingSaveSuccessDescription": "네트워크 설정이 성공적으로 업데이트되었습니다.",
- "remoteExitNodeNetworkingSaveError": "네트워크 설정 저장 실패",
- "remoteExitNodeNetworkingSubnetsTitle": "원격 서브넷",
- "remoteExitNodeNetworkingSubnetsDescription": "이 원격 출구 노드가 트래픽을 라우팅할 CIDR 범위를 정의합니다. 유효한 CIDR을 입력하고 Enter를 눌러 추가하세요 (예: 10.0.0.0/8).",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "CIDR 범위 추가 (예: 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "서브넷 로드 실패",
- "remoteExitNodeNetworkingLabelsTitle": "우선순위 레이블",
- "remoteExitNodeNetworkingLabelsDescription": "이 레이블이 있는 사이트는 이 원격 출구 노드를 통해 연결됩니다.",
- "remoteExitNodeNetworkingLabelsButtonText": "레이블 선택...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "레이블 검색...",
- "remoteExitNodeNetworkingLabelsLoadError": "레이블 로드 실패",
"remoteExitNodeCreate": {
"title": "원격 노드 생성",
"description": "새로운 자체 호스팅 원격 중계 및 프록시 서버 노드를 생성하십시오.",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "이 조직에 사용 가능한 노드가 없습니다. 로컬 사이트를 사용하려면 먼저 노드를 생성하세요.",
"exitNode": "종단 노드",
"country": "국가",
- "countryIsNot": "국가가 아닙니다",
"rulesMatchCountry": "현재 소스 IP를 기반으로 합니다",
"region": "지역",
"selectRegion": "지역 선택",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC 공급자",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자",
"subnet": "서브넷",
- "utilitySubnet": "유틸리티 서브넷",
"subnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.",
"customDomain": "사용자 정의 도메인",
"authPage": "인증 페이지",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "이중 인증 설정이 필요합니다. 이 단계를 완료하려면 {dashboardUrl}/auth/login 통해 다시 로그인하십시오. 그런 다음 여기로 돌아오세요.",
"additionalSecurityRequired": "추가 보안 필요",
"organizationRequiresAdditionalSteps": "이 조직은 자원에 접근하기 전에 추가 보안 단계를 요구합니다.",
- "sessionExpired": "세션 만료",
- "sessionExpiredReauthRequired": "조직의 보안 정책에 따라 세션이 만료되었습니다. 계속하려면 재인증하십시오.",
- "reauthenticate": "재 인증",
"completeTheseSteps": "이 단계를 완료하십시오",
"enableTwoFactorAuthentication": "이중 인증 활성화",
"completeSecuritySteps": "보안 단계 완료",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
"loadingDNSRecords": "DNS 레코드를 로드하는 중...",
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
- "updateAvailableInfo": "업데이트된 버전이 있습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
"client": "클라이언트",
"proxyProtocol": "프록시 프로토콜 설정",
"proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "사용자 추가",
"editInternalResourceDialogAddClients": "클라이언트 추가",
"editInternalResourceDialogDestinationLabel": "대상지",
- "editInternalResourceDialogDestinationDescription": "클라이언트가 이 리소스에 어떻게 도달하는지 구성합니다.",
+ "editInternalResourceDialogDestinationDescription": "내부 리소스의 목적지 주소를 지정하세요. 선택한 모드에 따라 이 주소는 호스트명, IP 주소, 또는 CIDR 범위가 될 수 있습니다. 더욱 쉽게 식별할 수 있도록 내부 DNS 별칭을 설정할 수 있습니다.",
"internalResourceFormMultiSiteRoutingHelp": "다중 사이트를 선택하면 높은 가용성을 위해 회복력 있는 라우팅 및 페일오버가 가능해집니다.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "자세히 알아보기",
"editInternalResourceDialogPortRestrictionsDescription": "특정 TCP/UDP 포트에 대한 접근을 제한하거나 모든 포트를 허용/차단하십시오.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "이메일 화이트리스트",
"memberPortalResourceDisabled": "리소스 비활성화됨",
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
- "resourceLauncherTitle": "리소스 런처",
- "resourceSidebarLauncherTitle": "런처",
- "resourceLauncherDescription": "모든 사용 가능한 리소스를 보고 중앙 허브에서 실행",
- "resourceLauncherSearchPlaceholder": "리소스를 검색하세요...",
- "resourceLauncherDefaultView": "기본값",
- "resourceLauncherSaveView": "보기를 저장",
- "resourceLauncherSaveToCurrentView": "현재 보기로 저장",
- "resourceLauncherSaveDefaultPersonal": "내게 저장",
- "resourceLauncherResetView": "보기를 재설정",
- "resourceLauncherResetSystemDefault": "시스템 기본값으로 재설정",
- "resourceLauncherSystemDefaultRestored": "시스템 기본값이 복원되었습니다",
- "resourceLauncherSystemDefaultRestoredDescription": "기본 보기가 원래 설정으로 재설정되었습니다.",
- "resourceLauncherSaveAsNewView": "새 보기로 저장",
- "resourceLauncherSaveAsNewViewDescription": "현재 필터와 레이아웃을 저장할 이름을 입력하세요.",
- "resourceLauncherSaveForEveryone": "모두에게 저장",
- "resourceLauncherSaveForEveryoneDescription": "이 보기를 모든 조직 구성원과 공유합니다. 체크 해제하면 해당 뷰는 사용자에게만 표시됩니다.",
- "resourceLauncherMakePersonal": "개인적으로 만들기",
- "resourceLauncherFilter": "필터",
- "resourceLauncherFilterWithCount": "필터, {count} 적용됨",
- "resourceLauncherSort": "정렬",
- "resourceLauncherSortAscending": "오름차순 정렬",
- "resourceLauncherSortDescending": "내림차순 정렬",
- "resourceLauncherSettings": "설정",
- "resourceLauncherGroupBy": "그룹화 기준",
- "resourceLauncherGroupBySite": "사이트",
- "resourceLauncherGroupByLabel": "레이블",
- "resourceLauncherGroupByNone": "없음",
- "resourceLauncherLayout": "레이아웃",
- "resourceLauncherLayoutGrid": "그리드",
- "resourceLauncherLayoutList": "목록",
- "resourceLauncherShowLabels": "레이블 표시",
- "resourceLauncherShowSiteTags": "사이트 태그 표시",
- "resourceLauncherShowRecents": "최근 항목 표시",
- "resourceLauncherDeleteView": "보기 삭제",
- "resourceLauncherDeleteViewTitle": "뷰 삭제",
- "resourceLauncherDeleteViewQuestion": "이 런처 뷰를 삭제하시겠습니까?",
- "resourceLauncherDeleteViewConfirm": "뷰 삭제",
- "resourceLauncherViewAsAdmin": "관리자로 보기",
- "resourceLauncherResourceDetailsDescription": "이 리소스에 대한 연결 정보 및 상태입니다.",
- "resourceLauncherResourceDetails": "리소스 세부 정보",
- "resourceLauncherAuthMethodsDescription": "이 리소스에 활성화된 인증 방법입니다.",
- "resourceLauncherPrivateClientRequired": "이 리소스에 개인적으로 접근하려면 기기에서 클라이언트로 연결하십시오.",
- "resourceLauncherPrivateClientRequiredTitle": "클라이언트 연결 필요",
- "resourceLauncherDownloadClient": "클라이언트 다운로드",
- "resourceLauncherFailedToLoadDetails": "리소스 세부 정보를 로드할 수 없습니다. 더 이상 이 리소스에 접근할 수 없을 수 있습니다.",
- "resourceLauncherNoPortRestrictions": "포트 제한 없음",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "레이블 없음",
- "resourceLauncherNoSite": "사이트 없음",
- "resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
- "resourceLauncherEmptyStateTitle": "사용 가능한 리소스 없음",
- "resourceLauncherEmptyStateDescription": "아직 리소스에 대한 액세스 권한이 없습니다. 액세스를 요청하려면 관리자에게 문의하세요.",
- "resourceLauncherEmptyStateNoResultsTitle": "리소스를 찾을 수 없음",
- "resourceLauncherEmptyStateNoResultsDescription": "현재 검색이나 필터에 맞는 리소스가 없습니다. 필터를 조정하여 찾으려는 항목을 확인해보세요.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "\"{query}\"와 일치하는 리소스가 없습니다. 검색을 조정하거나 필터를 지워서 모든 리소스를 확인해보세요.",
- "resourceLauncherSearchFirstTitle": "검색 또는 필터로 탐색하기",
- "resourceLauncherSearchFirstDescription": "많은 리소스에 접근할 수 있습니다. 필요한 것을 찾기 위해 사이트 또는 라벨로 검색하십시오.",
- "resourceLauncherSiteGroupingDisabled": "이 규모에서는 사이트 그룹화가 불가능합니다. 더 작은 그룹을 위해 사이트로 필터링하십시오.",
- "resourceLauncherLabelGroupingDisabled": "이 규모에서는 라벨 그룹화가 불가능합니다.",
- "resourceLauncherCompactModeHint": "더 빠른 탐색을 위해 단순화된 목록을 표시하고 있습니다. 검색하거나 필터를 사용하여 결과를 좁히십시오.",
- "resourceLauncherCompactGroupingHint": "그룹핑을 활성화하려면 사이트 또는 라벨 필터를 적용하십시오.",
- "resourceLauncherCopiedToClipboard": "클립보드에 복사됨",
- "resourceLauncherCopiedAccessDescription": "리소스 액세스가 클립보드에 복사되었습니다.",
- "resourceLauncherViewNamePlaceholder": "보기 이름",
- "resourceLauncherViewNameLabel": "뷰 이름",
- "resourceLauncherViewSaved": "보기 저장됨",
- "resourceLauncherViewSavedDescription": "런처 뷰가 저장되었습니다.",
- "resourceLauncherViewSaveFailed": "뷰 저장 실패",
- "resourceLauncherViewSaveFailedDescription": "런처 뷰를 저장할 수 없습니다. 다시 시도하세요.",
- "resourceLauncherViewDeleted": "보기 삭제됨",
- "resourceLauncherViewDeletedDescription": "런처 뷰가 삭제되었습니다.",
- "resourceLauncherViewDeleteFailed": "뷰 삭제 실패",
- "resourceLauncherViewDeleteFailedDescription": "런처 뷰를 삭제할 수 없습니다. 다시 시도하세요.",
"memberPortalPrevious": "이전",
"memberPortalNext": "다음",
"httpSettings": "HTTP 설정",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "프라이빗 키가 필요합니다",
"vncTitle": "VNC",
- "vncSignInDescription": "연결하기 위해 VNC 자격 증명을 입력하세요",
- "vncUsernameOptional": "사용자 이름 (선택 사항)",
+ "vncSignInDescription": "연결하려면 VNC 비밀번호를 입력하세요",
"vncPasswordOptional": "비밀번호 (선택 사항)",
"vncNoResourceTarget": "사용할 수 있는 리소스 대상이 없습니다",
"vncFailedToLoadNovnc": "noVNC 로드를 실패했습니다",
diff --git a/messages/nb-NO.json b/messages/nb-NO.json
index 1e0d6a1a3..52ddf8e33 100644
--- a/messages/nb-NO.json
+++ b/messages/nb-NO.json
@@ -66,15 +66,9 @@
"local": "Lokal",
"edit": "Rediger",
"siteConfirmDelete": "Bekreft Sletting av Område",
- "siteConfirmDeleteAndResources": "Bekreft sletting av nettsted og ressurser",
"siteDelete": "Slett Område",
- "siteDeleteAndResources": "Slett nettsted og ressurser",
"siteMessageRemove": "Når nettstedet er fjernet, vil det ikke lenger være tilgjengelig. Alle målene for nettstedet vil også bli fjernet.",
- "siteMessageRemoveAndResources": "Dette vil permanent slette alle offentlige og private ressurser tilknyttet dette nettstedet, selv om en ressurs også er tilknyttet andre nettsteder.",
"siteQuestionRemove": "Er du sikker på at du vil fjerne nettstedet fra organisasjonen?",
- "siteQuestionRemoveAndResources": "Er du sikker på at du vil slette dette nettstedet og alle tilknyttede ressurser?",
- "sitesTableDeleteSite": "Slett nettsted",
- "sitesTableDeleteSiteAndResources": "Slett nettsted og ressurser",
"siteManageSites": "Administrer Områder",
"siteDescription": "Opprette og administrere nettsteder for å aktivere tilkobling til private nettverk",
"sitesBannerTitle": "Koble til alle nettverk",
@@ -123,16 +117,6 @@
"siteUpdated": "Område oppdatert",
"siteUpdatedDescription": "Området har blitt oppdatert.",
"siteGeneralDescription": "Konfigurer de generelle innstillingene for dette området",
- "siteRestartTitle": "Start område på nytt",
- "siteRestartDescription": "Start WireGuard-tunnelen for dette området på nytt. Dette vil midlertidig avbryte tilkoblingen.",
- "siteRestartBody": "Bruk dette hvis områdetunnelen ikke fungerer riktig og du vil tvinge en ny tilkobling uten å starte verten på nytt.",
- "siteRestartButton": "Start område på nytt",
- "siteRestartDialogMessage": "Er du sikker på at du vil starte WireGuard-tunnelen for {name} på nytt? Området vil midlertidig miste tilkoblingen.",
- "siteRestartWarning": "Området vil kobles kort fra mens tunnelen starter om.",
- "siteRestarted": "Område startet på nytt",
- "siteRestartedDescription": "WireGuard-tunnelen er startet på nytt.",
- "siteErrorRestart": "Kan ikke starte område på nytt",
- "siteErrorRestartDescription": "En feil oppstod ved omstart av området.",
"siteSettingDescription": "Konfigurere innstillingene på nettstedet",
"siteResourcesTab": "Ressurser",
"siteResourcesNoneOnSite": "Dette nettstedet har ingen offentlige eller private ressurser enda.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy forespørsler om rå TCP/UDP ved hjelp av et portnummer. Krever sider for å koble til en ekstern node.",
"resourceCreate": "Opprett ressurs",
"resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs",
- "resourcePublicCreate": "Opprett offentlig ressurs",
- "resourcePublicCreateDescription": "Følg trinnene nedenfor for å opprette en ny offentlig ressurs som er tilgjengelig via en nettleser",
"resourceCreateGeneralDescription": "Konfigurer de grunnleggende ressursinnstillingene inkludert navnet og typen",
"resourceSeeAll": "Se alle ressurser",
"resourceCreateGeneral": "Generelt",
@@ -615,8 +597,7 @@
"idpNameInternal": "Intern",
"emailInvalid": "Ugyldig e-postadresse",
"inviteValidityDuration": "Vennligst velg en varighet",
- "accessRoleSelectPlease": "En bruker må tilhøre minst en rolle.",
- "accessRoleRequired": "Rolle påkrevd",
+ "accessRoleSelectPlease": "Vennligst velg en rolle",
"removeOwnAdminRoleConfirmTitle": "Fjern din administratoradgang?",
"removeOwnAdminRoleConfirmDescription": "Du vil ikke lenger ha administratorrettigheter i denne organisasjonen etter lagring. En annen administrator kan gjenopprette tilgang hvis nødvendig.",
"removeOwnAdminRoleConfirmButton": "Fjern min administratoradgang",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Grunnleggende Header Autentisering",
"policyAuthHeaderAuthDescription": "Bekreft et tilpasset HTTP-headernavn og verdi ved hver forespørsel",
"policyAuthHeaderAuthSummary": "Header konfigurert",
- "policyAuthHeaderName": "Brukernavn",
- "policyAuthHeaderValue": "Passord",
+ "policyAuthHeaderName": "Headernavn",
+ "policyAuthHeaderValue": "Forventet verdi",
"policyAuthSetPasscode": "Angi passordkode",
"policyAuthSetPincode": "Sett PIN-kode",
"policyAuthSetEmailWhitelist": "Angi e-post hviteliste",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Bruk blåkopi",
"actionListBlueprints": "List opp blåkopier",
"actionGetBlueprint": "Hent blåkopi",
- "actionCreateOrgWideLauncherView": "Opprett lanseringsvisning for hele organisasjonen",
"setupToken": "Oppsetttoken",
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Angi ressurspinkode",
"actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs",
"actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs",
- "actionGetResourcePolicy": "Hent ressursregel",
- "actionUpdateResourcePolicy": "Oppdater ressursregel",
- "actionSetResourcePolicyUsers": "Sett ressursregel for brukere",
- "actionSetResourcePolicyRoles": "Sett ressursregel for roller",
- "actionSetResourcePolicyPassword": "Sett ressursregel for passord",
- "actionSetResourcePolicyPincode": "Sett ressursregel for pinkode",
- "actionSetResourcePolicyHeaderAuth": "Sett ressursregel for header-autentisering",
- "actionSetResourcePolicyWhitelist": "Sett ressursregel for hvitlisting av e-post",
- "actionSetResourcePolicyRules": "Sett ressursregel for regler",
"actionCreateTarget": "Opprett mål",
"actionDeleteTarget": "Slett mål",
"actionGetTarget": "Hent mål",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Generer tilgangstoken",
"actionDeleteAccessToken": "Slett tilgangstoken",
"actionListAccessTokens": "List opp tilgangstokener",
- "actionCreateResourceSessionToken": "Opprett ressurs økt-token",
"actionCreateResourceRule": "Opprett ressursregel",
"actionDeleteResourceRule": "Slett ressursregel",
"actionListResourceRules": "List opp ressursregler",
@@ -1520,30 +1490,6 @@
"navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
"navbarDocsLink": "Dokumentasjon",
- "commandPaletteTitle": "Kommando-palett",
- "commandPaletteDescription": "Søk etter sider, organisasjoner, ressurser, og handlinger",
- "commandPaletteSearchPlaceholder": "Søk sider, ressurser, handlinger...",
- "commandPaletteNoResults": "Ingen resultater funnet.",
- "commandPaletteSearching": "Søker...",
- "commandPaletteNavigation": "Navigasjon",
- "commandPaletteOrganizations": "Organisasjoner",
- "commandPaletteSites": "Områder",
- "commandPaletteResources": "Ressurser",
- "commandPaletteUsers": "Brukere",
- "commandPaletteClients": "Maskinklienter",
- "commandPaletteActions": "Handlinger",
- "commandPaletteCreateSite": "Opprett område",
- "commandPaletteCreateProxyResource": "Opprett offentlig ressurs",
- "commandPaletteCreatePrivateResource": "Opprett privat ressurs",
- "commandPaletteCreateUser": "Opprett bruker",
- "commandPaletteCreateApiKey": "Opprett API-nøkkel",
- "commandPaletteCreateMachineClient": "Opprett maskinklient",
- "commandPaletteCreateAlertRule": "Opprett varslingsregel",
- "commandPaletteCreateIdentityProvider": "Opprett identitetsleverandør",
- "commandPaletteToggleTheme": "Bytt tema",
- "commandPaletteChooseOrganization": "Velg organisasjon",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Kunne ikke aktivere 2FA",
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Administrasjon",
"sidebarBillingAndLicenses": "Fakturering & lisenser",
"sidebarLogsAnalytics": "Analyser",
- "commandSites": "Områder",
- "commandActionModeInfo": "Skriv \">\" for å åpne handlingsmodus",
- "commandResources": "Ressurser",
- "commandProxyResources": "Offentlige ressurser",
- "commandClientResources": "Private ressurser",
- "commandClients": "Klienter",
- "commandUserDevices": "Bruker enheter",
- "commandMachineClients": "Maskinklienter",
- "commandDomains": "Domener",
- "commandRemoteExitNodes": "Eksterne noder",
- "commandTeam": "Lag",
- "commandUsers": "Brukere",
- "commandRoles": "Roller",
- "commandInvitations": "Invitasjoner",
- "commandPolicies": "Delte regler",
- "commandResourcePolicies": "Offentlige ressurser regler",
- "commandIdentityProviders": "Identitetsleverandører",
- "commandApprovals": "Godkjenningsforespørsler",
- "commandShareableLinks": "Delbare lenker",
- "commandOrganization": "Organisasjon",
- "commandLogsAndAnalytics": "Logg & Analyse",
- "commandLogsAnalytics": "Analyse",
- "commandLogsRequest": "HTTP forespørselslogger",
- "commandLogsAccess": "Logger for autentisering",
- "commandLogsAction": "Handlingslogger",
- "commandLogsConnection": "Loggfiler for tilkobling",
- "commandLogsStreaming": "Strømming",
- "commandManagement": "Administrasjon",
- "commandAlerting": "Varsler",
- "commandProvisioning": "Forsyning",
- "commandBluePrints": "Blåkopier",
- "commandApiKeys": "API-nøkler",
- "commandBillingAndLicenses": "Fakturering & Lisenser",
- "commandBilling": "Fakturering",
- "commandEnterpriseLicenses": "Lisenser",
- "commandSettings": "Innstillinger",
- "commandLauncher": "Oppstarter",
- "commandResourceLauncher": "Ressurs Oppstarter",
- "commandSearchResults": "Søkeresultater",
"alertingTitle": "Varsling",
"alertingDescription": "Definer kilder, triggere og handlinger for varsler",
"alertingRules": "Varslingsregler",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domener",
"billingOrganizations": "Orger",
"billingRemoteExitNodes": "Eksterne Noder",
- "billingPublicResources": "Offentlige ressurser",
- "billingPrivateResources": "Private ressurser",
- "billingMachineClients": "Maskinklienter",
"billingNoLimitConfigured": "Ingen grense konfigurert",
"billingEstimatedPeriod": "Estimert faktureringsperiode",
"billingIncludedUsage": "Inkludert Bruk",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Hvor mange brukere du kan bruke",
"billingDomainInfo": "Hvor mange domener du kan bruke",
"billingRemoteExitNodesInfo": "Hvor mange fjernnoder du kan bruke",
- "billingPublicResourcesInfo": "Hvor mange offentlige ressurser du kan bruke",
- "billingPrivateResourcesInfo": "Hvor mange private ressurser du kan bruke",
- "billingMachineClientsInfo": "Hvor mange maskinklienter du kan bruke",
"billingLicenseKeys": "Lisensnøkler",
"billingLicenseKeysDescription": "Administrer dine lisensnøkkelabonnementer",
"billingLicenseSubscription": "Lisens abonnement",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Subnett",
"addressDescription": "Den interne adressen til klienten. Må falle innenfor organisasjonens undernett.",
"selectSites": "Velg områder",
- "selectLabels": "Velg etiketter",
"sitesDescription": "Klienten vil ha tilkobling til de valgte områdene",
"clientInstallOlm": "Installer Olm",
"clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Lukk",
"createInternalResourceDialogCreateClientResource": "Opprett privat ressurs",
"createInternalResourceDialogCreateClientResourceDescription": "Opprett en ny ressurs som bare vil være tilgjengelig for kunder som er koblet til organisasjonen",
- "privateResourceGeneralDescription": "Konfigurer navnet, identifikatoren og andre generelle ressursinnstillinger.",
- "privateResourceCreatePageSeeAll": "Se alle private ressurser",
- "privateResourceAllowIcmpPing": "Tillat ICMP Ping",
- "privateResourceNetworkAccess": "Nettverkstilgang",
- "privateResourceNetworkAccessDescription": "Kontroller TCP/UDP porttilgang og om ICMP ping tillates for denne ressursen.",
- "hostSettings": "Vertinnstillinger",
- "cidrSettings": "CIDR-innstillinger",
"createInternalResourceDialogResourceProperties": "Ressursegenskaper",
"createInternalResourceDialogName": "Navn",
"createInternalResourceDialogSite": "Område",
"selectSite": "Velg område...",
"multiSitesSelectorSitesCount": "{count, plural, one {# sted} other {# steder}}",
- "labelsSelectorLabelsCount": "{count, plural, one {en etikett} other {# etiketter}}",
"noSitesFound": "Ingen områder funnet.",
"createInternalResourceDialogProtocol": "Protokoll",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
- "internalResourceAliasLocalWarning": "Alias som slutter på .local kan forårsake oppløsningsproblemer på grunn av mDNS på enkelte nettverk.",
"internalResourceDownstreamSchemeRequired": "Skjema er påkrevd for HTTP-ressurser",
"internalResourceHttpPortRequired": "Destinasjonsport er nødvendig for HTTP-ressurser",
"siteConfiguration": "Konfigurasjon",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Eksterne Noder",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Sikkerhetsnøkkel",
- "remoteExitNodeNetworkingTitle": "Nettverksinnstillinger",
- "remoteExitNodeNetworkingDescription": "Konfigurer hvordan denne fjerne utgangsnoden ruter trafikk og hvilke områder som foretrekker å koble gjennom den. Avanserte funksjoner for å brukes med bakhalstilkoplingskonfigurasjoner.",
- "remoteExitNodeNetworkingSave": "Lagre innstillinger",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Nettverksinnstillinger lagret",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Nettverksinnstillingene er oppdatert.",
- "remoteExitNodeNetworkingSaveError": "Klarte ikke å lagre nettverksinnstillinger",
- "remoteExitNodeNetworkingSubnetsTitle": "Fjern-subnett",
- "remoteExitNodeNetworkingSubnetsDescription": "Definer CIDR-områdene som denne fjernutgangsnoden vil rute trafikk til. Skriv inn en gyldig CIDR (f.eks. 10.0.0.0/8) og trykk Enter for å legge til.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Legg til et CIDR-område (f.eks. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Feil ved lasting av subnett",
- "remoteExitNodeNetworkingLabelsTitle": "Preferanseetiketter",
- "remoteExitNodeNetworkingLabelsDescription": "Områder med disse etikettene vil bli tvunget til å koble gjennom denne fjerne utgangsnoden.",
- "remoteExitNodeNetworkingLabelsButtonText": "Velg etiketter...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Søk etiketter...",
- "remoteExitNodeNetworkingLabelsLoadError": "Feil ved lasting av etiketter",
"remoteExitNodeCreate": {
"title": "Opprett ekstern node",
"description": "Opprett en ny egendrift ekstern relé- og proxyservernode",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Ingen noder er tilgjengelige for denne organisasjonen. Opprett en node først for å bruke lokale nettsteder.",
"exitNode": "Utgangsnode",
"country": "Land",
- "countryIsNot": "Land ikke",
"rulesMatchCountry": "For tiden basert på kilde IP",
"region": "Fylke",
"selectRegion": "Velg region",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC leverandør",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnett",
- "utilitySubnet": "Nyttesubnett",
"subnetDescription": "Undernettverket for denne organisasjonens nettverkskonfigurasjon.",
"customDomain": "Egendefinert domene",
"authPage": "Autentiseringssider",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "To-faktor autentiseringsoppsett er nødvendig. Vennligst logg inn igjen via {dashboardUrl}/auth/login og fullfør dette steget. Kom deretter tilbake her.",
"additionalSecurityRequired": "Ekstra sikkerhet kreves",
"organizationRequiresAdditionalSteps": "Denne organisasjonen krever ytterligere sikkerhetstrinn før du får tilgang til ressurser.",
- "sessionExpired": "Økt utløpt",
- "sessionExpiredReauthRequired": "Økten din har utløpt i henhold til organisasjonens sikkerhetspolitikk. Vennligst autentiser på nytt for å fortsette.",
- "reauthenticate": "Autentiser på nytt",
"completeTheseSteps": "Fullfør disse trinnene",
"enableTwoFactorAuthentication": "Aktiver to-faktor autentisering",
"completeSecuritySteps": "Fullfør sikkerhetstrinnene",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "ID for organisasjon eller domene mangler",
"loadingDNSRecords": "Laster DNS-poster...",
"olmUpdateAvailableInfo": "En oppdatert versjon av Olm er tilgjengelig. Oppdater til den nyeste versjonen for å få den beste opplevelsen.",
- "updateAvailableInfo": "En oppdatert versjon er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
"client": "Klient",
"proxyProtocol": "Protokoll innstillinger for Protokoll",
"proxyProtocolDescription": "Konfigurer Proxy-protokoll for å bevare klientens IP-adresser til TCP-tjenester.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Legg til brukere",
"editInternalResourceDialogAddClients": "Legg til klienter",
"editInternalResourceDialogDestinationLabel": "Destinasjon",
- "editInternalResourceDialogDestinationDescription": "Konfigurer hvordan klienter får tilgang til denne ressursen.",
+ "editInternalResourceDialogDestinationDescription": "Spesifiser destinasjonsadressen for den interne ressursen. Dette kan være et vertsnavn, IP-adresse eller CIDR-sjikt avhengig av valgt modus. Valgfrie oppsett av intern DNS-alias for enklere identifikasjon.",
"internalResourceFormMultiSiteRoutingHelp": "Valg av flere nettsteder muliggjør motstandskraftig ruting og failover for høy tilgjengelighet.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Lær mer",
"editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "E-post-hviteliste",
"memberPortalResourceDisabled": "Ressurs deaktivert",
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
- "resourceLauncherTitle": "Ressurslansering",
- "resourceSidebarLauncherTitle": "Oppstarter",
- "resourceLauncherDescription": "Se alle tilgjengelige ressurser og start dem fra ett sentralt sted",
- "resourceLauncherSearchPlaceholder": "Søk i ressurser...",
- "resourceLauncherDefaultView": "Standard",
- "resourceLauncherSaveView": "Lagre visning",
- "resourceLauncherSaveToCurrentView": "Lagre til nåværende visning",
- "resourceLauncherSaveDefaultPersonal": "Lagre for meg",
- "resourceLauncherResetView": "Tilbakestill visning",
- "resourceLauncherResetSystemDefault": "Tilbakestill til systemets standard",
- "resourceLauncherSystemDefaultRestored": "Systemstandard gjenopprettet",
- "resourceLauncherSystemDefaultRestoredDescription": "Standardvisningen er tilbakestilt til de opprinnelige innstillingene.",
- "resourceLauncherSaveAsNewView": "Lagre som ny visning",
- "resourceLauncherSaveAsNewViewDescription": "Gi denne visningen et navn for å lagre dine nåværende filtre og oppsett.",
- "resourceLauncherSaveForEveryone": "Lagre for alle",
- "resourceLauncherSaveForEveryoneDescription": "Del denne visningen med alle organisasjonsmedlemmer. Når avkrysset, er visningen synlig bare for deg.",
- "resourceLauncherMakePersonal": "Gjør personlig",
- "resourceLauncherFilter": "Filter",
- "resourceLauncherFilterWithCount": "Filter, {count} anvendt",
- "resourceLauncherSort": "Sorter",
- "resourceLauncherSortAscending": "Sorter stigende",
- "resourceLauncherSortDescending": "Sorter synkende",
- "resourceLauncherSettings": "Innstillinger",
- "resourceLauncherGroupBy": "Grupper etter",
- "resourceLauncherGroupBySite": "Område",
- "resourceLauncherGroupByLabel": "Etikett",
- "resourceLauncherGroupByNone": "Ingen",
- "resourceLauncherLayout": "Oppsett",
- "resourceLauncherLayoutGrid": "Rutenett",
- "resourceLauncherLayoutList": "Liste",
- "resourceLauncherShowLabels": "Vis etiketter",
- "resourceLauncherShowSiteTags": "Vis områdestikkord",
- "resourceLauncherShowRecents": "Vis nylige",
- "resourceLauncherDeleteView": "Slett visning",
- "resourceLauncherDeleteViewTitle": "Slett visning",
- "resourceLauncherDeleteViewQuestion": "Er du sikker på at du vil slette denne oppstartervisningen?",
- "resourceLauncherDeleteViewConfirm": "Slett visning",
- "resourceLauncherViewAsAdmin": "Vis som administrator",
- "resourceLauncherResourceDetailsDescription": "Tilkoblingsinformasjon og status for denne ressursen.",
- "resourceLauncherResourceDetails": "Ressursdetaljer",
- "resourceLauncherAuthMethodsDescription": "Godkjenningsmetoder aktivert for denne ressursen.",
- "resourceLauncherPrivateClientRequired": "Koble til med en klient på enheten din for å få privat tilgang til denne ressursen.",
- "resourceLauncherPrivateClientRequiredTitle": "Klienttilkobling kreves",
- "resourceLauncherDownloadClient": "Last ned klient",
- "resourceLauncherFailedToLoadDetails": "Kunne ikke laste ressurstetatter. Du har kanskje ikke lenger tilgang til denne ressursen.",
- "resourceLauncherNoPortRestrictions": "Ingen portbegrensninger",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Umerket",
- "resourceLauncherNoSite": "Ingen område",
- "resourceLauncherNoResourcesInGroup": "Ingen ressurser i denne gruppen",
- "resourceLauncherEmptyStateTitle": "Ingen tilgjengelige ressurser",
- "resourceLauncherEmptyStateDescription": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å be om tilgang.",
- "resourceLauncherEmptyStateNoResultsTitle": "Ingen ressurser funnet",
- "resourceLauncherEmptyStateNoResultsDescription": "Ingen ressurser matcher dine nåværende søk eller filtre. Prøv å justere dem for å finne det du leter etter.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Ingen ressurser samsvarer med \"{query}\". Prøv å justere søket eller fjern filtrene for å se alle ressursene.",
- "resourceLauncherSearchFirstTitle": "Søk eller filtrer for å bla gjennom",
- "resourceLauncherSearchFirstDescription": "Du har tilgang til mange ressurser. Bruk søk eller filtrer eksplisitt etter sted eller etikett for å finne det du trenger.",
- "resourceLauncherSiteGroupingDisabled": "Stedssamling er ikke tilgjengelig i denne omfanget. Filtrer etter sted for å gruppere et mindre sett.",
- "resourceLauncherLabelGroupingDisabled": "Etikettsamling er ikke tilgjengelig i denne omfanget.",
- "resourceLauncherCompactModeHint": "Viser en forenklet liste for raskere bla. Bruk søk eller filtrer for å innskrenke resultatene.",
- "resourceLauncherCompactGroupingHint": "Bruk filter for sted eller etikett for å aktivere gruppering.",
- "resourceLauncherCopiedToClipboard": "Kopiert til utklippstavlen",
- "resourceLauncherCopiedAccessDescription": "Ressurstilgang er kopiert til utklippstavlen din.",
- "resourceLauncherViewNamePlaceholder": "Visningsnavn",
- "resourceLauncherViewNameLabel": "Visningsnavn",
- "resourceLauncherViewSaved": "Visning lagret",
- "resourceLauncherViewSavedDescription": "Lanseringsvisningen din er lagret.",
- "resourceLauncherViewSaveFailed": "Feilet å lagre visning",
- "resourceLauncherViewSaveFailedDescription": "Kunne ikke lagre lanseringsvisningen. Vennligst prøv igjen.",
- "resourceLauncherViewDeleted": "Visning slettet",
- "resourceLauncherViewDeletedDescription": "Lanseringsvisningen er slettet.",
- "resourceLauncherViewDeleteFailed": "Klarte ikke å slette visning",
- "resourceLauncherViewDeleteFailedDescription": "Kunne ikke slette lanseringsvisningen. Vennligst prøv igjen.",
"memberPortalPrevious": "Forrige",
"memberPortalNext": "Neste",
"httpSettings": "HTTP Innstillinger",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGYNN OPENSSH PRIVAT NØKKEL-----",
"sshPrivateKeyRequired": "Privat nøkkel er påkrevd",
"vncTitle": "VNC",
- "vncSignInDescription": "Skriv inn VNC-kredentialene dine for å koble til",
- "vncUsernameOptional": "Brukernavn (valgfritt)",
+ "vncSignInDescription": "Skriv inn VNC-passordet for å koble til",
"vncPasswordOptional": "Passord (valgfritt)",
"vncNoResourceTarget": "Ingen ressursemål tilgjengelig",
"vncFailedToLoadNovnc": "Klarte ikke å laste noVNC",
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index 506a4a5b3..642423454 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -66,15 +66,9 @@
"local": "Lokaal",
"edit": "Bewerken",
"siteConfirmDelete": "Verwijderen van site bevestigen",
- "siteConfirmDeleteAndResources": "Bevestig Verwijderen van Site en Bronnen",
"siteDelete": "Site verwijderen",
- "siteDeleteAndResources": "Site en Bronnen verwijderen",
"siteMessageRemove": "Eenmaal verwijderd zal de site niet langer toegankelijk zijn. Alle aan de site gekoppelde doelen zullen ook worden verwijderd.",
- "siteMessageRemoveAndResources": "Dit zal permanent alle publieke en private resources gekoppeld aan deze site verwijderen, zelfs als een resource ook aan andere sites is gekoppeld.",
"siteQuestionRemove": "Weet u zeker dat u de site wilt verwijderen uit de organisatie?",
- "siteQuestionRemoveAndResources": "Weet u zeker dat u deze site en alle gekoppelde resources wilt verwijderen?",
- "sitesTableDeleteSite": "Site verwijderen",
- "sitesTableDeleteSiteAndResources": "Site en Bronnen verwijderen",
"siteManageSites": "Sites beheren",
"siteDescription": "Maak en beheer sites om verbinding met privénetwerken in te schakelen",
"sitesBannerTitle": "Verbind elk netwerk",
@@ -123,16 +117,6 @@
"siteUpdated": "Site bijgewerkt",
"siteUpdatedDescription": "De site is bijgewerkt.",
"siteGeneralDescription": "Algemene instellingen voor deze site configureren",
- "siteRestartTitle": "Herstart Site",
- "siteRestartDescription": "Herstart de WireGuard-tunnel voor deze site. Dit zal de connectiviteit kort onderbreken.",
- "siteRestartBody": "Gebruik dit als de sitetunnel niet correct functioneert en je wilt een herverbinding forceren zonder de host opnieuw op te starten.",
- "siteRestartButton": "Herstart Site",
- "siteRestartDialogMessage": "Weet u zeker dat u de WireGuard-tunnel voor {name} wilt herstarten? De site zal tijdelijk geen connectiviteit hebben.",
- "siteRestartWarning": "De site zal kort worden losgekoppeld terwijl de tunnel opnieuw wordt gestart.",
- "siteRestarted": "Site herstart",
- "siteRestartedDescription": "De WireGuard-tunnel is opnieuw gestart.",
- "siteErrorRestart": "Site herstarten mislukt",
- "siteErrorRestartDescription": "Er is een fout opgetreden tijdens het herstarten van de site.",
"siteSettingDescription": "Configureer de instellingen van de site",
"siteResourcesTab": "Bronnen",
"siteResourcesNoneOnSite": "Deze site heeft nog geen openbare of privébronnen.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy verzoeken over rauwe TCP/UDP met behulp van een poortnummer. Vereist sites om verbinding te maken met een remote node.",
"resourceCreate": "Bron maken",
"resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken",
- "resourcePublicCreate": "Maak Openbare Bron",
- "resourcePublicCreateDescription": "Volg de onderstaande stappen om een nieuwe openbare bron aan te maken die toegankelijk is via een webbrowser",
"resourceCreateGeneralDescription": "Configureer de basisinstellingen van de bron, inclusief de naam en het type",
"resourceSeeAll": "Alle bronnen bekijken",
"resourceCreateGeneral": "Algemeen",
@@ -615,8 +597,7 @@
"idpNameInternal": "Intern",
"emailInvalid": "Ongeldig e-mailadres",
"inviteValidityDuration": "Selecteer een tijdsduur",
- "accessRoleSelectPlease": "Een gebruiker moet tot minstens één rol behoren.",
- "accessRoleRequired": "Rol vereist",
+ "accessRoleSelectPlease": "Selecteer een rol",
"removeOwnAdminRoleConfirmTitle": "Uw beheerderstoegang verwijderen?",
"removeOwnAdminRoleConfirmDescription": "U zult na het opslaan geen beheerdersrechten meer hebben in deze organisatie. Een andere beheerder kan de toegang indien nodig herstellen.",
"removeOwnAdminRoleConfirmButton": "Verwijder Mijn Beheerderstoegang",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Basic Header Authenticatie",
"policyAuthHeaderAuthDescription": "Valideer een aangepaste HTTP-headernaam en waarde bij elk verzoek",
"policyAuthHeaderAuthSummary": "Header geconfigureerd",
- "policyAuthHeaderName": "Gebruikersnaam",
- "policyAuthHeaderValue": "Wachtwoord",
+ "policyAuthHeaderName": "Header naam",
+ "policyAuthHeaderValue": "Verwachte waarde",
"policyAuthSetPasscode": "Stel toegangscode in",
"policyAuthSetPincode": "Stel Pincode in",
"policyAuthSetEmailWhitelist": "Stel E-mail Whitelist in",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Blauwdruk toepassen",
"actionListBlueprints": "Lijst blauwdrukken",
"actionGetBlueprint": "Krijg Blauwdruk",
- "actionCreateOrgWideLauncherView": "Maak Organisatiebrede Launcher Weergave",
"setupToken": "Instel Token",
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Stel Resource Pincode in",
"actionSetResourceEmailWhitelist": "Stel Resource e-mail whitelist in",
"actionGetResourceEmailWhitelist": "Verkrijg Resource E-mail Whitelist",
- "actionGetResourcePolicy": "Haal Bronbeleid op",
- "actionUpdateResourcePolicy": "Werk Bronbeleid bij",
- "actionSetResourcePolicyUsers": "Stel gebruikers van bronbeleid in",
- "actionSetResourcePolicyRoles": "Stel rollen van bronbeleid in",
- "actionSetResourcePolicyPassword": "Stel wachtwoord van bronbeleid in",
- "actionSetResourcePolicyPincode": "Stel pincode van bronbeleid in",
- "actionSetResourcePolicyHeaderAuth": "Stel hoofdveraantwoorden van bronbeleid in",
- "actionSetResourcePolicyWhitelist": "Stel e-mail whitelist van bronbeleid in",
- "actionSetResourcePolicyRules": "Stel regels van bronbeleid in",
"actionCreateTarget": "Doelwit aanmaken",
"actionDeleteTarget": "Verwijder doel",
"actionGetTarget": "Verkrijg Doel",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Genereer Toegangstoken",
"actionDeleteAccessToken": "Verwijder toegangstoken",
"actionListAccessTokens": "Lijst toegangstokens",
- "actionCreateResourceSessionToken": "Maak Resource Sessie Token",
"actionCreateResourceRule": "Bronregel aanmaken",
"actionDeleteResourceRule": "Verwijder Resource Regel",
"actionListResourceRules": "Bron regels weergeven",
@@ -1520,30 +1490,6 @@
"navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
"navbarDocsLink": "Documentatie",
- "commandPaletteTitle": "Commandopalet",
- "commandPaletteDescription": "Zoek naar pagina's, organisaties, bronnen en acties",
- "commandPaletteSearchPlaceholder": "Zoek pagina's, bronnen, acties...",
- "commandPaletteNoResults": "Geen resultaten gevonden.",
- "commandPaletteSearching": "Zoeken...",
- "commandPaletteNavigation": "Navigatie",
- "commandPaletteOrganizations": "Organisaties",
- "commandPaletteSites": "Sites",
- "commandPaletteResources": "Bronnen",
- "commandPaletteUsers": "Gebruikers",
- "commandPaletteClients": "Machine klanten",
- "commandPaletteActions": "Acties",
- "commandPaletteCreateSite": "Site Aanmaken",
- "commandPaletteCreateProxyResource": "Maak Openbare Bron",
- "commandPaletteCreatePrivateResource": "Maak Privébron",
- "commandPaletteCreateUser": "Gebruiker aanmaken",
- "commandPaletteCreateApiKey": "API-sleutel aanmaken",
- "commandPaletteCreateMachineClient": "Machineclient aanmaken",
- "commandPaletteCreateAlertRule": "Alarmregel aanmaken",
- "commandPaletteCreateIdentityProvider": "Identiteitsprovider aanmaken",
- "commandPaletteToggleTheme": "Thema wisselen",
- "commandPaletteChooseOrganization": "Kies Organisatie",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Kan 2FA niet inschakelen",
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
"otpSetupCheckCode": "Voer een 6-cijferige code in",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Beheer",
"sidebarBillingAndLicenses": "Facturatie & Licenties",
"sidebarLogsAnalytics": "Analyses",
- "commandSites": "Sites",
- "commandActionModeInfo": "Typ \">\" Om Actiemodus Te Openen",
- "commandResources": "Bronnen",
- "commandProxyResources": "Openbare Bronnen",
- "commandClientResources": "Privébronnen",
- "commandClients": "Clienten",
- "commandUserDevices": "Apparaatgebruikers",
- "commandMachineClients": "Machine klanten",
- "commandDomains": "Domeinen",
- "commandRemoteExitNodes": "Externe Knooppunten",
- "commandTeam": "Team",
- "commandUsers": "Gebruikers",
- "commandRoles": "Rollen",
- "commandInvitations": "Uitnodigingen",
- "commandPolicies": "Gedeeld Beleid",
- "commandResourcePolicies": "Openbare Bronnenbeleid",
- "commandIdentityProviders": "Identiteitsproviders",
- "commandApprovals": "Goedkeuring Aanvragen",
- "commandShareableLinks": "Deelbare Links",
- "commandOrganization": "Organisatie",
- "commandLogsAndAnalytics": "Logboeken & Analytics",
- "commandLogsAnalytics": "Analyse",
- "commandLogsRequest": "HTTP-aanvraaglogboeken",
- "commandLogsAccess": "Toegang tot logboek",
- "commandLogsAction": "Actie Logboeken",
- "commandLogsConnection": "Connectie Logs",
- "commandLogsStreaming": "Streamen",
- "commandManagement": "Beheren",
- "commandAlerting": "Waarschuwingen",
- "commandProvisioning": "Inrichting",
- "commandBluePrints": "Blauwdrukken",
- "commandApiKeys": "API sleutels",
- "commandBillingAndLicenses": "Facturering & Licenties",
- "commandBilling": "Facturering",
- "commandEnterpriseLicenses": "Licentie",
- "commandSettings": "Instellingen",
- "commandLauncher": "Startprogramma",
- "commandResourceLauncher": "Bron Startprogramma",
- "commandSearchResults": "Zoekresultaten",
"alertingTitle": "Waarschuwingen",
"alertingDescription": "Definieer bronnen, triggers en acties voor meldingen",
"alertingRules": "Waarschuwingsregels",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domeinen",
"billingOrganizations": "Ordenen",
"billingRemoteExitNodes": "Externe knooppunten",
- "billingPublicResources": "Openbare Bronnen",
- "billingPrivateResources": "Privébronnen",
- "billingMachineClients": "Machine klanten",
"billingNoLimitConfigured": "Geen limiet ingesteld",
"billingEstimatedPeriod": "Geschatte Facturatie Periode",
"billingIncludedUsage": "Opgenomen Gebruik",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Hoeveel gebruikers je kan gebruiken",
"billingDomainInfo": "Hoeveel domeinen je kunt gebruiken",
"billingRemoteExitNodesInfo": "Hoeveel externe nodes je kunt gebruiken",
- "billingPublicResourcesInfo": "Hoeveel openbare bronnen u kunt gebruiken",
- "billingPrivateResourcesInfo": "Hoeveel privébronnen u kunt gebruiken",
- "billingMachineClientsInfo": "Hoeveel machineclients u kunt gebruiken",
"billingLicenseKeys": "Licentie Sleutels",
"billingLicenseKeysDescription": "Beheer uw licentiesleutelabonnementen",
"billingLicenseSubscription": "Licentie abonnement",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Subnet",
"addressDescription": "Het interne adres van de klant. Moet binnen het subnetwerk van de organisatie vallen.",
"selectSites": "Selecteer sites",
- "selectLabels": "Selecteer labels",
"sitesDescription": "De client heeft connectiviteit met de geselecteerde sites",
"clientInstallOlm": "Installeer Olm",
"clientInstallOlmDescription": "Laat Olm draaien op uw systeem",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Sluiten",
"createInternalResourceDialogCreateClientResource": "Privé bron maken",
"createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron aan die alleen toegankelijk is voor klanten die verbonden zijn met de organisatie",
- "privateResourceGeneralDescription": "Configureer de naam, identificatie en andere algemene resource-instellingen.",
- "privateResourceCreatePageSeeAll": "Bekijk Alle Privébronnen",
- "privateResourceAllowIcmpPing": "Sta ICMP Ping toe",
- "privateResourceNetworkAccess": "Netwerktoegang",
- "privateResourceNetworkAccessDescription": "Beheer TCP/UDP poorttoegang en of ICMP-ping is toegestaan voor deze bron.",
- "hostSettings": "Hostinstellingen",
- "cidrSettings": "CIDR-instellingen",
"createInternalResourceDialogResourceProperties": "Bron-eigenschappen",
"createInternalResourceDialogName": "Naam",
"createInternalResourceDialogSite": "Site",
"selectSite": "Selecteer site...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# label} other {# labels}}",
"noSitesFound": "Geen sites gevonden.",
"createInternalResourceDialogProtocol": "Protocol",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Het CIDR-bereik van het document op het netwerk van de site.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.",
- "internalResourceAliasLocalWarning": "Aliassen die eindigen op .local kunnen resolutieproblemen veroorzaken vanwege mDNS op sommige netwerken.",
"internalResourceDownstreamSchemeRequired": "Schema is vereist voor HTTP-bronnen",
"internalResourceHttpPortRequired": "Bestemmingspoort is vereist voor HTTP-bronnen",
"siteConfiguration": "Configuratie",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Externe knooppunten",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Geheim",
- "remoteExitNodeNetworkingTitle": "Netwerkinstellingen",
- "remoteExitNodeNetworkingDescription": "Configureer hoe dit externe exit-knooppunt verkeer routeert en welke sites de voorkeur hebben om er doorheen te verbinden. Geavanceerde functies te gebruiken met backhaul-netwerkconfiguraties.",
- "remoteExitNodeNetworkingSave": "Instellingen opslaan",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Netwerkinstellingen opgeslagen",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Netwerkinstellingen zijn succesvol bijgewerkt.",
- "remoteExitNodeNetworkingSaveError": "Kon netwerkinstellingen niet opslaan",
- "remoteExitNodeNetworkingSubnetsTitle": "Externe Subnets",
- "remoteExitNodeNetworkingSubnetsDescription": "Definieer de CIDR-bereiken waarnaar dit externe exit-knooppunt verkeer zal routeren. Voer een geldige CIDR in (bijv. 10.0.0.0/8) en druk op Enter om toe te voegen.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Voeg een CIDR-bereik toe (bijv. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Kon subnets niet laden",
- "remoteExitNodeNetworkingLabelsTitle": "Voorkeurslabels",
- "remoteExitNodeNetworkingLabelsDescription": "Sites met deze labels worden verplicht om verbinding te maken via dit externe exit-knooppunt.",
- "remoteExitNodeNetworkingLabelsButtonText": "Selecteer labels...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Labels zoeken...",
- "remoteExitNodeNetworkingLabelsLoadError": "Kon labels niet laden",
"remoteExitNodeCreate": {
"title": "Externe knoop aanmaken",
"description": "Maak een nieuwe zelf-gehoste externe relais- en proxyservermodule",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Er zijn geen knooppunten beschikbaar voor deze organisatie. Maak eerst een knooppunt aan om lokale sites te gebruiken.",
"exitNode": "Exit Node",
"country": "Land",
- "countryIsNot": "Land Is Niet",
"rulesMatchCountry": "Momenteel gebaseerd op bron IP",
"region": "Regio",
"selectRegion": "Selecteer regio",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC provider",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Subnet",
- "utilitySubnet": "Hulpmiddel Subnet",
"subnetDescription": "Het subnet van de netwerkconfiguratie van deze organisatie.",
"customDomain": "Aangepast domein",
"authPage": "Authenticatiepagina's",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Tweestapsverificatie instellen is vereist. Log opnieuw in via {dashboardUrl}/auth/login voltooide deze stap. Kom daarna hier terug.",
"additionalSecurityRequired": "Extra beveiliging vereist",
"organizationRequiresAdditionalSteps": "Deze organisatie vereist extra beveiligingsstappen voordat u toegang hebt tot de bronnen.",
- "sessionExpired": "Sessie Verlopen",
- "sessionExpiredReauthRequired": "Uw sessie is verlopen volgens het beveiligingsbeleid van uw organisatie. Verifieer alstublieft opnieuw om door te gaan.",
- "reauthenticate": "Opnieuw verifiëren",
"completeTheseSteps": "Voltooi deze stappen",
"enableTwoFactorAuthentication": "Tweestapsverificatie inschakelen",
"completeSecuritySteps": "Voltooi beveiligingsstappen",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Organisatie of domein ID ontbreekt",
"loadingDNSRecords": "DNS-records laden...",
"olmUpdateAvailableInfo": "Er is een bijgewerkte versie van Olm beschikbaar. Update alstublieft naar de nieuwste versie voor de beste ervaring.",
- "updateAvailableInfo": "Er is een bijgewerkte versie beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"client": "Klant",
"proxyProtocol": "Proxy Protocol Instellingen",
"proxyProtocolDescription": "Proxyprotocol configureren om de IP-adressen van de client voor TCP-diensten te bewaren.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Gebruikers toevoegen",
"editInternalResourceDialogAddClients": "Clienten toevoegen",
"editInternalResourceDialogDestinationLabel": "Bestemming",
- "editInternalResourceDialogDestinationDescription": "Configureer hoe klanten toegang krijgen tot deze resource.",
+ "editInternalResourceDialogDestinationDescription": "Specificeer het bestemmingsadres voor de interne bron. Dit kan een hostnaam, IP-adres of CIDR-bereik zijn, afhankelijk van de geselecteerde modus. Stel optioneel een interne DNS-alias in voor eenvoudigere identificatie.",
"internalResourceFormMultiSiteRoutingHelp": "Selecteren van meerdere sites maakt veerkrachtige routing en failover mogelijk voor hoge beschikbaarheid.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Meer informatie",
"editInternalResourceDialogPortRestrictionsDescription": "Beperk toegang tot specifieke TCP/UDP-poorten of sta alle poorten toe/blokkeer.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "E-mail whitelist",
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
- "resourceLauncherTitle": "Bron Launcher",
- "resourceSidebarLauncherTitle": "Startprogramma",
- "resourceLauncherDescription": "Bekijk alle beschikbare bronnen en start ze vanuit één centraal punt",
- "resourceLauncherSearchPlaceholder": "Zoek naar bronnen...",
- "resourceLauncherDefaultView": "Standaard",
- "resourceLauncherSaveView": "Weergave Opslaan",
- "resourceLauncherSaveToCurrentView": "Opslaan naar huidige weergave",
- "resourceLauncherSaveDefaultPersonal": "Sla voor mij op",
- "resourceLauncherResetView": "Weergave Herstellen",
- "resourceLauncherResetSystemDefault": "Reset naar standaardinstelling van het systeem",
- "resourceLauncherSystemDefaultRestored": "Systeemstandaard hersteld",
- "resourceLauncherSystemDefaultRestoredDescription": "De standaardweergave is teruggezet naar de oorspronkelijke instellingen.",
- "resourceLauncherSaveAsNewView": "Opslaan als Nieuwe Weergave",
- "resourceLauncherSaveAsNewViewDescription": "Geef deze weergave een naam om je huidige filters en indeling op te slaan.",
- "resourceLauncherSaveForEveryone": "Opslaan voor Iedereen",
- "resourceLauncherSaveForEveryoneDescription": "Deel deze weergave met alle organisatieleden. Als dit niet is aangevinkt, is de weergave alleen zichtbaar voor jou.",
- "resourceLauncherMakePersonal": "Persoonlijk Maken",
- "resourceLauncherFilter": "Filter",
- "resourceLauncherFilterWithCount": "Filter, {count} toegepast",
- "resourceLauncherSort": "Sorteren",
- "resourceLauncherSortAscending": "Oplopend sorteren",
- "resourceLauncherSortDescending": "Aflopend sorteren",
- "resourceLauncherSettings": "Instellingen",
- "resourceLauncherGroupBy": "Groep Op",
- "resourceLauncherGroupBySite": "Site",
- "resourceLauncherGroupByLabel": "Label",
- "resourceLauncherGroupByNone": "Geen",
- "resourceLauncherLayout": "Lay-out",
- "resourceLauncherLayoutGrid": "Raster",
- "resourceLauncherLayoutList": "Lijst",
- "resourceLauncherShowLabels": "Labels Weergeven",
- "resourceLauncherShowSiteTags": "Site Tags Weergeven",
- "resourceLauncherShowRecents": "Recente Weergeven",
- "resourceLauncherDeleteView": "Weergave Verwijderen",
- "resourceLauncherDeleteViewTitle": "Weergave verwijderen",
- "resourceLauncherDeleteViewQuestion": "Weet u zeker dat u deze launcher-weergave wilt verwijderen?",
- "resourceLauncherDeleteViewConfirm": "Weergave verwijderen",
- "resourceLauncherViewAsAdmin": "Bekijk als Admin",
- "resourceLauncherResourceDetailsDescription": "Verbindingsinformatie en -status voor deze bron.",
- "resourceLauncherResourceDetails": "Brongegevens",
- "resourceLauncherAuthMethodsDescription": "Authenticatiemethoden ingeschakeld voor deze bron.",
- "resourceLauncherPrivateClientRequired": "Maak verbinding met een client op uw apparaat om deze bron privé te benaderen.",
- "resourceLauncherPrivateClientRequiredTitle": "Client Verbinding Vereist",
- "resourceLauncherDownloadClient": "Client downloaden",
- "resourceLauncherFailedToLoadDetails": "Kon brongegevens niet laden. U heeft mogelijk geen toegang meer tot deze bron.",
- "resourceLauncherNoPortRestrictions": "Geen poortbeperkingen",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Geen label",
- "resourceLauncherNoSite": "Geen Site",
- "resourceLauncherNoResourcesInGroup": "Geen bronnen in deze groep",
- "resourceLauncherEmptyStateTitle": "Geen Bronnen Beschikbaar",
- "resourceLauncherEmptyStateDescription": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang aan te vragen.",
- "resourceLauncherEmptyStateNoResultsTitle": "Geen Bronnen Gevonden",
- "resourceLauncherEmptyStateNoResultsDescription": "Geen bronnen komen overeen met je huidige zoekopdracht of filters. Probeer ze aan te passen om te vinden wat je zoekt.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Geen bronnen komen overeen met \"{query}\". Probeer je zoekopdracht aan te passen of filters te wissen om alle bronnen te zien.",
- "resourceLauncherSearchFirstTitle": "Zoek of Filter om te bladeren",
- "resourceLauncherSearchFirstDescription": "U heeft toegang tot veel bronnen. Gebruik zoeken of filteren op site of label om te vinden wat u nodig heeft.",
- "resourceLauncherSiteGroupingDisabled": "Sitegroeping is niet beschikbaar op deze schaal. Filter op site om een kleiner aantal te groeperen.",
- "resourceLauncherLabelGroupingDisabled": "Labelgroepering is niet beschikbaar op deze schaal.",
- "resourceLauncherCompactModeHint": "Een vereenvoudigde lijst weergegeven voor sneller bladeren. Gebruik zoeken of filters om resultaten te verfijnen.",
- "resourceLauncherCompactGroupingHint": "Toepas site- of labelfilters om groepering mogelijk te maken.",
- "resourceLauncherCopiedToClipboard": "Gekopieerd naar klembord",
- "resourceLauncherCopiedAccessDescription": "Toegang tot bron is gekopieerd naar je klembord.",
- "resourceLauncherViewNamePlaceholder": "Weergavenaam",
- "resourceLauncherViewNameLabel": "Weergavenaam",
- "resourceLauncherViewSaved": "Weergave opgeslagen",
- "resourceLauncherViewSavedDescription": "Je launcher-weergave is opgeslagen.",
- "resourceLauncherViewSaveFailed": "Kon weergave niet opslaan",
- "resourceLauncherViewSaveFailedDescription": "Kon de launcher-weergave niet opslaan. Probeer het opnieuw.",
- "resourceLauncherViewDeleted": "Weergave verwijderd",
- "resourceLauncherViewDeletedDescription": "De launcher-weergave is verwijderd.",
- "resourceLauncherViewDeleteFailed": "Kon weergave niet verwijderen",
- "resourceLauncherViewDeleteFailedDescription": "Kon de launcher-weergave niet verwijderen. Probeer het opnieuw.",
"memberPortalPrevious": "Vorige",
"memberPortalNext": "Volgende",
"httpSettings": "HTTP-instellingen",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Privésleutel is vereist",
"vncTitle": "VNC",
- "vncSignInDescription": "Voer uw VNC-referenties in om verbinding te maken",
- "vncUsernameOptional": "Gebruikersnaam (optioneel)",
+ "vncSignInDescription": "Voer uw VNC-wachtwoord in om verbinding te maken",
"vncPasswordOptional": "Wachtwoord (optioneel)",
"vncNoResourceTarget": "Geen bron doelwit beschikbaar",
"vncFailedToLoadNovnc": "Laden van noVNC mislukt",
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index 718579932..3de2fe165 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -66,15 +66,9 @@
"local": "Lokalny",
"edit": "Edytuj",
"siteConfirmDelete": "Potwierdź usunięcie witryny",
- "siteConfirmDeleteAndResources": "Potwierdź usunięcie witryny i zasobów",
"siteDelete": "Usuń witrynę",
- "siteDeleteAndResources": "Usuń witrynę i zasoby",
"siteMessageRemove": "Po usunięciu witryna nie będzie już dostępna. Wszystkie cele związane z witryną zostaną również usunięte.",
- "siteMessageRemoveAndResources": "To spowoduje trwałe usunięcie wszystkich zasobów publicznych i prywatnych powiązanych z tą witryną, nawet jeśli zasób jest także powiązany z innymi witrynami.",
"siteQuestionRemove": "Czy na pewno chcesz usunąć witrynę z organizacji?",
- "siteQuestionRemoveAndResources": "Czy na pewno chcesz usunąć tę witrynę i wszystkie powiązane zasoby?",
- "sitesTableDeleteSite": "Usuń witrynę",
- "sitesTableDeleteSiteAndResources": "Usuń witrynę i zasoby",
"siteManageSites": "Zarządzaj stronami",
"siteDescription": "Tworzenie stron i zarządzanie nimi, aby włączyć połączenia z prywatnymi sieciami",
"sitesBannerTitle": "Połącz dowolną sieć",
@@ -123,16 +117,6 @@
"siteUpdated": "Strona zaktualizowana",
"siteUpdatedDescription": "Strona została zaktualizowana.",
"siteGeneralDescription": "Skonfiguruj ustawienia ogólne dla tej witryny",
- "siteRestartTitle": "Restartuj Stronę",
- "siteRestartDescription": "Uruchom ponownie tunel WireGuard dla tej strony. Spowoduje to tymczasowe przerwanie łączności.",
- "siteRestartBody": "Użyj tego, jeśli tunel strony nie działa prawidłowo i chcesz wymusić ponowne połączenie bez ponownego uruchamiania hosta.",
- "siteRestartButton": "Restartuj Stronę",
- "siteRestartDialogMessage": "Czy na pewno chcesz uruchomić ponownie tunel WireGuard dla {name}? Strona tymczasowo straci łączność.",
- "siteRestartWarning": "Strona tymczasowo rozłączy się podczas ponownego uruchamiania tunelu.",
- "siteRestarted": "Strona zrestartowana",
- "siteRestartedDescription": "Tunel WireGuard został ponownie uruchomiony.",
- "siteErrorRestart": "Nie udało się zrestartować strony",
- "siteErrorRestartDescription": "Wystąpił błąd podczas ponownego uruchamiania strony.",
"siteSettingDescription": "Skonfiguruj ustawienia na stronie",
"siteResourcesTab": "Zasoby",
"siteResourcesNoneOnSite": "Ta strona nie ma jeszcze żadnych zasobów publicznych ani prywatnych.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Żądania proxy nad surowym TCP/UDP przy użyciu numeru portu. Wymaga stron aby połączyć się ze zdalnym węzłem.",
"resourceCreate": "Utwórz zasób",
"resourceCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowy zasób",
- "resourcePublicCreate": "Utwórz zasób publiczny",
- "resourcePublicCreateDescription": "Postępuj zgodnie z poniższymi krokami, aby utworzyć nowy zasób publiczny dostępny przez przeglądarkę internetową",
"resourceCreateGeneralDescription": "Skonfiguruj podstawowe ustawienia zasobu, w tym nazwę i typ",
"resourceSeeAll": "Zobacz wszystkie zasoby",
"resourceCreateGeneral": "Ogólny",
@@ -615,8 +597,7 @@
"idpNameInternal": "Wewnętrzny",
"emailInvalid": "Nieprawidłowy adres e-mail",
"inviteValidityDuration": "Proszę wybrać okres ważności",
- "accessRoleSelectPlease": "Użytkownik musi należeć do co najmniej jednej roli.",
- "accessRoleRequired": "Wymagana rola",
+ "accessRoleSelectPlease": "Proszę wybrać rolę",
"removeOwnAdminRoleConfirmTitle": "Usunąć dostęp administratora?",
"removeOwnAdminRoleConfirmDescription": "Po zapisaniu nie będziesz już posiadał uprawnień administratora w tej organizacji. Inny administrator może przywrócić dostęp, jeśli to konieczne.",
"removeOwnAdminRoleConfirmButton": "Usuń mój dostęp administratora",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Podstawowe Uwierzytelnianie Nagłówka",
"policyAuthHeaderAuthDescription": "Walidacja niestandardowej nazwy i wartości nagłówka HTTP przy każdym żądaniu",
"policyAuthHeaderAuthSummary": "Skonfigurowany nagłówek",
- "policyAuthHeaderName": "Nazwa użytkownika",
- "policyAuthHeaderValue": "Hasło",
+ "policyAuthHeaderName": "Nazwa nagłówka",
+ "policyAuthHeaderValue": "Oczekiwana wartość",
"policyAuthSetPasscode": "Ustaw hasło dostępu",
"policyAuthSetPincode": "Ustaw kod PIN",
"policyAuthSetEmailWhitelist": "Ustaw białą listę e-mail",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Zastosuj schemat",
"actionListBlueprints": "Lista planów",
"actionGetBlueprint": "Pobierz plan",
- "actionCreateOrgWideLauncherView": "Utwórz Widok Uruchamiacza dla Całej Organizacji",
"setupToken": "Skonfiguruj token",
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
"setupTokenRequired": "Wymagany jest token konfiguracji",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Ustaw kod PIN zasobu",
"actionSetResourceEmailWhitelist": "Ustaw białą listę email zasobu",
"actionGetResourceEmailWhitelist": "Pobierz białą listę email zasobu",
- "actionGetResourcePolicy": "Pobierz politykę zasobów",
- "actionUpdateResourcePolicy": "Zaktualizuj politykę zasobów",
- "actionSetResourcePolicyUsers": "Ustaw użytkowników polityki zasobów",
- "actionSetResourcePolicyRoles": "Ustaw role polityki zasobów",
- "actionSetResourcePolicyPassword": "Ustaw hasło polityki zasobów",
- "actionSetResourcePolicyPincode": "Ustaw kod PIN polityki zasobów",
- "actionSetResourcePolicyHeaderAuth": "Ustaw nagłówkowe uwierzytelnianie polityki zasobów",
- "actionSetResourcePolicyWhitelist": "Ustaw białą listę email polityki zasobów",
- "actionSetResourcePolicyRules": "Ustaw zasady polityki zasobów",
"actionCreateTarget": "Utwórz cel",
"actionDeleteTarget": "Usuń cel",
"actionGetTarget": "Pobierz cel",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Wygeneruj token dostępu",
"actionDeleteAccessToken": "Usuń token dostępu",
"actionListAccessTokens": "Lista tokenów dostępu",
- "actionCreateResourceSessionToken": "Utwórz token sesji zasobu",
"actionCreateResourceRule": "Utwórz regułę zasobu",
"actionDeleteResourceRule": "Usuń regułę zasobu",
"actionListResourceRules": "Lista reguł zasobu",
@@ -1520,30 +1490,6 @@
"navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji",
"navbarDocsLink": "Dokumentacja",
- "commandPaletteTitle": "Paleta poleceń",
- "commandPaletteDescription": "Szukaj stron, organizacji, zasobów i akcji",
- "commandPaletteSearchPlaceholder": "Szukaj stron, zasobów, akcji...",
- "commandPaletteNoResults": "Nie znaleziono wyników.",
- "commandPaletteSearching": "Wyszukiwanie...",
- "commandPaletteNavigation": "Nawigacja",
- "commandPaletteOrganizations": "Organizacje",
- "commandPaletteSites": "Witryny",
- "commandPaletteResources": "Zasoby",
- "commandPaletteUsers": "Użytkownicy",
- "commandPaletteClients": "Klienci maszynowi",
- "commandPaletteActions": "Akcje",
- "commandPaletteCreateSite": "Utwórz witrynę",
- "commandPaletteCreateProxyResource": "Utwórz zasób publiczny",
- "commandPaletteCreatePrivateResource": "Utwórz zasób prywatny",
- "commandPaletteCreateUser": "Utwórz użytkownika",
- "commandPaletteCreateApiKey": "Utwórz klucz API",
- "commandPaletteCreateMachineClient": "Utwórz klienta maszynowego",
- "commandPaletteCreateAlertRule": "Utwórz regułę powiadomień",
- "commandPaletteCreateIdentityProvider": "Utwórz dostawcę tożsamości",
- "commandPaletteToggleTheme": "Przełącz motyw",
- "commandPaletteChooseOrganization": "Wybierz organizację",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Nie można włączyć 2FA",
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Zarządzanie",
"sidebarBillingAndLicenses": "Płatność i licencje",
"sidebarLogsAnalytics": "Analityka",
- "commandSites": "Witryny",
- "commandActionModeInfo": "Wpisz \">\" aby otworzyć tryb akcji",
- "commandResources": "Zasoby",
- "commandProxyResources": "Zasoby publiczne",
- "commandClientResources": "Zasoby prywatne",
- "commandClients": "Klienci",
- "commandUserDevices": "Urządzenia użytkownika",
- "commandMachineClients": "Klienci maszynowi",
- "commandDomains": "Domeny",
- "commandRemoteExitNodes": "Zdalne węzły",
- "commandTeam": "Zespół",
- "commandUsers": "Użytkownicy",
- "commandRoles": "Role",
- "commandInvitations": "Zaproszenia",
- "commandPolicies": "Polityki współdzielone",
- "commandResourcePolicies": "Polityki zasobów publicznych",
- "commandIdentityProviders": "Dostawcy tożsamości",
- "commandApprovals": "Żądania zatwierdzeń",
- "commandShareableLinks": "Linki do udostępnienia",
- "commandOrganization": "Organizacja",
- "commandLogsAndAnalytics": "Dzienniki i analizy",
- "commandLogsAnalytics": "Analizy",
- "commandLogsRequest": "Dzienniki żądań HTTP",
- "commandLogsAccess": "Dzienniki uwierzytelniania",
- "commandLogsAction": "Dzienniki działań administratora",
- "commandLogsConnection": "Dzienniki połączeń",
- "commandLogsStreaming": "Strumieniowanie zdarzeń",
- "commandManagement": "Zarządzanie",
- "commandAlerting": "Alarmy",
- "commandProvisioning": "Dostarczanie",
- "commandBluePrints": "Plany",
- "commandApiKeys": "Klucze API",
- "commandBillingAndLicenses": "Fakturowanie i licencje",
- "commandBilling": "Fakturowanie",
- "commandEnterpriseLicenses": "Licencje",
- "commandSettings": "Ustawienia",
- "commandLauncher": "Uruchamiacz",
- "commandResourceLauncher": "Uruchamiacz zasobów",
- "commandSearchResults": "Wyniki wyszukiwania",
"alertingTitle": "Alarmowanie",
"alertingDescription": "Zdefiniuj źródła, ustawienia, i działania dla powiadomień",
"alertingRules": "Reguły alarmowe",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domeny",
"billingOrganizations": "O masie całkowitej pojazdu przekraczającej 5 ton, ale nieprzekraczającej 5 ton",
"billingRemoteExitNodes": "Zdalne węzły",
- "billingPublicResources": "Zasoby publiczne",
- "billingPrivateResources": "Zasoby prywatne",
- "billingMachineClients": "Klienci maszynowi",
"billingNoLimitConfigured": "Nie skonfigurowano limitu",
"billingEstimatedPeriod": "Szacowany Okres Rozliczeniowy",
"billingIncludedUsage": "Zawarte użycie",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Ile użytkowników możesz użyć",
"billingDomainInfo": "Ile domen możesz użyć",
"billingRemoteExitNodesInfo": "Ile zdalnych węzłów możesz użyć",
- "billingPublicResourcesInfo": "Ile zasobów publicznych możesz wykorzystać",
- "billingPrivateResourcesInfo": "Ile zasobów prywatnych możesz wykorzystać",
- "billingMachineClientsInfo": "Ile klientów maszynowych możesz wykorzystać",
"billingLicenseKeys": "Klucze licencyjne",
"billingLicenseKeysDescription": "Zarządzaj subskrypcjami kluczy licencyjnych",
"billingLicenseSubscription": "Subskrypcja licencji",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Podsieć",
"addressDescription": "Adres wewnętrzny klienta. Musi mieścić się w podsieci organizacji.",
"selectSites": "Wybierz witryny",
- "selectLabels": "Wybierz etykiety",
"sitesDescription": "Klient będzie miał łączność z wybranymi witrynami",
"clientInstallOlm": "Zainstaluj Olm",
"clientInstallOlmDescription": "Uruchom Olm na swoim systemie",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Zamknij",
"createInternalResourceDialogCreateClientResource": "Utwórz zasób prywatny",
"createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny tylko dla klientów podłączonych do organizacji",
- "privateResourceGeneralDescription": "Skonfiguruj nazwę, identyfikator i inne ogólne ustawienia zasobów.",
- "privateResourceCreatePageSeeAll": "Zobacz wszystkie zasoby prywatne",
- "privateResourceAllowIcmpPing": "Zezwalaj na ping ICMP",
- "privateResourceNetworkAccess": "Dostęp do sieci",
- "privateResourceNetworkAccessDescription": "Kontroluj dostęp do portów TCP/UDP i czy ping ICMP jest dozwolony dla tego zasobu.",
- "hostSettings": "Ustawienia hosta",
- "cidrSettings": "Ustawienia CIDR",
"createInternalResourceDialogResourceProperties": "Właściwości zasobów",
"createInternalResourceDialogName": "Nazwa",
"createInternalResourceDialogSite": "Witryna",
"selectSite": "Wybierz stronę...",
"multiSitesSelectorSitesCount": "{count, plural, one {# witryna} few {# witryny} many {# witryn} other {# witryn}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# etykieta} few {# etykiety} many {# etykiet} other {# etykiet}}",
"noSitesFound": "Nie znaleziono stron.",
"createInternalResourceDialogProtocol": "Protokół",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
- "internalResourceAliasLocalWarning": "Alias kończący się na .local może powodować problemy z rozpoznawaniem z powodu mDNS w niektórych sieciach.",
"internalResourceDownstreamSchemeRequired": "Schemat jest wymagany dla zasobów HTTP",
"internalResourceHttpPortRequired": "Port docelowy jest wymagany dla zasobów HTTP",
"siteConfiguration": "Konfiguracja",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Zdalne węzły",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Sekret",
- "remoteExitNodeNetworkingTitle": "Ustawienia sieciowe",
- "remoteExitNodeNetworkingDescription": "Skonfiguruj, jak ten zdalny węzeł wyjściowy przekierowuje ruch i które strony preferują połączenie przez niego. Zaawansowane funkcje do użycia z konfiguracją sieci backhaul.",
- "remoteExitNodeNetworkingSave": "Zapisz ustawienia",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Ustawienia sieciowe zapisane",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Ustawienia sieciowe zostały pomyślnie zaktualizowane.",
- "remoteExitNodeNetworkingSaveError": "Nie udało się zapisać ustawień sieciowych",
- "remoteExitNodeNetworkingSubnetsTitle": "Zdalne Podsieci",
- "remoteExitNodeNetworkingSubnetsDescription": "Zdefiniuj zakresy CIDR, które ten zdalny węzeł wyjściowy przekieruje ruch do. Wpisz prawidłowy CIDR (np. 10.0.0.0/8) i naciśnij Enter, aby dodać.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Dodaj zakres CIDR (np. 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Nie udało się załadować podsieci",
- "remoteExitNodeNetworkingLabelsTitle": "Etykiety preferencji",
- "remoteExitNodeNetworkingLabelsDescription": "Strony z tymi etykietami będą zmuszone do połączenia się przez ten zdalny węzeł wyjściowy.",
- "remoteExitNodeNetworkingLabelsButtonText": "Wybierz etykiety...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Szukaj etykiet...",
- "remoteExitNodeNetworkingLabelsLoadError": "Nie udało się załadować etykiet",
"remoteExitNodeCreate": {
"title": "Utwórz zdalny węzeł",
"description": "Utwórz nowy, samodzielnie hostowany węzeł przekaźnika zdalnego i serwera proxy",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Węzły nie są dostępne dla tej organizacji. Utwórz węzeł, aby używać lokalnych witryn.",
"exitNode": "Węzeł Wyjściowy",
"country": "Kraj",
- "countryIsNot": "Kraj nie jest",
"rulesMatchCountry": "Obecnie bazuje na adresie IP źródła",
"region": "Region",
"selectRegion": "Wybierz region",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Dostawca Google OAuth2/OIDC",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Podsieć",
- "utilitySubnet": "Użyteczna podsieć",
"subnetDescription": "Podsieć dla konfiguracji sieci tej organizacji.",
"customDomain": "Niestandardowa domena",
"authPage": "Strony uwierzytelniania",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Konfiguracja uwierzytelniania dwuskładnikowego jest wymagana. Zaloguj się ponownie przez {dashboardUrl}/auth/login dokończ ten krok. Następnie wróć tutaj.",
"additionalSecurityRequired": "Wymagane dodatkowe zabezpieczenie",
"organizationRequiresAdditionalSteps": "Ta organizacja wymaga dodatkowych kroków bezpieczeństwa, zanim będziesz mógł uzyskać dostęp do zasobów.",
- "sessionExpired": "Sesja wygasła",
- "sessionExpiredReauthRequired": "Twoja sesja wygasła zgodnie z zasadami bezpieczeństwa Twojej organizacji. Proszę się ponownie zalogować, aby kontynuować.",
- "reauthenticate": "Zaloguj się ponownie",
"completeTheseSteps": "Wykonaj te kroki",
"enableTwoFactorAuthentication": "Włącz uwierzytelnianie dwuskładnikowe",
"completeSecuritySteps": "Zakończ kroki bezpieczeństwa",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Brakuje identyfikatora organizacji lub domeny",
"loadingDNSRecords": "Ładowanie rekordów DNS...",
"olmUpdateAvailableInfo": "Dostępna jest zaktualizowana wersja Olm. Zaktualizuj do najnowszej wersji, aby uzyskać najlepsze doświadczenia.",
- "updateAvailableInfo": "Dostępna jest zaktualizowana wersja. Zaktualizuj do najnowszej wersji, aby uzyskać najlepsze wrażenia z użytkowania.",
"client": "Klient",
"proxyProtocol": "Ustawienia protokołu proxy",
"proxyProtocolDescription": "Skonfiguruj protokół Proxy aby zachować adresy IP klienta dla usług TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Dodaj użytkowników",
"editInternalResourceDialogAddClients": "Dodaj klientów",
"editInternalResourceDialogDestinationLabel": "Miejsce docelowe",
- "editInternalResourceDialogDestinationDescription": "Skonfiguruj sposób, w jaki klienci docierają do tego zasobu.",
+ "editInternalResourceDialogDestinationDescription": "Określ adres docelowy dla wewnętrznego zasobu. Może to być nazwa hosta, adres IP lub zakres CIDR, w zależności od wybranego trybu. Opcjonalnie ustaw wewnętrzny alias DNS dla łatwiejszej identyfikacji.",
"internalResourceFormMultiSiteRoutingHelp": "Wybór wielu stron umożliwia odporne trasowanie i awarię dla wysokiej dostępności.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Dowiedz się więcej",
"editInternalResourceDialogPortRestrictionsDescription": "Ogranicz dostęp do konkretnych portów TCP/UDP lub zezwól/zablokuj wszystkie porty.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Biała lista e-mail",
"memberPortalResourceDisabled": "Zasób wyłączony",
"memberPortalShowingResources": "Wyświetlanie zasobów od {start} do {end} z {total}",
- "resourceLauncherTitle": "Uruchamiacz Zasobów",
- "resourceSidebarLauncherTitle": "Uruchamiacz",
- "resourceLauncherDescription": "Wyświetl wszystkie dostępne zasoby i uruchom je w jednym centralnym hubie",
- "resourceLauncherSearchPlaceholder": "Szukaj zasobów...",
- "resourceLauncherDefaultView": "Domyślny",
- "resourceLauncherSaveView": "Zapisz Widok",
- "resourceLauncherSaveToCurrentView": "Zapisz do bieżącego widoku",
- "resourceLauncherSaveDefaultPersonal": "Zapisz dla mnie",
- "resourceLauncherResetView": "Resetuj Widok",
- "resourceLauncherResetSystemDefault": "Przywróć do domyślnych ustawień systemowych",
- "resourceLauncherSystemDefaultRestored": "Przywrócono domyślne ustawienia systemowe",
- "resourceLauncherSystemDefaultRestoredDescription": "Domyślny widok został przywrócony do oryginalnych ustawień.",
- "resourceLauncherSaveAsNewView": "Zapisz jako Nowy Widok",
- "resourceLauncherSaveAsNewViewDescription": "Nadaj nazwę temu widokowi, aby zapisać swoje bieżące filtry i układ.",
- "resourceLauncherSaveForEveryone": "Zapisz dla wszystkich",
- "resourceLauncherSaveForEveryoneDescription": "Udostępnij ten widok wszystkim członkom organizacji. Gdy jest niezaznaczone, widok jest widoczny tylko dla Ciebie.",
- "resourceLauncherMakePersonal": "Zrób osobisty",
- "resourceLauncherFilter": "Filtr",
- "resourceLauncherFilterWithCount": "Filtr, zastosowano {count}",
- "resourceLauncherSort": "Sortuj",
- "resourceLauncherSortAscending": "Sortuj rosnąco",
- "resourceLauncherSortDescending": "Sortuj malejąco",
- "resourceLauncherSettings": "Ustawienia",
- "resourceLauncherGroupBy": "Grupuj według",
- "resourceLauncherGroupBySite": "Witryna",
- "resourceLauncherGroupByLabel": "Etykieta",
- "resourceLauncherGroupByNone": "Brak",
- "resourceLauncherLayout": "Układ",
- "resourceLauncherLayoutGrid": "Siatka",
- "resourceLauncherLayoutList": "Lista",
- "resourceLauncherShowLabels": "Pokaż etykiety",
- "resourceLauncherShowSiteTags": "Pokaż tagi stron",
- "resourceLauncherShowRecents": "Pokaż ostatnie",
- "resourceLauncherDeleteView": "Usuń Widok",
- "resourceLauncherDeleteViewTitle": "Usuń widok",
- "resourceLauncherDeleteViewQuestion": "Czy na pewno chcesz usunąć ten widok uruchamiania?",
- "resourceLauncherDeleteViewConfirm": "Usuń widok",
- "resourceLauncherViewAsAdmin": "Przeglądaj jako Administrator",
- "resourceLauncherResourceDetailsDescription": "Informacje i status połączenia dla tego zasobu.",
- "resourceLauncherResourceDetails": "Szczegóły zasobów",
- "resourceLauncherAuthMethodsDescription": "Metody uwierzytelniania włączone dla tego zasobu.",
- "resourceLauncherPrivateClientRequired": "Połącz się z klientem na swoim urządzeniu, aby uzyskać dostęp do tego zasobu prywatnie.",
- "resourceLauncherPrivateClientRequiredTitle": "Wymagane połączenie z klientem",
- "resourceLauncherDownloadClient": "Pobierz klienta",
- "resourceLauncherFailedToLoadDetails": "Nie można załadować szczegółów zasobu. Możliwe, że nie masz już dostępu do tego zasobu.",
- "resourceLauncherNoPortRestrictions": "Brak ograniczeń portów",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Bez etykiety",
- "resourceLauncherNoSite": "Brak strony",
- "resourceLauncherNoResourcesInGroup": "W tej grupie nie ma zasobów",
- "resourceLauncherEmptyStateTitle": "Brak dostępnych zasobów",
- "resourceLauncherEmptyStateDescription": "Jeszcze nie masz dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby poprosić o dostęp.",
- "resourceLauncherEmptyStateNoResultsTitle": "Nie znaleziono zasobów",
- "resourceLauncherEmptyStateNoResultsDescription": "Żadne zasoby nie spełniają twojego bieżącego wyszukiwania lub filtrów. Spróbuj je dostosować, aby znaleźć to, czego szukasz.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Żadne zasoby nie odpowiadają \"{query}\". Spróbuj dostosować swoje wyszukiwanie lub usunąć filtry, aby zobaczyć wszystkie zasoby.",
- "resourceLauncherSearchFirstTitle": "Szukaj lub filtruj, aby przeglądać",
- "resourceLauncherSearchFirstDescription": "Masz dostęp do wielu zasobów. Użyj wyszukiwania lub filtruj według witryny lub etykiety, aby znaleźć to, czego potrzebujesz.",
- "resourceLauncherSiteGroupingDisabled": "Grupowanie witryn jest niedostępne w tej skali. Filtruj według witryny, aby pogrupować mniejszy zestaw.",
- "resourceLauncherLabelGroupingDisabled": "Grupowanie etykiet jest niedostępne w tej skali.",
- "resourceLauncherCompactModeHint": "Wyświetlanie uproszczonej listy dla szybszego przeglądania. Użyj wyszukiwania lub filtrów, aby zawęzić wyniki.",
- "resourceLauncherCompactGroupingHint": "Zastosuj filtry witryn lub etykiet, aby włączyć grupowanie.",
- "resourceLauncherCopiedToClipboard": "Skopiowano do schowka",
- "resourceLauncherCopiedAccessDescription": "Dostęp do zasobu został skopiowany do schowka.",
- "resourceLauncherViewNamePlaceholder": "Nazwa widoku",
- "resourceLauncherViewNameLabel": "Nazwa Widoku",
- "resourceLauncherViewSaved": "Widok zapisany",
- "resourceLauncherViewSavedDescription": "Twój widok uruchamiacza został zapisany.",
- "resourceLauncherViewSaveFailed": "Nie udało się zapisać widoku",
- "resourceLauncherViewSaveFailedDescription": "Nie można zapisać widoku uruchamiacza. Proszę spróbować ponownie.",
- "resourceLauncherViewDeleted": "Widok usunięty",
- "resourceLauncherViewDeletedDescription": "Widok uruchamiacza został usunięty.",
- "resourceLauncherViewDeleteFailed": "Nie udało się usunąć widoku",
- "resourceLauncherViewDeleteFailedDescription": "Nie można usunąć widoku uruchamiacza. Proszę spróbować ponownie.",
"memberPortalPrevious": "Poprzedni",
"memberPortalNext": "Następny",
"httpSettings": "Ustawienia HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Wymagany jest klucz prywatny",
"vncTitle": "VNC",
- "vncSignInDescription": "Wprowadź swoje dane uwierzytelniające VNC aby się połączyć",
- "vncUsernameOptional": "Nazwa użytkownika (opcjonalnie)",
+ "vncSignInDescription": "Wprowadź hasło VNC, aby się połączyć",
"vncPasswordOptional": "Hasło (opcjonalne)",
"vncNoResourceTarget": "Brak dostępnego celu zasobu",
"vncFailedToLoadNovnc": "Błąd ładowania noVNC",
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index c0d65e0fb..78105f403 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -66,15 +66,9 @@
"local": "Localização",
"edit": "Alterar",
"siteConfirmDelete": "Confirmar que pretende apagar o site",
- "siteConfirmDeleteAndResources": "Confirmar Exclusão do Site e Recursos",
"siteDelete": "Excluir site",
- "siteDeleteAndResources": "Excluir Site e Recursos",
"siteMessageRemove": "Uma vez removido, o site não estará mais acessível. Todas as metas associadas ao site também serão removidas.",
- "siteMessageRemoveAndResources": "Isso excluirá permanentemente todos os recursos públicos e privados vinculados a este site, mesmo que um recurso também esteja associado a outros sites.",
"siteQuestionRemove": "Você tem certeza que deseja remover este site da organização?",
- "siteQuestionRemoveAndResources": "Tem certeza de que deseja excluir este site e todos os recursos associados?",
- "sitesTableDeleteSite": "Excluir Site",
- "sitesTableDeleteSiteAndResources": "Excluir Site e Recursos",
"siteManageSites": "Gerir sites",
"siteDescription": "Criar e gerenciar sites para ativar a conectividade a redes privadas",
"sitesBannerTitle": "Conectar a Qualquer Rede",
@@ -123,16 +117,6 @@
"siteUpdated": "Site atualizado",
"siteUpdatedDescription": "O site foi atualizado.",
"siteGeneralDescription": "Configurar as configurações gerais para este site",
- "siteRestartTitle": "Reiniciar site",
- "siteRestartDescription": "Reinicie o túnel WireGuard para este site. Isso interromperá brevemente a conectividade.",
- "siteRestartBody": "Use isso se o túnel do site não estiver funcionando corretamente e você quiser forçar uma reconexão sem reiniciar o host.",
- "siteRestartButton": "Reiniciar site",
- "siteRestartDialogMessage": "Tem certeza de que deseja reiniciar o túnel WireGuard para {name}? O site perderá brevemente a conectividade.",
- "siteRestartWarning": "O site será desconectado brevemente enquanto o túnel reinicia.",
- "siteRestarted": "Site reiniciado",
- "siteRestartedDescription": "O túnel WireGuard foi reiniciado.",
- "siteErrorRestart": "Falha ao reiniciar o site",
- "siteErrorRestartDescription": "Ocorreu um erro ao reiniciar o site.",
"siteSettingDescription": "Configurar as configurações no site",
"siteResourcesTab": "Recursos",
"siteResourcesNoneOnSite": "Este site ainda não possui recursos públicos ou privados.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy solicita por TCP/UDP bruto usando um número de porta. Requer que sites se conectem a um nó remoto.",
"resourceCreate": "Criar Recurso",
"resourceCreateDescription": "Siga os passos abaixo para criar um novo recurso",
- "resourcePublicCreate": "Criar Recurso Público",
- "resourcePublicCreateDescription": "Siga os passos abaixo para criar um novo recurso público acessível através de um navegador web",
"resourceCreateGeneralDescription": "Configure as configurações gerais do recurso, incluindo o nome e o tipo",
"resourceSeeAll": "Ver todos os recursos",
"resourceCreateGeneral": "Gerais",
@@ -615,8 +597,7 @@
"idpNameInternal": "Interno",
"emailInvalid": "Endereço de email inválido",
"inviteValidityDuration": "Por favor, selecione uma duração",
- "accessRoleSelectPlease": "Um usuário deve pertencer a pelo menos um papel.",
- "accessRoleRequired": "Papel necessário",
+ "accessRoleSelectPlease": "Por favor, selecione uma função",
"removeOwnAdminRoleConfirmTitle": "Remover seu acesso de administrador?",
"removeOwnAdminRoleConfirmDescription": "Você não terá mais permissões de administrador nesta organização após salvar. Outro administrador pode restaurar seu acesso, se necessário.",
"removeOwnAdminRoleConfirmButton": "Remover Meu Acesso de Administrador",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Autenticação de Cabeçalho Básico",
"policyAuthHeaderAuthDescription": "Valide um nome e valor de cabeçalho HTTP personalizado em cada solicitação",
"policyAuthHeaderAuthSummary": "Cabeçalho configurado",
- "policyAuthHeaderName": "Nome de usuário",
- "policyAuthHeaderValue": "Palavra-passe",
+ "policyAuthHeaderName": "Nome do Cabeçalho",
+ "policyAuthHeaderValue": "Valor esperado",
"policyAuthSetPasscode": "Definir Código de Acesso",
"policyAuthSetPincode": "Definir Código PIN",
"policyAuthSetEmailWhitelist": "Definir Lista de E-mails Permitidos",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Aplicar Diagrama",
"actionListBlueprints": "Listar Modelos",
"actionGetBlueprint": "Obter Modelo",
- "actionCreateOrgWideLauncherView": "Criar Visualização do Lançador para Toda a Organização",
"setupToken": "Configuração do Token",
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
"setupTokenRequired": "Token de configuração é necessário",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Definir Código PIN do Recurso",
"actionSetResourceEmailWhitelist": "Definir Lista Permitida de Emails do Recurso",
"actionGetResourceEmailWhitelist": "Obter Lista Permitida de Emails do Recurso",
- "actionGetResourcePolicy": "Obter Política de Recurso",
- "actionUpdateResourcePolicy": "Atualizar Política de Recurso",
- "actionSetResourcePolicyUsers": "Definir Utilizadores da Política de Recurso",
- "actionSetResourcePolicyRoles": "Definir Papéis da Política de Recurso",
- "actionSetResourcePolicyPassword": "Definir Palavra-passe da Política de Recurso",
- "actionSetResourcePolicyPincode": "Definir Código PIN da Política de Recurso",
- "actionSetResourcePolicyHeaderAuth": "Definir Autenticação de Cabeçalho da Política de Recurso",
- "actionSetResourcePolicyWhitelist": "Definir Lista Permitida de Emails da Política de Recurso",
- "actionSetResourcePolicyRules": "Definir Regras da Política de Recurso",
"actionCreateTarget": "Criar Alvo",
"actionDeleteTarget": "Eliminar Alvo",
"actionGetTarget": "Obter Alvo",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Gerar Token de Acesso",
"actionDeleteAccessToken": "Eliminar Token de Acesso",
"actionListAccessTokens": "Listar Tokens de Acesso",
- "actionCreateResourceSessionToken": "Criar Token de Sessão de Recurso",
"actionCreateResourceRule": "Criar Regra de Recurso",
"actionDeleteResourceRule": "Eliminar Regra de Recurso",
"actionListResourceRules": "Listar Regras de Recurso",
@@ -1520,30 +1490,6 @@
"navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação",
"navbarDocsLink": "Documentação",
- "commandPaletteTitle": "Paleta de Comando",
- "commandPaletteDescription": "Pesquisar por páginas, organizações, recursos e ações",
- "commandPaletteSearchPlaceholder": "Pesquisar páginas, recursos, ações...",
- "commandPaletteNoResults": "Nenhum resultado encontrado.",
- "commandPaletteSearching": "Buscando...",
- "commandPaletteNavigation": "Navegação",
- "commandPaletteOrganizations": "Organizações",
- "commandPaletteSites": "Sites",
- "commandPaletteResources": "Recursos",
- "commandPaletteUsers": "Utilizadores",
- "commandPaletteClients": "Clientes de Máquina",
- "commandPaletteActions": "Ações",
- "commandPaletteCreateSite": "Criar Site",
- "commandPaletteCreateProxyResource": "Criar Recurso Público",
- "commandPaletteCreatePrivateResource": "Criar Recurso Privado",
- "commandPaletteCreateUser": "Criar Usuário",
- "commandPaletteCreateApiKey": "Criar Chave API",
- "commandPaletteCreateMachineClient": "Criar Cliente de Máquina",
- "commandPaletteCreateAlertRule": "Criar Regra de Alerta",
- "commandPaletteCreateIdentityProvider": "Criar Provedor de Identidade",
- "commandPaletteToggleTheme": "Alternar Tema",
- "commandPaletteChooseOrganization": "Escolher Organização",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Não foi possível ativar 2FA",
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Gestão",
"sidebarBillingAndLicenses": "Faturamento e Licenças",
"sidebarLogsAnalytics": "Análises",
- "commandSites": "Sites",
- "commandActionModeInfo": "Digite \">\" Para Abrir Modo de Ação",
- "commandResources": "Recursos",
- "commandProxyResources": "Recursos Públicos",
- "commandClientResources": "Recursos Privados",
- "commandClients": "Clientes",
- "commandUserDevices": "Dispositivos do Usuário",
- "commandMachineClients": "Clientes de Máquina",
- "commandDomains": "Domínios",
- "commandRemoteExitNodes": "Nodos Remotos",
- "commandTeam": "Equipe",
- "commandUsers": "Utilizadores",
- "commandRoles": "Papéis",
- "commandInvitations": "Convites",
- "commandPolicies": "Políticas Compartilhadas",
- "commandResourcePolicies": "Políticas de Recursos Públicos",
- "commandIdentityProviders": "Provedores de Identidade",
- "commandApprovals": "Pedidos de Aprovação",
- "commandShareableLinks": "Links Compartilháveis",
- "commandOrganization": "Organização",
- "commandLogsAndAnalytics": "Logs e Análises",
- "commandLogsAnalytics": "Análises",
- "commandLogsRequest": "Registros de Pedidos HTTP",
- "commandLogsAccess": "Logs de Autenticação",
- "commandLogsAction": "Logs de Ações do Administrador",
- "commandLogsConnection": "Logs da Conexão",
- "commandLogsStreaming": "Transmissão de Eventos",
- "commandManagement": "Gestão",
- "commandAlerting": "Alertas",
- "commandProvisioning": "Providência",
- "commandBluePrints": "Plantas",
- "commandApiKeys": "Chaves API",
- "commandBillingAndLicenses": "Faturamento e Licenças",
- "commandBilling": "Faturamento",
- "commandEnterpriseLicenses": "Licenças",
- "commandSettings": "Configurações",
- "commandLauncher": "Inicializador",
- "commandResourceLauncher": "Inicializador de Recurso",
- "commandSearchResults": "Resultados da Pesquisa",
"alertingTitle": "Alertas",
"alertingDescription": "Defina fontes, gatilhos e ações para notificações",
"alertingRules": "Regras de alerta",
@@ -1991,9 +1898,6 @@
"billingDomains": "Domínios",
"billingOrganizations": "Órgãos",
"billingRemoteExitNodes": "Nós remotos",
- "billingPublicResources": "Recursos Públicos",
- "billingPrivateResources": "Recursos Privados",
- "billingMachineClients": "Clientes de Máquina",
"billingNoLimitConfigured": "Nenhum limite configurado",
"billingEstimatedPeriod": "Período Estimado de Cobrança",
"billingIncludedUsage": "Uso Incluído",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Quantos usuários você pode usar",
"billingDomainInfo": "Quantos domínios você pode usar",
"billingRemoteExitNodesInfo": "Quantos nós remotos você pode usar",
- "billingPublicResourcesInfo": "Quantos recursos públicos você pode usar",
- "billingPrivateResourcesInfo": "Quantos recursos privados você pode usar",
- "billingMachineClientsInfo": "Quantos clientes de máquina você pode usar",
"billingLicenseKeys": "Chaves de Licença",
"billingLicenseKeysDescription": "Gerenciar suas subscrições de chave de licença",
"billingLicenseSubscription": "Assinatura de Licença",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Sub-rede",
"addressDescription": "O endereço interno do cliente. Deve estar dentro da sub-rede da organização.",
"selectSites": "Selecionar sites",
- "selectLabels": "Selecionar etiquetas",
"sitesDescription": "O cliente terá conectividade com os sites selecionados",
"clientInstallOlm": "Instalar Olm",
"clientInstallOlmDescription": "Execute o Olm em seu sistema",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Fechar",
"createInternalResourceDialogCreateClientResource": "Criar Recurso Privado",
"createInternalResourceDialogCreateClientResourceDescription": "Criar um novo recurso que só será acessível para clientes conectados à organização",
- "privateResourceGeneralDescription": "Configure o nome, identificador e outras configurações gerais de recursos.",
- "privateResourceCreatePageSeeAll": "Ver Todos os Recursos Privados",
- "privateResourceAllowIcmpPing": "Permitir ICMP Ping",
- "privateResourceNetworkAccess": "Acesso à Rede",
- "privateResourceNetworkAccessDescription": "Controlar o acesso à porta TCP/UDP e se o ICMP ping é permitido para este recurso.",
- "hostSettings": "Configurações do Host",
- "cidrSettings": "Configurações CIDR",
"createInternalResourceDialogResourceProperties": "Propriedades do Recurso",
"createInternalResourceDialogName": "Nome",
"createInternalResourceDialogSite": "Site",
"selectSite": "Selecionar site...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# rótulo} other {# rótulos}}",
"noSitesFound": "Nenhum site encontrado.",
"createInternalResourceDialogProtocol": "Protocolo",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.",
- "internalResourceAliasLocalWarning": "Os aliases terminando em .local podem causar problemas de resolução devido ao mDNS em algumas redes.",
"internalResourceDownstreamSchemeRequired": "Esquema é obrigatório para recursos HTTP",
"internalResourceHttpPortRequired": "Porta de destino é obrigatória para recursos HTTP",
"siteConfiguration": "Configuração",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Nós remotos",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Chave Secreta",
- "remoteExitNodeNetworkingTitle": "Configurações de Rede",
- "remoteExitNodeNetworkingDescription": "Configure como este nó de saída remoto roteia o tráfego e quais sites preferem se conectar através dele. Recursos avançados para serem usados com configurações de rede de backhaul.",
- "remoteExitNodeNetworkingSave": "Guardar Configurações",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Configurações de rede salvas",
- "remoteExitNodeNetworkingSaveSuccessDescription": "As configurações de rede foram atualizadas com sucesso.",
- "remoteExitNodeNetworkingSaveError": "Falha ao guardar as configurações de rede",
- "remoteExitNodeNetworkingSubnetsTitle": "Sub-redes Remotas",
- "remoteExitNodeNetworkingSubnetsDescription": "Defina os intervalos de CIDR que este nó de saída remoto irá rotear o tráfego. Digite um CIDR válido (por exemplo, 10.0.0.0/8) e pressione Enter para adicionar.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Adicione um intervalo de CIDR (por exemplo, 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Falha ao carregar sub-redes",
- "remoteExitNodeNetworkingLabelsTitle": "Etiquetas de Preferência",
- "remoteExitNodeNetworkingLabelsDescription": "Os sites com essas etiquetas serão forçados a se conectar através deste nó de saída remoto.",
- "remoteExitNodeNetworkingLabelsButtonText": "Selecionar etiquetas...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Pesquisar etiquetas...",
- "remoteExitNodeNetworkingLabelsLoadError": "Falha ao carregar etiquetas",
"remoteExitNodeCreate": {
"title": "Criar Nó Remoto",
"description": "Crie um novo nó de retransmissão e proxy servidor auto-hospedado",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Nenhum nó está disponível para esta organização. Crie um nó primeiro para usar sites locais.",
"exitNode": "Nodo de Saída",
"country": "País",
- "countryIsNot": "País Não é",
"rulesMatchCountry": "Atualmente baseado no IP de origem",
"region": "Região",
"selectRegion": "Selecionar região",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Provedor Google OAuth2/OIDC",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Sub-rede",
- "utilitySubnet": "Sub-rede de utilidade",
"subnetDescription": "A sub-rede para a configuração de rede dessa organização.",
"customDomain": "Domínio Personalizado",
"authPage": "Páginas de Autenticação",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Configuração de autenticação de dois fatores é necessária. Por favor, entre novamente via {dashboardUrl}/auth/login conclua este passo. Em seguida, volte aqui.",
"additionalSecurityRequired": "Segurança adicional necessária",
"organizationRequiresAdditionalSteps": "Esta organização requer etapas de segurança adicionais antes que você possa acessar os recursos.",
- "sessionExpired": "Sessão Expirada",
- "sessionExpiredReauthRequired": "Sua sessão expirou conforme a política de segurança da sua organização. Faça uma nova autenticação para continuar.",
- "reauthenticate": "Reautenticar",
"completeTheseSteps": "Conclua estas etapas",
"enableTwoFactorAuthentication": "Ativar autenticação de dois fatores",
"completeSecuritySteps": "Passos de segurança completos",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "ID da organização ou domínio está faltando",
"loadingDNSRecords": "Carregando registros DNS...",
"olmUpdateAvailableInfo": "Uma versão atualizada do Olm está disponível. Atualize para a versão mais recente para ter a melhor experiência.",
- "updateAvailableInfo": "Uma versão atualizada está disponível. Por favor, atualize para a versão mais recente para uma melhor experiência.",
"client": "Cliente",
"proxyProtocol": "Configurações de Protocolo Proxy",
"proxyProtocolDescription": "Configurar o protocolo proxy para preservar endereços IP do cliente para serviços TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Adicionar Usuários",
"editInternalResourceDialogAddClients": "Adicionar Clientes",
"editInternalResourceDialogDestinationLabel": "Destino",
- "editInternalResourceDialogDestinationDescription": "Configure como os clientes acessam este recurso.",
+ "editInternalResourceDialogDestinationDescription": "Especifique o endereço de destino para o recurso interno. Isso pode ser um nome de host, endereço IP ou intervalo CIDR, dependendo do modo selecionado. Opcionalmente, defina um alias interno de DNS para facilitar a identificação.",
"internalResourceFormMultiSiteRoutingHelp": "Selecionar múltiplos sites permite roteamento resiliente e failover para alta disponibilidade.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Saiba mais",
"editInternalResourceDialogPortRestrictionsDescription": "Restrinja o acesso a portas TCP/UDP específicas ou permita/bloqueie todas as portas.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Lista de E-mails Permitidos",
"memberPortalResourceDisabled": "Recurso Desativado",
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
- "resourceLauncherTitle": "Lançador de Recursos",
- "resourceSidebarLauncherTitle": "Inicializador",
- "resourceLauncherDescription": "Veja todos os recursos disponíveis e inicie-os de um único local",
- "resourceLauncherSearchPlaceholder": "Procurar recursos...",
- "resourceLauncherDefaultView": "Padrão",
- "resourceLauncherSaveView": "Salvar Visualização",
- "resourceLauncherSaveToCurrentView": "Salvar na Visualização Atual",
- "resourceLauncherSaveDefaultPersonal": "Salvar para mim",
- "resourceLauncherResetView": "Redefinir Visualização",
- "resourceLauncherResetSystemDefault": "Redefinir para o padrão do sistema",
- "resourceLauncherSystemDefaultRestored": "Padrão do sistema restaurado",
- "resourceLauncherSystemDefaultRestoredDescription": "A visualização padrão foi redefinida para as configurações originais.",
- "resourceLauncherSaveAsNewView": "Salvar como Nova Visualização",
- "resourceLauncherSaveAsNewViewDescription": "Dê um nome a esta visualização para salvar os filtros e layout atuais.",
- "resourceLauncherSaveForEveryone": "Salvar para Todos",
- "resourceLauncherSaveForEveryoneDescription": "Compartilhe esta visualização com todos os membros da organização. Quando desmarcado, a visualização é visível apenas para você.",
- "resourceLauncherMakePersonal": "Tornar Pessoal",
- "resourceLauncherFilter": "Filtro",
- "resourceLauncherFilterWithCount": "Filtro, {count} aplicado",
- "resourceLauncherSort": "Ordenar",
- "resourceLauncherSortAscending": "Ordenar ascendente",
- "resourceLauncherSortDescending": "Ordenar descendente",
- "resourceLauncherSettings": "Configurações",
- "resourceLauncherGroupBy": "Agrupar por",
- "resourceLauncherGroupBySite": "Site",
- "resourceLauncherGroupByLabel": "Marcador",
- "resourceLauncherGroupByNone": "Nenhum",
- "resourceLauncherLayout": "Layout",
- "resourceLauncherLayoutGrid": "Grade",
- "resourceLauncherLayoutList": "Lista",
- "resourceLauncherShowLabels": "Mostrar Marcadores",
- "resourceLauncherShowSiteTags": "Mostrar Etiquetas de Site",
- "resourceLauncherShowRecents": "Mostrar Recents",
- "resourceLauncherDeleteView": "Excluir Visualização",
- "resourceLauncherDeleteViewTitle": "Excluir Visualização",
- "resourceLauncherDeleteViewQuestion": "Tem certeza de que deseja excluir esta visualização de inicializador?",
- "resourceLauncherDeleteViewConfirm": "Excluir Visualização",
- "resourceLauncherViewAsAdmin": "Visualizar como Administrador",
- "resourceLauncherResourceDetailsDescription": "Informações de conexão e status para este recurso.",
- "resourceLauncherResourceDetails": "Detalhes do Recurso",
- "resourceLauncherAuthMethodsDescription": "Métodos de autenticação habilitados para este recurso.",
- "resourceLauncherPrivateClientRequired": "Conecte com um cliente no seu dispositivo para acessar este recurso privadamente.",
- "resourceLauncherPrivateClientRequiredTitle": "Conexão do Cliente Obrigatória",
- "resourceLauncherDownloadClient": "Baixar cliente",
- "resourceLauncherFailedToLoadDetails": "Não foi possível carregar os detalhes do recurso. Você pode não ter mais acesso a este recurso.",
- "resourceLauncherNoPortRestrictions": "Sem restrições de porta",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Sem Etiqueta",
- "resourceLauncherNoSite": "Sem Site",
- "resourceLauncherNoResourcesInGroup": "Nenhum recurso neste grupo",
- "resourceLauncherEmptyStateTitle": "Nenhum Recurso Disponível",
- "resourceLauncherEmptyStateDescription": "Você não tem acesso a nenhum recurso ainda. Entre em contato com seu administrador para solicitar acesso.",
- "resourceLauncherEmptyStateNoResultsTitle": "Nenhum Recurso Encontrado",
- "resourceLauncherEmptyStateNoResultsDescription": "Nenhum recurso corresponde à sua busca ou filtros atuais. Experimente ajustá-los para encontrar o que está procurando.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Nenhum recurso corresponde a \"{query}\". Tente ajustar sua busca ou limpar os filtros para ver todos os recursos.",
- "resourceLauncherSearchFirstTitle": "Pesquisar ou Filtrar para Navegar",
- "resourceLauncherSearchFirstDescription": "Você tem acesso a muitos recursos. Use a pesquisa ou filtre por site ou etiqueta para encontrar o que precisa.",
- "resourceLauncherSiteGroupingDisabled": "Agrupamento de site indisponível nesta escala. Filtrar por site para agrupar um conjunto menor.",
- "resourceLauncherLabelGroupingDisabled": "Agrupamento de etiquetas indisponível nesta escala.",
- "resourceLauncherCompactModeHint": "Mostrando uma lista simplificada para navegação mais rápida. Use pesquisa ou filtros para restringir os resultados.",
- "resourceLauncherCompactGroupingHint": "Aplique filtros de site ou etiqueta para habilitar o agrupamento.",
- "resourceLauncherCopiedToClipboard": "Copiado para a área de transferência",
- "resourceLauncherCopiedAccessDescription": "O acesso ao recurso foi copiado para sua área de transferência.",
- "resourceLauncherViewNamePlaceholder": "Nome da Visualização",
- "resourceLauncherViewNameLabel": "Nome da Visualização",
- "resourceLauncherViewSaved": "Visualização salva",
- "resourceLauncherViewSavedDescription": "Sua visualização do lançador foi salva.",
- "resourceLauncherViewSaveFailed": "Falha ao salvar visualização",
- "resourceLauncherViewSaveFailedDescription": "Não foi possível salvar a visualização do lançador. Por favor, tente novamente.",
- "resourceLauncherViewDeleted": "Visualização excluída",
- "resourceLauncherViewDeletedDescription": "A visualização do lançador foi excluída.",
- "resourceLauncherViewDeleteFailed": "Falha ao excluir visualização",
- "resourceLauncherViewDeleteFailedDescription": "Não foi possível excluir a visualização do lançador. Por favor, tente novamente.",
"memberPortalPrevious": "Anterior",
"memberPortalNext": "Próximo",
"httpSettings": "Configurações HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "Chave privada é necessária",
"vncTitle": "VNC",
- "vncSignInDescription": "Digite suas credenciais VNC para conectar",
- "vncUsernameOptional": "Nome de usuário (opcional)",
+ "vncSignInDescription": "Digite sua senha VNC para conectar",
"vncPasswordOptional": "Senha (opcional)",
"vncNoResourceTarget": "Nenhum alvo de recurso disponível",
"vncFailedToLoadNovnc": "Falha ao carregar noVNC",
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 53bf7b015..cacc4b895 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -66,15 +66,9 @@
"local": "Локальный",
"edit": "Редактировать",
"siteConfirmDelete": "Подтвердить удаление сайта",
- "siteConfirmDeleteAndResources": "Подтвердите удаление сайта и ресурсов",
"siteDelete": "Удалить сайт",
- "siteDeleteAndResources": "Удалить сайт и ресурсы",
"siteMessageRemove": "После удаления сайт больше не будет доступен. Все цели, связанные с сайтом, также будут удалены.",
- "siteMessageRemoveAndResources": "Это навсегда удалит все общественные и частные ресурсы, связанные с этим сайтом, даже если ресурс также связан с другими сайтами.",
"siteQuestionRemove": "Вы уверены, что хотите удалить сайт из организации?",
- "siteQuestionRemoveAndResources": "Вы уверены, что хотите удалить этот сайт и все связанные с ним ресурсы?",
- "sitesTableDeleteSite": "Удалить сайт",
- "sitesTableDeleteSiteAndResources": "Удалить сайт и ресурсы",
"siteManageSites": "Управление сайтами",
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
"sitesBannerTitle": "Подключить любую сеть",
@@ -123,16 +117,6 @@
"siteUpdated": "Сайт обновлён",
"siteUpdatedDescription": "Сайт был успешно обновлён.",
"siteGeneralDescription": "Настройте общие параметры для этого сайта",
- "siteRestartTitle": "Перезагрузить сайт",
- "siteRestartDescription": "Перезапустите туннель WireGuard для этого сайта. Это кратковременно прервет соединение.",
- "siteRestartBody": "Используйте это, если туннель сайта не работает должным образом и вам нужно принудительно переподключиться без перезапуска хоста.",
- "siteRestartButton": "Перезагрузить сайт",
- "siteRestartDialogMessage": "Вы уверены, что хотите перезапустить туннель WireGuard для {name}? Сайт кратковременно потеряет соединение.",
- "siteRestartWarning": "Сайт кратковременно отключится во время перезапуска туннеля.",
- "siteRestarted": "Сайт перезапущен",
- "siteRestartedDescription": "Туннель WireGuard был перезапущен.",
- "siteErrorRestart": "Не удалось перезапустить сайт",
- "siteErrorRestartDescription": "Произошла ошибка во время перезапуска сайта.",
"siteSettingDescription": "Настройка параметров на сайте",
"siteResourcesTab": "Ресурсы",
"siteResourcesNoneOnSite": "На этом сайте пока нет публичных или частных ресурсов.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Прокси запросы через необработанный TCP/UDP с использованием номера порта. Требуется подключение сайтов к удаленному узлу.",
"resourceCreate": "Создание ресурса",
"resourceCreateDescription": "Следуйте инструкциям ниже для создания нового ресурса",
- "resourcePublicCreate": "Создать публичный ресурс",
- "resourcePublicCreateDescription": "Следуйте инструкциям ниже, чтобы создать новый публичный ресурс, доступный через веб-браузер",
"resourceCreateGeneralDescription": "Настройте основные параметры ресурса, включая его имя и тип",
"resourceSeeAll": "Посмотреть все ресурсы",
"resourceCreateGeneral": "Общие",
@@ -615,8 +597,7 @@
"idpNameInternal": "Внутренний",
"emailInvalid": "Неверный адрес Email",
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
- "accessRoleSelectPlease": "Пользователь должен принадлежать хотя бы к одной роли.",
- "accessRoleRequired": "Требуется роль",
+ "accessRoleSelectPlease": "Пожалуйста, выберите роль",
"removeOwnAdminRoleConfirmTitle": "Удалить доступ администратора?",
"removeOwnAdminRoleConfirmDescription": "После сохранения у вас больше не будет прав администратора в этой организации. Другой администратор может восстановить доступ, если это необходимо.",
"removeOwnAdminRoleConfirmButton": "Удалить мой доступ администратора",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Базовая аутентификация заголовка",
"policyAuthHeaderAuthDescription": "Проверка пользовательского имени и значения HTTP-заголовка для каждого запроса",
"policyAuthHeaderAuthSummary": "Заголовок настроен",
- "policyAuthHeaderName": "Имя пользователя",
- "policyAuthHeaderValue": "Пароль",
+ "policyAuthHeaderName": "Имя заголовка",
+ "policyAuthHeaderValue": "Ожидаемое значение",
"policyAuthSetPasscode": "Установить пароль",
"policyAuthSetPincode": "Установить ПИН-код",
"policyAuthSetEmailWhitelist": "Установить белый список email",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Применить чертёж",
"actionListBlueprints": "Список чертежей",
"actionGetBlueprint": "Получить чертёж",
- "actionCreateOrgWideLauncherView": "Создать вид запуска на уровне организации",
"setupToken": "Код настройки",
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
"setupTokenRequired": "Токен настройки обязателен",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Установить ПИН-код ресурса",
"actionSetResourceEmailWhitelist": "Настроить белый список ресурсов email",
"actionGetResourceEmailWhitelist": "Получить белый список ресурсов email",
- "actionGetResourcePolicy": "Получить политику ресурса",
- "actionUpdateResourcePolicy": "Обновить политику ресурса",
- "actionSetResourcePolicyUsers": "Установить пользователей политики ресурса",
- "actionSetResourcePolicyRoles": "Установить роли политики ресурса",
- "actionSetResourcePolicyPassword": "Установить пароль политики ресурса",
- "actionSetResourcePolicyPincode": "Установить ПИН-код политики ресурса",
- "actionSetResourcePolicyHeaderAuth": "Установить аутентификацию по заголовкам для политики ресурса",
- "actionSetResourcePolicyWhitelist": "Установить белый список по email для политики ресурса",
- "actionSetResourcePolicyRules": "Установить правила политики ресурса",
"actionCreateTarget": "Создать цель",
"actionDeleteTarget": "Удалить цель",
"actionGetTarget": "Получить цель",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Сгенерировать токен доступа",
"actionDeleteAccessToken": "Удалить токен доступа",
"actionListAccessTokens": "Список токенов доступа",
- "actionCreateResourceSessionToken": "Создать токен сеанса ресурса",
"actionCreateResourceRule": "Создать правило ресурса",
"actionDeleteResourceRule": "Удалить правило ресурса",
"actionListResourceRules": "Список правил ресурса",
@@ -1520,30 +1490,6 @@
"navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения",
"navbarDocsLink": "Документация",
- "commandPaletteTitle": "Палитра команд",
- "commandPaletteDescription": "Поиск страниц, организаций, ресурсов и действий",
- "commandPaletteSearchPlaceholder": "Поиск страниц, ресурсов, действий...",
- "commandPaletteNoResults": "Результаты не найдены.",
- "commandPaletteSearching": "Поиск...",
- "commandPaletteNavigation": "Навигация",
- "commandPaletteOrganizations": "Организации",
- "commandPaletteSites": "Сайты",
- "commandPaletteResources": "Ресурсы",
- "commandPaletteUsers": "Пользователи",
- "commandPaletteClients": "Клиенты машин",
- "commandPaletteActions": "Действия",
- "commandPaletteCreateSite": "Создать сайт",
- "commandPaletteCreateProxyResource": "Создать публичный ресурс",
- "commandPaletteCreatePrivateResource": "Создать частный ресурс",
- "commandPaletteCreateUser": "Создать пользователя",
- "commandPaletteCreateApiKey": "Создать ключ API",
- "commandPaletteCreateMachineClient": "Создать клиент машин",
- "commandPaletteCreateAlertRule": "Создать правило предупреждения",
- "commandPaletteCreateIdentityProvider": "Создать поставщика удостоверений",
- "commandPaletteToggleTheme": "Переключить тему",
- "commandPaletteChooseOrganization": "Выбрать организацию",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "Невозможно включить 2FA",
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Управление",
"sidebarBillingAndLicenses": "Биллинг и лицензии",
"sidebarLogsAnalytics": "Статистика",
- "commandSites": "Сайты",
- "commandActionModeInfo": "Введите \">\", чтобы открыть режим действий",
- "commandResources": "Ресурсы",
- "commandProxyResources": "Публичные ресурсы",
- "commandClientResources": "Частные ресурсы",
- "commandClients": "Клиенты",
- "commandUserDevices": "Устройства пользователей",
- "commandMachineClients": "Клиенты машин",
- "commandDomains": "Домены",
- "commandRemoteExitNodes": "Удаленные узлы",
- "commandTeam": "Команда",
- "commandUsers": "Пользователи",
- "commandRoles": "Роли",
- "commandInvitations": "Приглашения",
- "commandPolicies": "Общие политики",
- "commandResourcePolicies": "Политики публичных ресурсов",
- "commandIdentityProviders": "Поставщики удостоверений",
- "commandApprovals": "Запросы на одобрение",
- "commandShareableLinks": "Общие ссылки",
- "commandOrganization": "Организация",
- "commandLogsAndAnalytics": "Логи и аналитика",
- "commandLogsAnalytics": "Аналитика",
- "commandLogsRequest": "HTTP журналы запросов",
- "commandLogsAccess": "Журналы аутентификации",
- "commandLogsAction": "Журналы административных действий",
- "commandLogsConnection": "Журнал сетевых подключений",
- "commandLogsStreaming": "Трансляция события",
- "commandManagement": "Управление",
- "commandAlerting": "Оповещения",
- "commandProvisioning": "Провиженинг",
- "commandBluePrints": "Шаблоны",
- "commandApiKeys": "API ключи",
- "commandBillingAndLicenses": "Выставление счетов и лицензии",
- "commandBilling": "Выставление счетов",
- "commandEnterpriseLicenses": "Лицензии",
- "commandSettings": "Настройки",
- "commandLauncher": "Запускатор",
- "commandResourceLauncher": "Запускатор ресурсов",
- "commandSearchResults": "Результаты поиска",
"alertingTitle": "Оповещения",
"alertingDescription": "Определите источники, триггеры и действия для уведомлений",
"alertingRules": "Правила оповещений",
@@ -1991,9 +1898,6 @@
"billingDomains": "Домены",
"billingOrganizations": "Орги",
"billingRemoteExitNodes": "Удаленные узлы",
- "billingPublicResources": "Публичные ресурсы",
- "billingPrivateResources": "Частные ресурсы",
- "billingMachineClients": "Клиенты машин",
"billingNoLimitConfigured": "Лимит не установлен",
"billingEstimatedPeriod": "Предполагаемый период выставления счетов",
"billingIncludedUsage": "Включенное использование",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Сколько пользователей вы можете использовать",
"billingDomainInfo": "Сколько доменов вы можете использовать",
"billingRemoteExitNodesInfo": "Сколько удаленных узлов вы можете использовать",
- "billingPublicResourcesInfo": "Сколько публичных ресурсов вы можете использовать",
- "billingPrivateResourcesInfo": "Сколько частных ресурсов вы можете использовать",
- "billingMachineClientsInfo": "Сколько машинных клиентов вы можете использовать",
"billingLicenseKeys": "Лицензионные ключи",
"billingLicenseKeysDescription": "Управление подписками на лицензионные ключи",
"billingLicenseSubscription": "Лицензионное соглашение",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Подсеть",
"addressDescription": "Внутренний адрес клиента. Должен находиться в подсети организации.",
"selectSites": "Выберите сайты",
- "selectLabels": "Выберите метки",
"sitesDescription": "Клиент будет иметь подключение к выбранным сайтам",
"clientInstallOlm": "Установить Olm",
"clientInstallOlmDescription": "Запустите Olm на вашей системе",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Закрыть",
"createInternalResourceDialogCreateClientResource": "Создать приватный ресурс",
"createInternalResourceDialogCreateClientResourceDescription": "Создать новый ресурс, который будет доступен только клиентам, подключенным к организации",
- "privateResourceGeneralDescription": "Настройте имя, идентификатор и другие общие параметры ресурса.",
- "privateResourceCreatePageSeeAll": "Посмотреть все частные ресурсы",
- "privateResourceAllowIcmpPing": "Разрешить ICMP Ping",
- "privateResourceNetworkAccess": "Сетевой доступ",
- "privateResourceNetworkAccessDescription": "Управляйте доступом к портам TCP/UDP и настройте разрешение ICMP ping для данного ресурса.",
- "hostSettings": "Настройки хоста",
- "cidrSettings": "Настройки CIDR",
"createInternalResourceDialogResourceProperties": "Свойства ресурса",
"createInternalResourceDialogName": "Имя",
"createInternalResourceDialogSite": "Сайт",
"selectSite": "Выберите сайт...",
"multiSitesSelectorSitesCount": "{count, plural, one {# сайт} few {# сайта} many {# сайтов} other {# сайтов}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# метка} few {# метки} many {# меток} other {# меток}}",
"noSitesFound": "Сайты не найдены.",
"createInternalResourceDialogProtocol": "Протокол",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
- "internalResourceAliasLocalWarning": "Псевдонимы, оканчивающиеся на .local, могут вызывать проблемы с разрешением из-за mDNS в некоторых сетях.",
"internalResourceDownstreamSchemeRequired": "Схема обязательна для HTTP ресурсов",
"internalResourceHttpPortRequired": "Порт назначения обязателен для HTTP ресурсов",
"siteConfiguration": "Конфигурация",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Удаленные узлы",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Секретный ключ",
- "remoteExitNodeNetworkingTitle": "Настройки сети",
- "remoteExitNodeNetworkingDescription": "Настройте, как этот удаленный узел выхода маршрутизирует трафик и какие сайты предпочитают подключаться через него. Расширенные функции для использования с конфигурациями магистральной сети.",
- "remoteExitNodeNetworkingSave": "Сохранить настройки",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Сетевые настройки сохранены",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Сетевые настройки были успешно обновлены.",
- "remoteExitNodeNetworkingSaveError": "Не удалось сохранить сетевые настройки",
- "remoteExitNodeNetworkingSubnetsTitle": "Удалённые подсети",
- "remoteExitNodeNetworkingSubnetsDescription": "Определите диапазоны CIDR, которые этот удаленный узел выхода будет использовать для маршрутизации трафика. Введите действительный CIDR (например, 10.0.0.0/8) и нажмите Enter, чтобы добавить.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Добавить диапазон CIDR (например, 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Не удалось загрузить подсети",
- "remoteExitNodeNetworkingLabelsTitle": "Этикетки предпочтений",
- "remoteExitNodeNetworkingLabelsDescription": "Сайты с этими метками будут обязаны подключаться через этот удаленный узел выхода.",
- "remoteExitNodeNetworkingLabelsButtonText": "Выберите метки...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Поиск меток...",
- "remoteExitNodeNetworkingLabelsLoadError": "Не удалось загрузить метки",
"remoteExitNodeCreate": {
"title": "Создать удалённый узел",
"description": "Создайте новый самостоятельный удалённый ретранслятор и узел прокси-сервера",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Для этой организации узлы не доступны. Сначала создайте узел, чтобы использовать локальные сайты.",
"exitNode": "Узел выхода",
"country": "Страна",
- "countryIsNot": "Страна не является",
"rulesMatchCountry": "В настоящее время основано на исходном IP",
"region": "Регион",
"selectRegion": "Выберите регион",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC провайдер",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "Подсеть",
- "utilitySubnet": "Утилита подсети",
"subnetDescription": "Подсеть для конфигурации сети этой организации.",
"customDomain": "Пользовательский домен",
"authPage": "Страницы аутентификации",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "Требуется настройка двухфакторной аутентификации. Пожалуйста, войдите снова через {dashboardUrl}/auth/login завершить этот шаг. Затем вернитесь сюда.",
"additionalSecurityRequired": "Требуется дополнительная безопасность",
"organizationRequiresAdditionalSteps": "Эта организация требует дополнительных шагов безопасности, прежде чем вы сможете получить доступ к ресурсам.",
- "sessionExpired": "Сессия истекла",
- "sessionExpiredReauthRequired": "Ваша сессия истекла согласно политике безопасности вашей организации. Пожалуйста, повторно пройдите аутентификацию, чтобы продолжить.",
- "reauthenticate": "Повторная аутентификация",
"completeTheseSteps": "Выполните эти шаги",
"enableTwoFactorAuthentication": "Включить двухфакторную аутентификацию",
"completeSecuritySteps": "Пройти шаги безопасности",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Отсутствует организация или ID домена",
"loadingDNSRecords": "Загрузка записей DNS...",
"olmUpdateAvailableInfo": "Доступна обновленная версия Олма. Пожалуйста, обновитесь до последней версии.",
- "updateAvailableInfo": "Доступна обновленная версия. Пожалуйста, обновитесь до последней версии для получения лучшего опыта.",
"client": "Клиент",
"proxyProtocol": "Настройки протокола прокси",
"proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Добавить пользователей",
"editInternalResourceDialogAddClients": "Добавить клиентов",
"editInternalResourceDialogDestinationLabel": "Пункт назначения",
- "editInternalResourceDialogDestinationDescription": "Настройте, как клиенты получают доступ к этому ресурсу.",
+ "editInternalResourceDialogDestinationDescription": "Укажите адрес назначения для внутреннего ресурса. Это может быть имя хоста, IP-адрес или диапазон CIDR в зависимости от выбранного режима. При необходимости установите внутренний DNS-алиас для облегчения идентификации.",
"internalResourceFormMultiSiteRoutingHelp": "Выбор нескольких сайтов позволяет обеспечить отказоустойчивую маршрутизацию и фейловер для высокой доступности.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Узнать больше",
"editInternalResourceDialogPortRestrictionsDescription": "Ограничьте доступ к определенным TCP/UDP-портам или разрешите/заблокируйте все порты.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "Белый список email",
"memberPortalResourceDisabled": "Ресурс отключён",
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
- "resourceLauncherTitle": "Запуск ресурса",
- "resourceSidebarLauncherTitle": "Запускатор",
- "resourceLauncherDescription": "Просмотрите все доступные ресурсы и запустите их из одного централизованного узла",
- "resourceLauncherSearchPlaceholder": "Поиск ваших ресурсов...",
- "resourceLauncherDefaultView": "По умолчанию",
- "resourceLauncherSaveView": "Сохранить вид",
- "resourceLauncherSaveToCurrentView": "Сохранить в текущий вид",
- "resourceLauncherSaveDefaultPersonal": "Сохранить для меня",
- "resourceLauncherResetView": "Сбросить вид",
- "resourceLauncherResetSystemDefault": "Сбросить на системные настройки по умолчанию",
- "resourceLauncherSystemDefaultRestored": "Системные настройки по умолчанию восстановлены",
- "resourceLauncherSystemDefaultRestoredDescription": "Вид по умолчанию был сброшен до исходных настроек.",
- "resourceLauncherSaveAsNewView": "Сохранить как новый вид",
- "resourceLauncherSaveAsNewViewDescription": "Дайте этому виду имя, чтобы сохранить текущие фильтры и макет.",
- "resourceLauncherSaveForEveryone": "Сохранить для всех",
- "resourceLauncherSaveForEveryoneDescription": "Поделитесь этим видом со всеми членами организации. Если не отмечено, видимость только для вас.",
- "resourceLauncherMakePersonal": "Сделать личным",
- "resourceLauncherFilter": "Фильтр",
- "resourceLauncherFilterWithCount": "Фильтр, применено {count}",
- "resourceLauncherSort": "Сортировать",
- "resourceLauncherSortAscending": "Сортировать по возрастанию",
- "resourceLauncherSortDescending": "Сортировать по убыванию",
- "resourceLauncherSettings": "Настройки",
- "resourceLauncherGroupBy": "Группировать по",
- "resourceLauncherGroupBySite": "Сайт",
- "resourceLauncherGroupByLabel": "Метка",
- "resourceLauncherGroupByNone": "Нет",
- "resourceLauncherLayout": "Макет",
- "resourceLauncherLayoutGrid": "Сетка",
- "resourceLauncherLayoutList": "Список",
- "resourceLauncherShowLabels": "Показать метки",
- "resourceLauncherShowSiteTags": "Показать теги сайта",
- "resourceLauncherShowRecents": "Показать недавно",
- "resourceLauncherDeleteView": "Удалить вид",
- "resourceLauncherDeleteViewTitle": "Удалить вид",
- "resourceLauncherDeleteViewQuestion": "Вы уверены, что хотите удалить этот вид запускатора?",
- "resourceLauncherDeleteViewConfirm": "Удалить вид",
- "resourceLauncherViewAsAdmin": "Просмотр как администратор",
- "resourceLauncherResourceDetailsDescription": "Информация о подключении и статус для данного ресурса.",
- "resourceLauncherResourceDetails": "Детали ресурса",
- "resourceLauncherAuthMethodsDescription": "Методы аутентификации, включенные для этого ресурса.",
- "resourceLauncherPrivateClientRequired": "Подключитесь с клиентом на устройстве для доступа к этому ресурсу в частном порядке.",
- "resourceLauncherPrivateClientRequiredTitle": "Требуется подключение клиента",
- "resourceLauncherDownloadClient": "Скачать клиент",
- "resourceLauncherFailedToLoadDetails": "Не удалось загрузить детали ресурса. Возможно, у вас больше нет доступа к этому ресурсу.",
- "resourceLauncherNoPortRestrictions": "Нет ограничений портов",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Без меток",
- "resourceLauncherNoSite": "Без сайта",
- "resourceLauncherNoResourcesInGroup": "Нет ресурсов в данной группе",
- "resourceLauncherEmptyStateTitle": "Нет доступных ресурсов",
- "resourceLauncherEmptyStateDescription": "У вас пока нет доступа ни к одному ресурсу. Обратитесь к администратору, чтобы запросить доступ.",
- "resourceLauncherEmptyStateNoResultsTitle": "Ресурсы не найдены",
- "resourceLauncherEmptyStateNoResultsDescription": "Ни один ресурс не соответствует вашему текущему поисковому запросу или фильтрам. Попробуйте их изменить, чтобы найти нужное.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "Ни один ресурс не соответствует \"{query}\". Попробуйте изменить параметры поиска или очистить фильтры, чтобы увидеть все ресурсы.",
- "resourceLauncherSearchFirstTitle": "Поиск или фильтр для просмотра",
- "resourceLauncherSearchFirstDescription": "У вас есть доступ ко многим ресурсам. Используйте поиск или фильтр по сайту или метке, чтобы найти, что вам нужно.",
- "resourceLauncherSiteGroupingDisabled": "Группировка по сайту недоступна в этом масштабе. Отфильтруйте по сайту, чтобы сгруппировать меньший набор.",
- "resourceLauncherLabelGroupingDisabled": "Группировка по меткам недоступна в этом масштабе.",
- "resourceLauncherCompactModeHint": "Показывается упрощённый список для более быстрого просмотра. Используйте поиск или фильтры, чтобы сузить результаты.",
- "resourceLauncherCompactGroupingHint": "Примените фильтры сайта или меток, чтобы включить группировку.",
- "resourceLauncherCopiedToClipboard": "Скопировано в буфер обмена",
- "resourceLauncherCopiedAccessDescription": "Доступ к ресурсу был скопирован в ваш буфер обмена.",
- "resourceLauncherViewNamePlaceholder": "Имя вида",
- "resourceLauncherViewNameLabel": "Имя вида",
- "resourceLauncherViewSaved": "Вид сохранён",
- "resourceLauncherViewSavedDescription": "Ваш вид запуска был сохранён.",
- "resourceLauncherViewSaveFailed": "Не удалось сохранить вид",
- "resourceLauncherViewSaveFailedDescription": "Не удалось сохранить вид. Пожалуйста, попробуйте еще раз.",
- "resourceLauncherViewDeleted": "Вид удалён",
- "resourceLauncherViewDeletedDescription": "Вид запуска был удалён.",
- "resourceLauncherViewDeleteFailed": "Не удалось удалить вид",
- "resourceLauncherViewDeleteFailedDescription": "Не удалось удалить вид. Пожалуйста, попробуйте еще раз.",
"memberPortalPrevious": "Предыдущий",
"memberPortalNext": "Следующий",
"httpSettings": "Настройки HTTP",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----НАЧАЛО ЛИЧНОГО КЛЮЧА OPENSSH-----",
"sshPrivateKeyRequired": "Требуется личный ключ",
"vncTitle": "VNC",
- "vncSignInDescription": "Введите ваши учетные данные VNC для подключения",
- "vncUsernameOptional": "Имя пользователя (необязательно)",
+ "vncSignInDescription": "Введите пароль VNC для подключения",
"vncPasswordOptional": "Пароль (необязательно)",
"vncNoResourceTarget": "Отсутствует целевой ресурс",
"vncFailedToLoadNovnc": "Не удалось загрузить noVNC",
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index 6ab5d026d..d5fd66262 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -66,15 +66,9 @@
"local": "Yerel",
"edit": "Düzenle",
"siteConfirmDelete": "Site Silmeyi Onayla",
- "siteConfirmDeleteAndResources": "Site ve Kaynakları Silmeyi Onayla",
"siteDelete": "Siteyi Sil",
- "siteDeleteAndResources": "Site ve Kaynakları Sil",
"siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacaktır. Siteyle ilişkilendirilmiş tüm hedefler de kaldırılacaktır.",
- "siteMessageRemoveAndResources": "Bu işlem, diğer sitelerle de ilişkilendirilmiş olsa bile, bu siteye bağlı tüm genel ve özel kaynakları kalıcı olarak silecektir.",
"siteQuestionRemove": "Siteyi organizasyondan kaldırmak istediğinizden emin misiniz?",
- "siteQuestionRemoveAndResources": "Bu siteyi ve tüm ilişkili kaynakları silmek istediğinizden emin misiniz?",
- "sitesTableDeleteSite": "Siteyi Sil",
- "sitesTableDeleteSiteAndResources": "Site ve Kaynakları Sil",
"siteManageSites": "Siteleri Yönet",
"siteDescription": "Özel ağlara erişimi etkinleştirmek için siteler oluşturun ve yönetin",
"sitesBannerTitle": "Herhangi Bir Ağa Bağlan",
@@ -123,16 +117,6 @@
"siteUpdated": "Site güncellendi",
"siteUpdatedDescription": "Site güncellendi.",
"siteGeneralDescription": "Bu site için genel ayarları yapılandırın",
- "siteRestartTitle": "Siteyi Yeniden Başlat",
- "siteRestartDescription": "Bu site için WireGuard tünelini yeniden başlatın. Bu, bağlantıyı kısa süreliğine keser.",
- "siteRestartBody": "Site tüneli düzgün çalışmadığında ve ana bilgisayarı yeniden başlatmadan bağlantıyı yeniden sağlamak istiyorsanız bunu kullanın.",
- "siteRestartButton": "Siteyi Yeniden Başlat",
- "siteRestartDialogMessage": "{name} için WireGuard tünelini yeniden başlatmak istediğinizden emin misiniz? Site kısa süreliğine bağlantıyı kaybedecektir.",
- "siteRestartWarning": "Tünel yeniden başlatılırken site kısa süreliğine kesintiye uğrar.",
- "siteRestarted": "Site yeniden başlatıldı",
- "siteRestartedDescription": "WireGuard tüneli yeniden başlatıldı.",
- "siteErrorRestart": "Sitenin yeniden başlatılması başarısız oldu",
- "siteErrorRestartDescription": "Site yeniden başlatılırken bir hata oluştu.",
"siteSettingDescription": "Sitenizdeki ayarları yapılandırın",
"siteResourcesTab": "Kaynaklar",
"siteResourcesNoneOnSite": "Bu sitede henüz genel veya özel kaynak yok.",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "Proxy isteklerini bir port numarası kullanarak ham TCP/UDP üzerinden yapın. Sitelerin uzak bir düğüme bağlanması gereklidir.",
"resourceCreate": "Kaynak Oluştur",
"resourceCreateDescription": "Yeni bir kaynak oluşturmak için aşağıdaki adımları izleyin",
- "resourcePublicCreate": "Halka Açık Kaynak Oluştur",
- "resourcePublicCreateDescription": "Web tarayıcısı üzerinden erişilebilen yeni bir genel kaynak oluşturmak için aşağıdaki adımları izleyin",
"resourceCreateGeneralDescription": "Adı ve türü dahil temel kaynak ayarlarını yapılandırın",
"resourceSeeAll": "Tüm Kaynakları Gör",
"resourceCreateGeneral": "Genel",
@@ -615,8 +597,7 @@
"idpNameInternal": "Dahili",
"emailInvalid": "Geçersiz e-posta adresi",
"inviteValidityDuration": "Lütfen bir süre seçin",
- "accessRoleSelectPlease": "Bir kullanıcı en az bir role ait olmalıdır.",
- "accessRoleRequired": "Rol gerekli",
+ "accessRoleSelectPlease": "Lütfen bir rol seçin",
"removeOwnAdminRoleConfirmTitle": "Yönetici erişiminizi kaldırmak istiyor musunuz?",
"removeOwnAdminRoleConfirmDescription": "Kaydettikten sonra, bu organizasyonda artık yönetici izinleriniz olmayacak. Gerekirse başka bir yönetici erişimi geri yükleyebilir.",
"removeOwnAdminRoleConfirmButton": "Yönetici Erişimi Kaldır",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "Temel Başlık Kimlik Doğrulama",
"policyAuthHeaderAuthDescription": "Her istekte özel bir HTTP başlık adını ve değerini doğrulayın",
"policyAuthHeaderAuthSummary": "Başlık yapılandırıldı",
- "policyAuthHeaderName": "Kullanıcı Adı",
- "policyAuthHeaderValue": "Şifre",
+ "policyAuthHeaderName": "Başlık adı",
+ "policyAuthHeaderValue": "Beklenen değer",
"policyAuthSetPasscode": "Şifreyi Ayarla",
"policyAuthSetPincode": "PIN Kodunu Ayarla",
"policyAuthSetEmailWhitelist": "E-posta Beyaz Listesini Ayarla",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "Planı Uygula",
"actionListBlueprints": "Plan Listesini Görüntüle",
"actionGetBlueprint": "Planı Elde Et",
- "actionCreateOrgWideLauncherView": "Kuruluş Genelinde Başlatıcı Görünümü Oluşturma",
"setupToken": "Kurulum Simgesi",
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "Kaynak PIN Kodunu Ayarla",
"actionSetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesi Ayarla",
"actionGetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesini Al",
- "actionGetResourcePolicy": "Kaynak Politikasını Al",
- "actionUpdateResourcePolicy": "Kaynak Politikasını Güncelle",
- "actionSetResourcePolicyUsers": "Kaynak Politika Kullanıcılarını Ayarla",
- "actionSetResourcePolicyRoles": "Kaynak Politika Rolleri Ayarla",
- "actionSetResourcePolicyPassword": "Kaynak Politika Şifresini Ayarla",
- "actionSetResourcePolicyPincode": "Kaynak Politika Pincode Ayarla",
- "actionSetResourcePolicyHeaderAuth": "Kaynak Politika Başlık Kimlik Doğrulama Ayarla",
- "actionSetResourcePolicyWhitelist": "Kaynak Politika E-posta Beyaz Listesi Ayarla",
- "actionSetResourcePolicyRules": "Kaynak Politika Kurallarını Ayarla",
"actionCreateTarget": "Hedef Oluştur",
"actionDeleteTarget": "Hedefi Sil",
"actionGetTarget": "Hedefi Al",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "Erişim Jetonu Oluştur",
"actionDeleteAccessToken": "Erişim Jetonunu Sil",
"actionListAccessTokens": "Erişim Jetonlarını Listele",
- "actionCreateResourceSessionToken": "Kaynak Oturum Otomasyonu Oluştur",
"actionCreateResourceRule": "Kaynak Kuralı Oluştur",
"actionDeleteResourceRule": "Kaynak Kuralını Sil",
"actionListResourceRules": "Kaynak Kurallarını Listele",
@@ -1520,30 +1490,6 @@
"navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
"navbarDocsLink": "Dokümantasyon",
- "commandPaletteTitle": "Komut Paleti",
- "commandPaletteDescription": "Sayfalar, organizasyonlar, kaynaklar ve işlemler için arama yapın",
- "commandPaletteSearchPlaceholder": "Sayfaları, kaynakları, işlemleri ara...",
- "commandPaletteNoResults": "Sonuç bulunamadı.",
- "commandPaletteSearching": "Aranıyor...",
- "commandPaletteNavigation": "Navigasyon",
- "commandPaletteOrganizations": "Organizasyonlar",
- "commandPaletteSites": "Siteler",
- "commandPaletteResources": "Kaynaklar",
- "commandPaletteUsers": "Kullanıcılar",
- "commandPaletteClients": "Makine İstemcileri",
- "commandPaletteActions": "İşlemler",
- "commandPaletteCreateSite": "Site Oluştur",
- "commandPaletteCreateProxyResource": "Genel Kaynak Oluştur",
- "commandPaletteCreatePrivateResource": "Özel Kaynak Oluştur",
- "commandPaletteCreateUser": "Kullanıcı Oluştur",
- "commandPaletteCreateApiKey": "API Anahtarı Oluştur",
- "commandPaletteCreateMachineClient": "Makine İstemcisi Oluştur",
- "commandPaletteCreateAlertRule": "Uyarı Kuralı Oluştur",
- "commandPaletteCreateIdentityProvider": "Kimlik Sağlayıcı Oluştur",
- "commandPaletteToggleTheme": "Temayı Aç/Kapat",
- "commandPaletteChooseOrganization": "Organizasyon Seç",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "2FA etkinleştirilemedi",
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
"otpSetupCheckCode": "6 haneli bir kod girin",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "Yönetim",
"sidebarBillingAndLicenses": "Faturalandırma & Lisanslar",
"sidebarLogsAnalytics": "Analitik",
- "commandSites": "Siteler",
- "commandActionModeInfo": "Eylem Modunu Açmak İçin \">\" Yazın",
- "commandResources": "Kaynaklar",
- "commandProxyResources": "Genel Kaynaklar",
- "commandClientResources": "Özel Kaynaklar",
- "commandClients": "İstemciler",
- "commandUserDevices": "Kullanıcı Aygıtları",
- "commandMachineClients": "Makine İstemcileri",
- "commandDomains": "Alan Adları",
- "commandRemoteExitNodes": "Uzak Düğümler",
- "commandTeam": "Ekip",
- "commandUsers": "Kullanıcılar",
- "commandRoles": "Roller",
- "commandInvitations": "Davetiyeler",
- "commandPolicies": "Paylaşılan Politikalar",
- "commandResourcePolicies": "Genel Kaynaklar Politikaları",
- "commandIdentityProviders": "Kimlik Sağlayıcılar",
- "commandApprovals": "Onay İstekleri",
- "commandShareableLinks": "Paylaşılabilir Bağlantılar",
- "commandOrganization": "Organizasyon",
- "commandLogsAndAnalytics": "Günlükler ve Analitik",
- "commandLogsAnalytics": "Analitik",
- "commandLogsRequest": "HTTP İstek Günlükleri",
- "commandLogsAccess": "Kimlik Doğrulama Günlükleri",
- "commandLogsAction": "Yönetim Eylemi Günlükleri",
- "commandLogsConnection": "Ağ Günlükleri",
- "commandLogsStreaming": "Olay Akışı",
- "commandManagement": "Yönetim",
- "commandAlerting": "Uyarılar",
- "commandProvisioning": "Sağlama",
- "commandBluePrints": "Planlar",
- "commandApiKeys": "API Anahtarları",
- "commandBillingAndLicenses": "Faturalama ve Lisanslar",
- "commandBilling": "Faturalama",
- "commandEnterpriseLicenses": "Lisanslar",
- "commandSettings": "Ayarlar",
- "commandLauncher": "Başlatıcı",
- "commandResourceLauncher": "Kaynak Başlatıcı",
- "commandSearchResults": "Arama Sonuçları",
"alertingTitle": "Uyarı",
"alertingDescription": "Bildirimler için kaynakları, tetikleyicileri ve eylemleri tanımlayın",
"alertingRules": "Uyarı kuralları",
@@ -1991,9 +1898,6 @@
"billingDomains": "Alan Adları",
"billingOrganizations": "Organizasyonlar",
"billingRemoteExitNodes": "Uzak Düğümler",
- "billingPublicResources": "Genel Kaynaklar",
- "billingPrivateResources": "Özel Kaynaklar",
- "billingMachineClients": "Makine İstemcileri",
"billingNoLimitConfigured": "Hiçbir limit yapılandırılmadı",
"billingEstimatedPeriod": "Tahmini Fatura Dönemi",
"billingIncludedUsage": "Dahil Kullanım",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "Kaç tane kullanıcı kullanabileceğiniz",
"billingDomainInfo": "Kaç tane alan adı kullanabileceğiniz",
"billingRemoteExitNodesInfo": "Kaç tane uzaktan düğüm kullanabileceğiniz",
- "billingPublicResourcesInfo": "Kaç adet genel kaynağı kullanabileceğinizi görün",
- "billingPrivateResourcesInfo": "Kaç adet özel kaynağı kullanabileceğinizi görün",
- "billingMachineClientsInfo": "Kaç adet makine istemcisi kullanabileceğinizi görün",
"billingLicenseKeys": "Lisans Anahtarları",
"billingLicenseKeysDescription": "Lisans anahtarı aboneliklerinizi yönetin",
"billingLicenseSubscription": "Lisans Aboneliği",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "Alt ağ",
"addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.",
"selectSites": "Siteleri seçin",
- "selectLabels": "Etiketleri seçin",
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
"clientInstallOlm": "Olm Yükle",
"clientInstallOlmDescription": "Sisteminizde Olm çalıştırın",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "Kapat",
"createInternalResourceDialogCreateClientResource": "Özel Kaynak Oluştur",
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemcilere erişilebilir olacak yeni bir kaynak oluşturun",
- "privateResourceGeneralDescription": "Kaynağın adı, tanımlayıcı ve diğer genel ayarlarını yapılandırın.",
- "privateResourceCreatePageSeeAll": "Tüm Özel Kaynakları Gör",
- "privateResourceAllowIcmpPing": "ICMP Ping İzne Ver",
- "privateResourceNetworkAccess": "Ağ Erişimi",
- "privateResourceNetworkAccessDescription": "Bu kaynak için TCP/UDP port erişimini kontrol edin ve ICMP ping'in izin verilip verilmediğini kontrol edin.",
- "hostSettings": "Sunucu Ayarları",
- "cidrSettings": "CIDR Ayarları",
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
"createInternalResourceDialogName": "Ad",
"createInternalResourceDialogSite": "Site",
"selectSite": "Site seç...",
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# siteler}}",
- "labelsSelectorLabelsCount": "{count, plural, one {# etiket} other {# etiketler}}",
"noSitesFound": "Site bulunamadı.",
"createInternalResourceDialogProtocol": "Protokol",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "Site ağındaki kaynağın CIDR aralığı.",
"createInternalResourceDialogAlias": "Takma Ad",
"createInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.",
- "internalResourceAliasLocalWarning": "Bazı ağlarda mDNS nedeniyle .local ile biten takma adlar çözümleme sorunlarına neden olabilir.",
"internalResourceDownstreamSchemeRequired": "HTTP kaynakları için şema gereklidir",
"internalResourceHttpPortRequired": "HTTP kaynakları için hedef bağlantı noktası gereklidir",
"siteConfiguration": "Yapılandırma",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "Uzak Düğümler",
"remoteExitNodeId": "Kimlik",
"remoteExitNodeSecretKey": "Gizli",
- "remoteExitNodeNetworkingTitle": "Ağ Ayarları",
- "remoteExitNodeNetworkingDescription": "Bu uzak çıkış düğümünün trafiği nasıl yönlendireceğini ve hangi sitelerin bu üzerinden bağlanmayı tercih edeceğini yapılandırın. Gelişmiş özellikler geri bağlantı ağ konfigürasyonları ile kullanılmalıdır.",
- "remoteExitNodeNetworkingSave": "Ayarları Kaydet",
- "remoteExitNodeNetworkingSaveSuccessTitle": "Ağ ayarları kaydedildi",
- "remoteExitNodeNetworkingSaveSuccessDescription": "Ağ ayarları başarıyla güncellendi.",
- "remoteExitNodeNetworkingSaveError": "Ağ ayarları kaydedilemedi",
- "remoteExitNodeNetworkingSubnetsTitle": "Uzak Alt Ağlar",
- "remoteExitNodeNetworkingSubnetsDescription": "Bu uzak çıkış düğümünün trafiği taşıyacağı CIDR aralıklarını tanımlayın. Geçerli bir CIDR (örneğin, 10.0.0.0/8) yazın ve eklemek için Enter tuşuna basın.",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "Bir CIDR aralığı ekle (örneğin, 10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "Alt ağlar yüklenemedi",
- "remoteExitNodeNetworkingLabelsTitle": "Tercih Etiketleri",
- "remoteExitNodeNetworkingLabelsDescription": "Bu etiketlere sahip siteler, bu uzak çıkış düğümü üzerinden bağlantı kurmaya zorlanacaktır.",
- "remoteExitNodeNetworkingLabelsButtonText": "Etiketleri seç...",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "Etiketleri ara...",
- "remoteExitNodeNetworkingLabelsLoadError": "Etiketler yüklenemedi",
"remoteExitNodeCreate": {
"title": "Uzak Düğüm Oluştur",
"description": "Yeni bir kendine misafir uzaktan ileti ve ara sunucu düğümü oluşturun",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "Bu organizasyon için düğüm mevcut değil. Yerel siteleri kullanmak için önce bir düğüm oluşturun.",
"exitNode": "Çıkış Düğümü",
"country": "Ülke",
- "countryIsNot": "Ülke Değil",
"rulesMatchCountry": "Şu anda kaynak IP'ye dayanarak",
"region": "Bölge",
"selectRegion": "Bölgeyi seçin",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC sağlayıcısı",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı",
"subnet": "Alt ağ",
- "utilitySubnet": "Yardımcı Alt Ağ",
"subnetDescription": "Bu organizasyonun ağ yapılandırması için alt ağ.",
"customDomain": "Özel Alan",
"authPage": "Kimlik Sayfaları",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "İki faktörlü kimlik doğrulama ayarı gereklidir. Bu adımı tamamlamak için lütfen tekrar {dashboardUrl}/auth/login üzerinden oturum açın. Sonra buraya geri dönün.",
"additionalSecurityRequired": "Ek Güvenlik Gereklidir",
"organizationRequiresAdditionalSteps": "Bu kuruluş, kaynaklara erişmeden önce ek güvenlik adımları gerektirir.",
- "sessionExpired": "Oturum Süresi Doldu",
- "sessionExpiredReauthRequired": "Organizasyonunuzun güvenlik politikası gereği oturum süreniz doldu. Devam etmek için yeniden kimlik doğrulaması yapın.",
- "reauthenticate": "Yeniden Kimlik Doğrula",
"completeTheseSteps": "Bu adımları tamamlayın",
"enableTwoFactorAuthentication": "İki faktörlü kimlik doğrulamayı etkinleştir",
"completeSecuritySteps": "Güvenlik Adımlarını Tamamla",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "Organizasyon veya Alan Adı Kimliği eksik",
"loadingDNSRecords": "DNS kayıtları yükleniyor...",
"olmUpdateAvailableInfo": "Olm'nin güncellenmiş bir sürümü mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
- "updateAvailableInfo": "Güncellenmiş bir sürüm mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
"client": "İstemci",
"proxyProtocol": "Proxy Protokol Ayarları",
"proxyProtocolDescription": "TCP hizmetleri için istemci IP adreslerini korumak amacıyla Proxy Protokolünü yapılandırın.",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "Kullanıcılar Ekle",
"editInternalResourceDialogAddClients": "Müşteriler Ekle",
"editInternalResourceDialogDestinationLabel": "Hedef",
- "editInternalResourceDialogDestinationDescription": "Bu kaynağa müşterilerin nasıl erişeceğini yapılandırın.",
+ "editInternalResourceDialogDestinationDescription": "Dahili kaynak için hedef adresi belirtin. Seçilen moda bağlı olarak bu bir ana bilgisayar adı, IP adresi veya CIDR aralığı olabilir. Daha kolay tanımlama için isteğe bağlı olarak dahili bir DNS takma adı ayarlayın.",
"internalResourceFormMultiSiteRoutingHelp": "Birden fazla site seçmek, yüksek kullanılabilirlik için dirençli yönlendirme ve yedeklik sağlar.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Daha fazla bilgi",
"editInternalResourceDialogPortRestrictionsDescription": "Belirtilen TCP/UDP portlarına erişimi kısıtlayın veya tüm portlara izin/engelleme verin.",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "E-posta Beyaz Listesi",
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
- "resourceLauncherTitle": "Kaynak Başlatıcı",
- "resourceSidebarLauncherTitle": "Başlatıcı",
- "resourceLauncherDescription": "Tüm mevcut kaynakları görün ve bunları merkezi bir merkezden başlatın",
- "resourceLauncherSearchPlaceholder": "Kaynaklarınızı arayın...",
- "resourceLauncherDefaultView": "Varsayılan",
- "resourceLauncherSaveView": "Görünümü Kaydet",
- "resourceLauncherSaveToCurrentView": "Mevcut Görünüme Kaydet",
- "resourceLauncherSaveDefaultPersonal": "Benim için Kaydet",
- "resourceLauncherResetView": "Görünümü Sıfırla",
- "resourceLauncherResetSystemDefault": "Sistem Varsayılanını Sıfırla",
- "resourceLauncherSystemDefaultRestored": "Sistem varsayılanı geri yüklendi",
- "resourceLauncherSystemDefaultRestoredDescription": "Varsayılan görünüm orijinal ayarlara sıfırlandı.",
- "resourceLauncherSaveAsNewView": "Yeni Görünüm Olarak Kaydet",
- "resourceLauncherSaveAsNewViewDescription": "Geçerli filtrelerinizi ve düzeninizi kaydetmek için bu görünüme bir ad verin.",
- "resourceLauncherSaveForEveryone": "Herkes İçin Kaydet",
- "resourceLauncherSaveForEveryoneDescription": "Bu görünümü tüm kuruluş üyeleriyle paylaşın. İşaretli değilse, görünüm yalnızca size görünür olur.",
- "resourceLauncherMakePersonal": "Kişisel Yap",
- "resourceLauncherFilter": "Filtre",
- "resourceLauncherFilterWithCount": "Filtre, {count} uygulandı",
- "resourceLauncherSort": "Sıralama",
- "resourceLauncherSortAscending": "Artan sırala",
- "resourceLauncherSortDescending": "Azalan sırala",
- "resourceLauncherSettings": "Ayarlar",
- "resourceLauncherGroupBy": "Grupla",
- "resourceLauncherGroupBySite": "Site",
- "resourceLauncherGroupByLabel": "Etiket",
- "resourceLauncherGroupByNone": "Hiçbiri",
- "resourceLauncherLayout": "Düzen",
- "resourceLauncherLayoutGrid": "Izgara",
- "resourceLauncherLayoutList": "Liste",
- "resourceLauncherShowLabels": "Etiketleri Göster",
- "resourceLauncherShowSiteTags": "Site Etiketlerini Göster",
- "resourceLauncherShowRecents": "Son Eklenenleri Göster",
- "resourceLauncherDeleteView": "Görünümü Sil",
- "resourceLauncherDeleteViewTitle": "Görünümü Sil",
- "resourceLauncherDeleteViewQuestion": "Bu başlatıcı görünümünü silmek istediğinizden emin misiniz?",
- "resourceLauncherDeleteViewConfirm": "Görünümü Sil",
- "resourceLauncherViewAsAdmin": "Yönetici Olarak Görüntüle",
- "resourceLauncherResourceDetailsDescription": "Bu kaynağın bağlantı bilgileri ve durumu.",
- "resourceLauncherResourceDetails": "Kaynak Detayları",
- "resourceLauncherAuthMethodsDescription": "Bu kaynak için etkin kimlik doğrulama yöntemleri.",
- "resourceLauncherPrivateClientRequired": "Bu kaynağa özel olarak erişmek için cihazınızda bir istemci ile bağlanın.",
- "resourceLauncherPrivateClientRequiredTitle": "İstemci Bağlantısı Gerekli",
- "resourceLauncherDownloadClient": "İstemci indir",
- "resourceLauncherFailedToLoadDetails": "Kaynak detayları yüklenemedi. Bu kaynağa artık erişiminiz olmayabilir.",
- "resourceLauncherNoPortRestrictions": "Hiçbir port kısıtlaması yok",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "Etiketsiz",
- "resourceLauncherNoSite": "Site Yok",
- "resourceLauncherNoResourcesInGroup": "Bu grupta kaynak yok",
- "resourceLauncherEmptyStateTitle": "Kullanılabilir Kaynak Yok",
- "resourceLauncherEmptyStateDescription": "Henüz hiçbir kaynağa erişiminiz yok. Erişim istemek için yöneticinizle iletişime geçin.",
- "resourceLauncherEmptyStateNoResultsTitle": "Kaynak Bulunamadı",
- "resourceLauncherEmptyStateNoResultsDescription": "Mevcut arama veya filtrelerinizle eşleşen kaynak yok. Aradığınızı bulmak için ayarları değiştirmeyi deneyin.",
- "resourceLauncherEmptyStateNoResultsWithQuery": "\"{query}\" ile eşleşen kaynak yok. Tüm kaynakları görmek için aramayı düzenlemeyi veya filtreleri temizlemeyi deneyin.",
- "resourceLauncherSearchFirstTitle": "Aranacak ya da Göz Atılacak Filtreler",
- "resourceLauncherSearchFirstDescription": "Birçok kaynağa erişiminiz var. İhtiyacınız olanı bulmak için site veya etiketle arayın veya filtreleyin.",
- "resourceLauncherSiteGroupingDisabled": "Bu ölçekte site gruplama mevcut değil. Daha küçük bir seti gruplamak için siteye göre filtreleyin.",
- "resourceLauncherLabelGroupingDisabled": "Bu ölçekte etiket gruplama mevcut değil.",
- "resourceLauncherCompactModeHint": "Daha hızlı göz atmak için basitleştirilmiş bir liste gösteriliyor. Sonuçları daraltmak için arayın veya filtreler kullanın.",
- "resourceLauncherCompactGroupingHint": "Gruplama etkinleştirmek için site veya etiket filtreleri uygulayın.",
- "resourceLauncherCopiedToClipboard": "Panoya kopyalandı",
- "resourceLauncherCopiedAccessDescription": "Kaynağa erişim panonuza kopyalandı.",
- "resourceLauncherViewNamePlaceholder": "Görünüm adı",
- "resourceLauncherViewNameLabel": "Görünüm Adı",
- "resourceLauncherViewSaved": "Görünüm kaydedildi",
- "resourceLauncherViewSavedDescription": "Başlatıcı görünümünüz kaydedildi.",
- "resourceLauncherViewSaveFailed": "Görünüm kaydedilemedi",
- "resourceLauncherViewSaveFailedDescription": "Başlatıcı görünümü kaydedilemedi. Lütfen yeniden deneyin.",
- "resourceLauncherViewDeleted": "Görünüm silindi",
- "resourceLauncherViewDeletedDescription": "Başlatıcı görünüm silindi.",
- "resourceLauncherViewDeleteFailed": "Görünüm silinemedi",
- "resourceLauncherViewDeleteFailedDescription": "Başlatıcı görünümü silinemedi. Lütfen tekrar deneyin.",
"memberPortalPrevious": "Önceki",
"memberPortalNext": "Sonraki",
"httpSettings": "HTTP Ayarları",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BAŞLANGIÇ OPENSSH ÖZEL ANAHTARI-----",
"sshPrivateKeyRequired": "Özel anahtar gereklidir",
"vncTitle": "VNC",
- "vncSignInDescription": "Bağlanmak için VNC kimlik bilgilerinizi girin",
- "vncUsernameOptional": "Kullanıcı Adı (isteğe bağlı)",
+ "vncSignInDescription": "Bağlanmak için VNC parolanızı girin",
"vncPasswordOptional": "Parola (isteğe bağlı)",
"vncNoResourceTarget": "Kaynak hedefi mevcut değil",
"vncFailedToLoadNovnc": "NoVNC yüklenemedi",
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 3d34fef2e..e25c82e52 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -17,7 +17,7 @@
"componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。",
"componentsErrorNoMember": "您目前不是任何组织的成员。",
"welcome": "欢迎使用 Pangolin",
- "welcomeTo": "欢迎使用",
+ "welcomeTo": "欢迎来到",
"componentsCreateOrg": "创建组织",
"componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。",
"componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。",
@@ -35,7 +35,7 @@
"trialDaysRemaining": "{count, plural, other {# 天剩余}}",
"trialDaysLeftShort": "试用期剩余 {days} 天",
"trialGoToBilling": "转到账单页面",
- "subscriptionViolationViewBilling": "查看账单",
+ "subscriptionViolationViewBilling": "查看计费",
"componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。",
"componentsSupporterMessage": "感谢您的支持!您现在是 Pangolin 的 {tier} 用户。",
"inviteErrorNotValid": "很抱歉,但看起来你试图访问的邀请尚未被接受或不再有效。",
@@ -58,27 +58,21 @@
"name": "名称",
"online": "在线",
"offline": "离线的",
- "site": "节点",
+ "site": "站点",
"dataIn": "数据输入",
"dataOut": "数据输出",
"connectionType": "连接类型",
"tunnelType": "隧道类型",
"local": "本地的",
"edit": "编辑",
- "siteConfirmDelete": "确认删除节点",
- "siteConfirmDeleteAndResources": "确认删除站点及资源",
- "siteDelete": "删除节点",
- "siteDeleteAndResources": "删除站点及资源",
- "siteMessageRemove": "一旦移除,节点将无法访问。与节点相关的所有目标也将被移除。",
- "siteMessageRemoveAndResources": "这将永久删除与该站点关联的所有公共和私人资源,即使资源也与其他站点相关联。",
- "siteQuestionRemove": "您确定要从组织中删除该节点吗?",
- "siteQuestionRemoveAndResources": "您确定要删除此站点及所有关联资源吗?",
- "sitesTableDeleteSite": "删除站点",
- "sitesTableDeleteSiteAndResources": "删除站点及资源",
+ "siteConfirmDelete": "确认删除站点",
+ "siteDelete": "删除站点",
+ "siteMessageRemove": "一旦移除,站点将无法访问。与站点相关的所有目标也将被移除。",
+ "siteQuestionRemove": "您确定要从组织中删除该站点吗?",
"siteManageSites": "管理站点",
"siteDescription": "创建和管理站点,启用与私人网络的连接",
"sitesBannerTitle": "连接任何网络",
- "sitesBannerDescription": "站点是到远程网络的连接,使 Pangolin 能够向任何位置的用户提提供公共或私有的资源访问。你可以在任何能够运行二进制文件或容器的地方安装站点网络连接器(Newt),以建立连接。",
+ "sitesBannerDescription": "站点是连接到远程网络的链接,允许Pangolin为用户提供资源访问,无论是公共还是私人。可以在任何可以运行二进制文件或容器的地方安装站点网络连接器(Newt)以建立连接。",
"sitesBannerButtonText": "安装站点",
"approvalsBannerTitle": "批准或拒绝设备访问",
"approvalsBannerDescription": "审核、批准或拒绝用户的设备访问请求。 当需要设备批准时,用户必须先获得管理员批准,然后他们的设备才能连接到您的组织资源。",
@@ -123,16 +117,6 @@
"siteUpdated": "站点已更新",
"siteUpdatedDescription": "网站已更新。",
"siteGeneralDescription": "配置此站点的常规设置",
- "siteRestartTitle": "重启站点",
- "siteRestartDescription": "重启此站点的WireGuard隧道。此操作将暂时中断连接。",
- "siteRestartBody": "如果站点隧道无法正常工作,并且您希望在不重启主机的情况下强制重新连接,请使用此选项。",
- "siteRestartButton": "重启站点",
- "siteRestartDialogMessage": "确定要重启{name}的WireGuard隧道吗?站点将暂时断开连接。",
- "siteRestartWarning": "隧道重启时,站点将暂时断开连接。",
- "siteRestarted": "站点已重启",
- "siteRestartedDescription": "WireGuard隧道已重启。",
- "siteErrorRestart": "重启站点失败",
- "siteErrorRestartDescription": "重启站点时发生错误。",
"siteSettingDescription": "配置站点设置",
"siteResourcesTab": "资源",
"siteResourcesNoneOnSite": "此站点尚无公开或私人资源。",
@@ -150,7 +134,7 @@
"siteResourcesHowToAccess": "如何访问",
"siteResourcesTargetsOnSite": "此站点上的目标",
"siteSetting": "{siteName} 设置",
- "siteNewtTunnel": "新节点 (推荐)",
+ "siteNewtTunnel": "新站点 (推荐)",
"siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。",
"siteWg": "基本 WireGuard",
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
@@ -159,23 +143,23 @@
"siteLocalDescriptionSaas": "仅本地资源。没有隧道。仅在远程节点上可用。",
"siteSeeAll": "查看所有站点",
"siteTunnelDescription": "确定如何连接到站点",
- "siteNewtCredentials": "凭证",
- "siteNewtCredentialsDescription": "节点如何与服务器进行身份验证",
+ "siteNewtCredentials": "全权证书",
+ "siteNewtCredentialsDescription": "站点如何通过服务器进行身份验证",
"remoteNodeCredentialsDescription": "这是远程节点如何与服务器进行身份验证",
"siteCredentialsSave": "保存证书",
"siteCredentialsSaveDescription": "您只能看到一次。请确保将其复制并保存到一个安全的地方。",
"siteInfo": "站点信息",
"status": "状态",
- "shareTitle": "管理共享链接",
+ "shareTitle": "管理可共享链接",
"shareDescription": "创建可共享的链接,允许临时或永久访问代理资源",
- "shareSearch": "搜索共享链接……",
- "shareCreate": "创建共享链接",
+ "shareSearch": "搜索可共享链接……",
+ "shareCreate": "创建可共享链接",
"shareErrorDelete": "删除链接失败",
"shareErrorDeleteMessage": "删除链接时出错",
"shareDeleted": "链接已删除",
"shareDeletedDescription": "链接已删除",
- "shareDelete": "删除共享链接",
- "shareDeleteConfirm": "确认删除共享链接",
+ "shareDelete": "删除可共享链接",
+ "shareDeleteConfirm": "确认删除可共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
@@ -220,11 +204,11 @@
"proxyResourceTitle": "管理公共资源",
"proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源",
"publicResourcesBannerTitle": "基于 Web 的公共访问",
- "publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可供互联网上的任何人通过 Web 浏览器访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
+ "publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可以通过网络浏览器在互联网上的任何人访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
"clientResourceTitle": "管理私有资源",
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
- "privateResourcesBannerTitle": "零信任私有访问",
- "privateResourcesBannerDescription": "私有资源采用零信任安全机制,确保只有获得明确授权的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
+ "privateResourcesBannerTitle": "零信任的私人访问",
+ "privateResourcesBannerDescription": "私人资源使用零信任安全性,确保只允许明确授予的用户和机器访问资源。可以连接用户设备或机器客户端,通过安全的虚拟专用网络访问这些资源。",
"resourcesSearch": "搜索资源...",
"resourceAdd": "添加资源",
"resourceErrorDelte": "删除资源时出错",
@@ -266,8 +250,6 @@
"resourceRawDescriptionCloud": "正在使用端口号使用 TCP/UDP 代理请求。需要站点连接到远程节点。",
"resourceCreate": "创建资源",
"resourceCreateDescription": "按照下面的步骤创建新资源",
- "resourcePublicCreate": "创建公共资源",
- "resourcePublicCreateDescription": "按照以下步骤创建一个可通过网页浏览器访问的新公共资源",
"resourceCreateGeneralDescription": "配置基本资源设置,包括名称和类型",
"resourceSeeAll": "查看所有资源",
"resourceCreateGeneral": "概览",
@@ -345,7 +327,7 @@
"passToAuth": "传递至认证",
"orgSettingsDescription": "配置组织设置",
"orgGeneralSettings": "组织设置",
- "orgGeneralSettingsDescription": "管理组织的详细信息和配置",
+ "orgGeneralSettingsDescription": "管理机构的详细信息和配置",
"saveGeneralSettings": "保存常规设置",
"saveSettings": "保存设置",
"orgDangerZone": "危险区域",
@@ -399,7 +381,7 @@
"accessApprovalsDescription": "查看和管理待审批的组织访问权限",
"description": "描述",
"inviteTitle": "打开邀请",
- "inviteDescription": "管理其他用户加入组织的邀请",
+ "inviteDescription": "管理其他用户加入机构的邀请",
"inviteSearch": "搜索邀请...",
"minutes": "分钟",
"hours": "小时",
@@ -443,24 +425,24 @@
"apiKeysDelete": "删除 API 密钥",
"apiKeysManage": "管理 API 密钥",
"apiKeysDescription": "API 密钥用于认证集成 API",
- "provisioningKeysTitle": "预配密钥",
- "provisioningKeysManage": "管理预配密钥",
+ "provisioningKeysTitle": "置备密钥",
+ "provisioningKeysManage": "管理置备键",
"provisioningKeysDescription": "置备密钥用于验证您组织的自动站点配置。",
- "provisioningManage": "预配",
- "provisioningDescription": "管理预配密钥,并审核待批准的站点。",
- "pendingSites": "待审批站点",
+ "provisioningManage": "置备中",
+ "provisioningDescription": "管理预配键和审查等待批准的站点。",
+ "pendingSites": "待定站点",
"siteApproveSuccess": "站点批准成功",
"siteApproveError": "批准站点出错",
"provisioningKeys": "置备键",
"searchProvisioningKeys": "搜索配备密钥...",
- "provisioningKeysAdd": "生成预配密钥",
+ "provisioningKeysAdd": "生成置备键",
"provisioningKeysErrorDelete": "删除预配键时出错",
"provisioningKeysErrorDeleteMessage": "删除预配键时出错",
"provisioningKeysQuestionRemove": "您确定要从组织中删除此预配键吗?",
"provisioningKeysMessageRemove": "一旦移除,密钥不能再用于站点预配。",
"provisioningKeysDeleteConfirm": "确认删除置备键",
"provisioningKeysDelete": "删除置备键",
- "provisioningKeysCreate": "生成预配密钥",
+ "provisioningKeysCreate": "生成置备键",
"provisioningKeysCreateDescription": "为组织生成一个新的预置密钥",
"provisioningKeysSeeAll": "查看所有预配键",
"provisioningKeysSave": "保存预配键",
@@ -480,16 +462,16 @@
"provisioningKeysNeverUsed": "永不过期",
"provisioningKeysEdit": "编辑置备键",
"provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。",
- "provisioningKeysApproveNewSites": "批准新节点",
- "provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的节点。",
+ "provisioningKeysApproveNewSites": "批准新站点",
+ "provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的站点。",
"provisioningKeysUpdateError": "更新预配键时出错",
"provisioningKeysUpdated": "置备密钥已更新",
"provisioningKeysUpdatedDescription": "您的更改已保存。",
- "provisioningKeysBannerTitle": "站点预配密钥",
- "provisioningKeysBannerDescription": "生成预配密钥,并将其与 Newt 连接器配合使用,即可在首次启动时自动创建站点,无需为每个站点单独配置凭据。",
+ "provisioningKeysBannerTitle": "站点置备密钥",
+ "provisioningKeysBannerDescription": "生成一个供应密钥,并将其与 Newt 连接器一起使用,以在首次启动时自动创建站点 - 无需为每个站点设置单独的凭据。",
"provisioningKeysBannerButtonText": "了解更多",
- "pendingSitesBannerTitle": "待审批站点",
- "pendingSitesBannerDescription": "使用预配密钥连接的网站会在这里以供审核。",
+ "pendingSitesBannerTitle": "待定站点",
+ "pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。",
"pendingSitesBannerButtonText": "了解更多",
"apiKeysSettings": "{apiKeyName} 设置",
"userTitle": "管理所有用户",
@@ -615,8 +597,7 @@
"idpNameInternal": "内部设置",
"emailInvalid": "无效的电子邮件地址",
"inviteValidityDuration": "请选择持续时间",
- "accessRoleSelectPlease": "用户必须至少属于一个角色。",
- "accessRoleRequired": "需要角色",
+ "accessRoleSelectPlease": "请选择一个角色",
"removeOwnAdminRoleConfirmTitle": "移除你的管理员权限?",
"removeOwnAdminRoleConfirmDescription": "保存后,你将不再拥有该组织的管理员权限。如果需要,其他管理员可以恢复访问。",
"removeOwnAdminRoleConfirmButton": "移除我的管理员访问权限",
@@ -867,8 +848,8 @@
"policyAuthHeaderAuthTitle": "基础标题认证",
"policyAuthHeaderAuthDescription": "在每次请求上验证自定义 HTTP 标头名称和值",
"policyAuthHeaderAuthSummary": "标头已配置",
- "policyAuthHeaderName": "用户名",
- "policyAuthHeaderValue": "密码",
+ "policyAuthHeaderName": "标头名称",
+ "policyAuthHeaderValue": "预期值",
"policyAuthSetPasscode": "设定密码",
"policyAuthSetPincode": "设置 PIN 码",
"policyAuthSetEmailWhitelist": "设置电子邮件白名单",
@@ -902,11 +883,11 @@
"resourcesErrorUpdateDescription": "更新资源时出错",
"access": "访问权限",
"accessControl": "访问控制",
- "shareLink": "{resource} 的共享链接",
+ "shareLink": "{resource} 可共享链接",
"resourceSelect": "选择资源",
- "shareLinks": "共享链接",
+ "shareLinks": "可共享链接",
"share": "分享链接",
- "shareDescription2": "创建资源的共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
+ "shareDescription2": "创建资源的可共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
"shareEasyCreate": "轻松创建和分享",
"shareConfigurableExpirationDuration": "可配置的过期时间",
"shareSecureAndRevocable": "安全和可撤销的",
@@ -1078,7 +1059,7 @@
"network": "网络",
"manage": "管理",
"sitesNotFound": "未找到站点。",
- "pangolinServerAdmin": "服务器管理 - Pangolin",
+ "pangolinServerAdmin": "服务器管理员 - Pangolin",
"licenseTierProfessional": "专业许可证",
"licenseTierEnterprise": "企业许可证",
"licenseTierPersonal": "个人许可证",
@@ -1385,7 +1366,7 @@
"supportKeyBuy": "购买支持者密钥",
"logoutError": "注销错误",
"signingAs": "登录为",
- "serverAdmin": "服务器管理",
+ "serverAdmin": "服务器管理员",
"managedSelfhosted": "托管自托管",
"otpEnable": "启用双因子认证",
"otpDisable": "禁用双因子认证",
@@ -1414,7 +1395,6 @@
"actionApplyBlueprint": "应用蓝图",
"actionListBlueprints": "列表蓝图",
"actionGetBlueprint": "获取蓝图",
- "actionCreateOrgWideLauncherView": "创建组织范围的启动器视图",
"setupToken": "设置令牌",
"setupTokenDescription": "从服务器控制台输入设置令牌。",
"setupTokenRequired": "需要设置令牌",
@@ -1434,15 +1414,6 @@
"actionSetResourcePincode": "设置资源粉码",
"actionSetResourceEmailWhitelist": "设置资源电子邮件白名单",
"actionGetResourceEmailWhitelist": "获取资源电子邮件白名单",
- "actionGetResourcePolicy": "获取资源策略",
- "actionUpdateResourcePolicy": "更新资源策略",
- "actionSetResourcePolicyUsers": "设置资源策略用户",
- "actionSetResourcePolicyRoles": "设置资源策略角色",
- "actionSetResourcePolicyPassword": "设置资源策略密码",
- "actionSetResourcePolicyPincode": "设置资源策略密码",
- "actionSetResourcePolicyHeaderAuth": "设置资源策略头部身份验证",
- "actionSetResourcePolicyWhitelist": "设置资源策略邮件白名单",
- "actionSetResourcePolicyRules": "设置资源策略规则",
"actionCreateTarget": "创建目标",
"actionDeleteTarget": "删除目标",
"actionGetTarget": "获取目标",
@@ -1462,7 +1433,6 @@
"actionGenerateAccessToken": "生成访问令牌",
"actionDeleteAccessToken": "删除访问令牌",
"actionListAccessTokens": "访问令牌",
- "actionCreateResourceSessionToken": "创建资源会话令牌",
"actionCreateResourceRule": "创建资源规则",
"actionDeleteResourceRule": "删除资源规则",
"actionListResourceRules": "列出资源规则",
@@ -1520,30 +1490,6 @@
"navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单",
"navbarDocsLink": "文件",
- "commandPaletteTitle": "命令面板",
- "commandPaletteDescription": "搜索页、组织、资源和操作",
- "commandPaletteSearchPlaceholder": "搜索页面、资源、操作……",
- "commandPaletteNoResults": "未找到任何结果。",
- "commandPaletteSearching": "搜索中……",
- "commandPaletteNavigation": "导航",
- "commandPaletteOrganizations": "组织",
- "commandPaletteSites": "网站",
- "commandPaletteResources": "资源",
- "commandPaletteUsers": "用户",
- "commandPaletteClients": "机器客户端",
- "commandPaletteActions": "操作",
- "commandPaletteCreateSite": "创建网站",
- "commandPaletteCreateProxyResource": "创建公共资源",
- "commandPaletteCreatePrivateResource": "创建私人资源",
- "commandPaletteCreateUser": "创建用户",
- "commandPaletteCreateApiKey": "创建 API 密钥",
- "commandPaletteCreateMachineClient": "创建机器客户端",
- "commandPaletteCreateAlertRule": "创建警报规则",
- "commandPaletteCreateIdentityProvider": "创建身份提供商",
- "commandPaletteToggleTheme": "切换主题",
- "commandPaletteChooseOrganization": "选择组织",
- "commandPaletteShortcutMac": "⌘K",
- "commandPaletteShortcutWindows": "Ctrl K",
"otpErrorEnable": "无法启用 2FA",
"otpErrorEnableDescription": "启用 2FA 时出错",
"otpSetupCheckCode": "请输入您的6位数字代码",
@@ -1590,8 +1536,8 @@
"sidebarSites": "站点",
"sidebarApprovals": "审批请求",
"sidebarResources": "资源",
- "sidebarProxyResources": "公开资源",
- "sidebarClientResources": "私有资源",
+ "sidebarProxyResources": "公开的",
+ "sidebarClientResources": "非公开的",
"sidebarPolicies": "共享策略",
"sidebarResourcePolicies": "公共资源",
"sidebarAccessControl": "访问控制",
@@ -1601,17 +1547,17 @@
"sidebarAdmin": "管理员",
"sidebarInvitations": "邀请",
"sidebarRoles": "角色",
- "sidebarShareableLinks": "共享链接",
+ "sidebarShareableLinks": "可共享链接",
"sidebarApiKeys": "API密钥",
- "sidebarProvisioning": "预配",
+ "sidebarProvisioning": "置备中",
"sidebarSettings": "设置",
"sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书",
"sidebarClients": "客户端",
"sidebarUserDevices": "用户设备",
- "sidebarMachineClients": "机器身份",
- "sidebarDomains": "域名",
+ "sidebarMachineClients": "机",
+ "sidebarDomains": "域",
"sidebarGeneral": "管理",
"sidebarLogAndAnalytics": "日志与分析",
"sidebarBluePrints": "蓝图",
@@ -1621,45 +1567,6 @@
"sidebarManagement": "管理",
"sidebarBillingAndLicenses": "帐单和许可证",
"sidebarLogsAnalytics": "分析",
- "commandSites": "网站",
- "commandActionModeInfo": "输入 \">\" 打开操作模式",
- "commandResources": "资源",
- "commandProxyResources": "公共资源",
- "commandClientResources": "私有资源",
- "commandClients": "客户端",
- "commandUserDevices": "用户设备",
- "commandMachineClients": "机器客户端",
- "commandDomains": "域",
- "commandRemoteExitNodes": "远程节点",
- "commandTeam": "团队",
- "commandUsers": "用户",
- "commandRoles": "角色",
- "commandInvitations": "邀请",
- "commandPolicies": "共享策略",
- "commandResourcePolicies": "公共资源政策",
- "commandIdentityProviders": "身份提供商",
- "commandApprovals": "审批请求",
- "commandShareableLinks": "可共享链接",
- "commandOrganization": "组织",
- "commandLogsAndAnalytics": "日志与分析",
- "commandLogsAnalytics": "分析",
- "commandLogsRequest": "HTTP 请求日志",
- "commandLogsAccess": "身份验证日志",
- "commandLogsAction": "管理员操作日志",
- "commandLogsConnection": "网络日志",
- "commandLogsStreaming": "事件流",
- "commandManagement": "管理",
- "commandAlerting": "警报",
- "commandProvisioning": "配置",
- "commandBluePrints": "蓝图",
- "commandApiKeys": "API 密钥",
- "commandBillingAndLicenses": "计费与许可证",
- "commandBilling": "计费",
- "commandEnterpriseLicenses": "许可证",
- "commandSettings": "设置",
- "commandLauncher": "启动器",
- "commandResourceLauncher": "资源启动器",
- "commandSearchResults": "搜索结果",
"alertingTitle": "告警",
"alertingDescription": "定义通知的来源、触发器和操作",
"alertingRules": "告警规则",
@@ -1782,8 +1689,8 @@
"alertingTabHealthChecks": "健康检查",
"alertingRulesBannerTitle": "获取通知",
"alertingRulesBannerDescription": "每条规则都连接要监视的对象(站点、健康检查或资源),触发时间(例如离线或不健康),以及如何通过电子邮件、Webhooks 或集成将通知发送给团队。使用此列表创建、启用和管理这些规则。",
- "alertingHealthChecksBannerTitle": "资源与健康监控",
- "alertingHealthChecksBannerDescription": "通过 HTTP 或 TCP 检查目标状态,并在服务异常或恢复时发送通知。资源中配置的健康检查也会显示在这里。",
+ "alertingHealthChecksBannerTitle": "监视健康和资源",
+ "alertingHealthChecksBannerDescription": "健康检查是您一次定义的 HTTP 或 TCP 监控。然后可以将它们用作告警规则中的来源,以便目标变得正常或不正常时得到通知。资源上的健康检查也会出现在此处。",
"standaloneHcTableTitle": "健康检查",
"standaloneHcSearchPlaceholder": "搜索健康检查…",
"standaloneHcAddButton": "创建健康检查",
@@ -1884,17 +1791,17 @@
"theme": "主题",
"subnetRequired": "子网是必填项",
"initialSetupTitle": "初始服务器设置",
- "initialSetupDescription": "创建初始的管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
+ "initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
"createAdminAccount": "创建管理员帐户",
- "setupErrorCreateAdmin": "创建管理员账户时发生错误。",
+ "setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。",
"certificateStatus": "证书",
"certificateStatusAutoRefreshHint": "状态自动刷新。",
"loading": "加载中",
"loadingEllipsis": "加载中……",
"loadingAnalytics": "加载分析",
"restart": "重启",
- "domains": "域名",
- "domainsDescription": "创建和管理组织中可用的域名",
+ "domains": "域",
+ "domainsDescription": "创建和管理组织中可用的域",
"domainsSearch": "搜索域...",
"domainAdd": "添加域",
"domainAddDescription": "注册一个新域名到组织",
@@ -1991,9 +1898,6 @@
"billingDomains": "域",
"billingOrganizations": "球队",
"billingRemoteExitNodes": "远程节点",
- "billingPublicResources": "公共资源",
- "billingPrivateResources": "私有资源",
- "billingMachineClients": "机器客户端",
"billingNoLimitConfigured": "未配置限制",
"billingEstimatedPeriod": "估计结算周期",
"billingIncludedUsage": "包含的使用量",
@@ -2022,9 +1926,6 @@
"billingUsersInfo": "您可以使用多少用户",
"billingDomainInfo": "您可以使用多少域",
"billingRemoteExitNodesInfo": "您可以使用多少远程节点",
- "billingPublicResourcesInfo": "您可以使用多少公共资源",
- "billingPrivateResourcesInfo": "您可以使用多少私有资源",
- "billingMachineClientsInfo": "您可以使用多少机器客户端",
"billingLicenseKeys": "许可证密钥",
"billingLicenseKeysDescription": "管理您的许可证密钥订阅",
"billingLicenseSubscription": "许可订阅",
@@ -2170,7 +2071,6 @@
"subnetPlaceholder": "子网",
"addressDescription": "客户的内部地址。必须属于组织的子网。",
"selectSites": "选择站点",
- "selectLabels": "选择标签",
"sitesDescription": "客户端将与所选站点进行连接",
"clientInstallOlm": "安装 Olm",
"clientInstallOlmDescription": "在您的系统上运行 Olm",
@@ -2265,12 +2165,12 @@
"sshSudoMode": "Sudo 访问",
"sshSudoModeNone": "无",
"sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
- "sshSudoModeFull": "完整 Sudo 权限",
+ "sshSudoModeFull": "全苏多",
"sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
"sshSudoModeCommands": "命令",
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
"sshSudo": "允许Sudo",
- "sshSudoCommands": "可用 Sudo 命令",
+ "sshSudoCommands": "Sudo 命令",
"sshSudoCommandsDescription": "用户可以使用 sudo 运行的命令列表,以逗号、空格或新行分隔。必须使用绝对路径。",
"sshCreateHomeDir": "创建主目录",
"sshUnixGroups": "Unix 组",
@@ -2283,7 +2183,7 @@
"roleTextImportAppend": "附加到现有",
"roleTextImportMode": "导入模式",
"roleTextImportPreview": "预览",
- "roleTextImportItemCount": "{count, plural, =0 {没有可导入的项目} one {1 个可导入项目} other {# 个可导入项目}}",
+ "roleTextImportItemCount": "{count, plural, =0 {No items to import} one {1 item to import} other {# items to import}}",
"roleTextImportTotalCount": "{existing} 个现有 + {imported} 个导入 = {total} 个总计",
"roleTextImportConfirm": "导入",
"roleTextImportInvalidFile": "不支持的文件类型",
@@ -2335,8 +2235,8 @@
"resourceEditDomain": "编辑域名",
"siteName": "站点名称",
"proxyPort": "端口",
- "resourcesTableProxyResources": "",
- "resourcesTableClientResources": "私有资源",
+ "resourcesTableProxyResources": "公开的",
+ "resourcesTableClientResources": "非公开的",
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
"resourcesTableDestination": "目标",
@@ -2393,19 +2293,11 @@
"createInternalResourceDialogClose": "关闭",
"createInternalResourceDialogCreateClientResource": "创建私有资源",
"createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源只能为连接到组织的客户端访问",
- "privateResourceGeneralDescription": "配置名称、标识符和其他一般资源设置。",
- "privateResourceCreatePageSeeAll": "查看所有私人资源",
- "privateResourceAllowIcmpPing": "允许 ICMP Ping",
- "privateResourceNetworkAccess": "网络访问",
- "privateResourceNetworkAccessDescription": "控制此资源的 TCP/UDP 端口访问以及是否允许 ICMP ping。",
- "hostSettings": "主机设置",
- "cidrSettings": "CIDR 设置",
"createInternalResourceDialogResourceProperties": "资源属性",
"createInternalResourceDialogName": "名称",
"createInternalResourceDialogSite": "站点",
"selectSite": "选择站点...",
"multiSitesSelectorSitesCount": "{count, plural, other {# 个网站}}",
- "labelsSelectorLabelsCount": "{count, plural, other {# 标签}}",
"noSitesFound": "未找到站点。",
"createInternalResourceDialogProtocol": "协议",
"createInternalResourceDialogTcp": "TCP",
@@ -2446,7 +2338,6 @@
"createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
- "internalResourceAliasLocalWarning": "以 .local 结尾的别名可能会因某些网络上的 mDNS 而导致解析问题。",
"internalResourceDownstreamSchemeRequired": "HTTP 资源需要方案",
"internalResourceHttpPortRequired": "HTTP 资源需要目的端口",
"siteConfiguration": "配置",
@@ -2480,21 +2371,6 @@
"sidebarRemoteExitNodes": "远程节点",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "密钥",
- "remoteExitNodeNetworkingTitle": "网络设置",
- "remoteExitNodeNetworkingDescription": "配置此远程出口节点如何路由流量以及哪些站点优先通过其连接。高级功能可用于回程网络配置。",
- "remoteExitNodeNetworkingSave": "保存设置",
- "remoteExitNodeNetworkingSaveSuccessTitle": "网络设置已保存",
- "remoteExitNodeNetworkingSaveSuccessDescription": "网络设置已成功更新。",
- "remoteExitNodeNetworkingSaveError": "保存网络设置失败",
- "remoteExitNodeNetworkingSubnetsTitle": "远程子网",
- "remoteExitNodeNetworkingSubnetsDescription": "定义此远程出口节点将路由流量的CIDR范围。输入有效的CIDR(例如10.0.0.0/8)并按Enter键添加。",
- "remoteExitNodeNetworkingSubnetsPlaceholder": "添加CIDR范围(例如10.0.0.0/8)",
- "remoteExitNodeNetworkingSubnetsLoadError": "无法加载子网",
- "remoteExitNodeNetworkingLabelsTitle": "首选标签",
- "remoteExitNodeNetworkingLabelsDescription": "拥有这些标签的站点将强制通过此远程出口节点连接。",
- "remoteExitNodeNetworkingLabelsButtonText": "选择标签……",
- "remoteExitNodeNetworkingLabelsSearchPlaceholder": "搜索标签……",
- "remoteExitNodeNetworkingLabelsLoadError": "无法加载标签",
"remoteExitNodeCreate": {
"title": "创建远程节点",
"description": "创建一个新的自托管远程中继和代理服务器节点",
@@ -2548,7 +2424,6 @@
"noRemoteExitNodesAvailableDescription": "此组织没有可用的节点。首先创建一个节点来使用本地站点。",
"exitNode": "出口节点",
"country": "国家",
- "countryIsNot": "不在国家",
"rulesMatchCountry": "当前基于源 IP",
"region": "地区",
"selectRegion": "选择区域",
@@ -2674,7 +2549,6 @@
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
"subnet": "子网",
- "utilitySubnet": "实用程序子网",
"subnetDescription": "此组织网络配置的子网。",
"customDomain": "自定义域",
"authPage": "身份验证页面",
@@ -2758,9 +2632,6 @@
"twoFactorSetupRequired": "需要设置双因素身份验证。 请通过 {dashboardUrl}/auth/login 再次登录以完成此步骤。 然后,回到这里。",
"additionalSecurityRequired": "需要额外的安全",
"organizationRequiresAdditionalSteps": "这个组织需要额外的安全步骤才能访问资源。",
- "sessionExpired": "会话已过期",
- "sessionExpiredReauthRequired": "根据贵组织的安全策略,您的会话已过期。请重新认证以继续。",
- "reauthenticate": "重新认证",
"completeTheseSteps": "完成这些步骤",
"enableTwoFactorAuthentication": "启用两步验证",
"completeSecuritySteps": "完成安全步骤",
@@ -3054,7 +2925,7 @@
"logRetentionRequestDescription": "保留请求日志的时间",
"logRetentionAccessLabel": "访问日志保留",
"logRetentionAccessDescription": "保留访问日志的时间",
- "logRetentionActionLabel": "审计日志保留",
+ "logRetentionActionLabel": "动作日志保留",
"logRetentionActionDescription": "保留操作日志的时间",
"logRetentionConnectionLabel": "连接日志保留",
"logRetentionConnectionDescription": "保留连接日志的时间",
@@ -3067,11 +2938,11 @@
"logRetentionForever": "永远的",
"logRetentionEndOfFollowingYear": "下一年结束",
"actionLogsDescription": "查看此机构执行的操作历史",
- "accessLogsDescription": "查看此组织资源的访问认证请求",
+ "accessLogsDescription": "查看此机构资源的访问认证请求",
"connectionLogs": "连接日志",
"connectionLogsDescription": "查看此机构隧道的连接日志",
"sidebarLogsConnection": "连接日志",
- "sidebarLogsStreaming": "事件流",
+ "sidebarLogsStreaming": "流流",
"sourceAddress": "源地址",
"destinationAddress": "目的地址",
"duration": "期限",
@@ -3096,7 +2967,6 @@
"orgOrDomainIdMissing": "缺少机构或域 ID",
"loadingDNSRecords": "正在载入DNS记录...",
"olmUpdateAvailableInfo": "有最新版本的 Olm 可用。请更新到最新版本以获取最佳体验。",
- "updateAvailableInfo": "有新版本可用。请更新到最新版本以获得最佳体验。",
"client": "客户端:",
"proxyProtocol": "代理协议设置",
"proxyProtocolDescription": "配置代理协议以保留TCP服务的客户端 IP 地址。",
@@ -3270,7 +3140,7 @@
"editInternalResourceDialogAddUsers": "添加用户",
"editInternalResourceDialogAddClients": "添加客户端",
"editInternalResourceDialogDestinationLabel": "目标",
- "editInternalResourceDialogDestinationDescription": "配置客户端如何访问此资源。",
+ "editInternalResourceDialogDestinationDescription": "指定内部资源的目标地址。根据选择的模式,这可以是主机名、IP地址或CIDR范围。可选的,设置一个内部DNS别名以便于识别。",
"internalResourceFormMultiSiteRoutingHelp": "选择多个站点可以实现高可用性的弹性路由和故障转移。",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "了解更多",
"editInternalResourceDialogPortRestrictionsDescription": "限制访问特定的TCP/UDP端口或允许/阻止所有端口。",
@@ -3663,80 +3533,6 @@
"memberPortalEmailWhitelist": "电子邮件白名单",
"memberPortalResourceDisabled": "资源已禁用",
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
- "resourceLauncherTitle": "资源启动器",
- "resourceSidebarLauncherTitle": "启动器",
- "resourceLauncherDescription": "查看所有可用资源,并从一个中央集中访问它们",
- "resourceLauncherSearchPlaceholder": "搜索您的资源……",
- "resourceLauncherDefaultView": "默认",
- "resourceLauncherSaveView": "保存视图",
- "resourceLauncherSaveToCurrentView": "保存至当前视图",
- "resourceLauncherSaveDefaultPersonal": "保存到我的",
- "resourceLauncherResetView": "重置视图",
- "resourceLauncherResetSystemDefault": "重置为系统默认",
- "resourceLauncherSystemDefaultRestored": "系统默认已恢复",
- "resourceLauncherSystemDefaultRestoredDescription": "已将默认视图重置为原始设置。",
- "resourceLauncherSaveAsNewView": "另存为新视图",
- "resourceLauncherSaveAsNewViewDescription": "为此视图命名,以便保存您当前的过滤器和布局。",
- "resourceLauncherSaveForEveryone": "为所有人保存",
- "resourceLauncherSaveForEveryoneDescription": "与所有组织成员共享此视图。如果未选中,此视图仅对您可见。",
- "resourceLauncherMakePersonal": "创建个人",
- "resourceLauncherFilter": "筛选",
- "resourceLauncherFilterWithCount": "筛选,已应用{count}个",
- "resourceLauncherSort": "排序",
- "resourceLauncherSortAscending": "按升序排序",
- "resourceLauncherSortDescending": "按降序排序",
- "resourceLauncherSettings": "设置",
- "resourceLauncherGroupBy": "按组",
- "resourceLauncherGroupBySite": "站点",
- "resourceLauncherGroupByLabel": "标签",
- "resourceLauncherGroupByNone": "无",
- "resourceLauncherLayout": "布局",
- "resourceLauncherLayoutGrid": "网格",
- "resourceLauncherLayoutList": "列表",
- "resourceLauncherShowLabels": "显示标签",
- "resourceLauncherShowSiteTags": "显示站点标签",
- "resourceLauncherShowRecents": "显示最近使用",
- "resourceLauncherDeleteView": "删除视图",
- "resourceLauncherDeleteViewTitle": "删除视图",
- "resourceLauncherDeleteViewQuestion": "您确定要删除此启动器视图吗?",
- "resourceLauncherDeleteViewConfirm": "删除视图",
- "resourceLauncherViewAsAdmin": "以管理员身份查看",
- "resourceLauncherResourceDetailsDescription": "此资源的连接信息和状态。",
- "resourceLauncherResourceDetails": "资源详情",
- "resourceLauncherAuthMethodsDescription": "此资源启用的身份验证方法。",
- "resourceLauncherPrivateClientRequired": "请在您的设备上连接客户端以私密访问此资源。",
- "resourceLauncherPrivateClientRequiredTitle": "需要客户端连接",
- "resourceLauncherDownloadClient": "下载客户端",
- "resourceLauncherFailedToLoadDetails": "无法加载资源详细信息。您可能已经没有权限访问此资源。",
- "resourceLauncherNoPortRestrictions": "无端口限制",
- "resourceLauncherTcp": "TCP",
- "resourceLauncherUdp": "UDP",
- "resourceLauncherUnlabeled": "未标记",
- "resourceLauncherNoSite": "无站点",
- "resourceLauncherNoResourcesInGroup": "此组中没有资源",
- "resourceLauncherEmptyStateTitle": "没有可用资源",
- "resourceLauncherEmptyStateDescription": "您还没有访问任何资源。请联系您的管理员以请求访问。",
- "resourceLauncherEmptyStateNoResultsTitle": "未找到资源",
- "resourceLauncherEmptyStateNoResultsDescription": "没有资源与您当前的搜索或过滤器匹配。尝试调整它们以找到您想要的内容。",
- "resourceLauncherEmptyStateNoResultsWithQuery": "没有资源匹配\"{query}\"。尝试调整您的搜索或清除过滤器以查看所有资源。",
- "resourceLauncherSearchFirstTitle": "搜索或筛选以浏览",
- "resourceLauncherSearchFirstDescription": "您可以访问许多资源。使用搜索或按站点或标签筛选以查找所需内容。",
- "resourceLauncherSiteGroupingDisabled": "在此规模下无法进行站点分组。通过站点进行筛选以分组更小的集合。",
- "resourceLauncherLabelGroupingDisabled": "在此规模下无法进行标签分组。",
- "resourceLauncherCompactModeHint": "显示简化列表以便快速浏览。使用搜索或筛选器缩小结果范围。",
- "resourceLauncherCompactGroupingHint": "应用站点或标签筛选器以启用分组。",
- "resourceLauncherCopiedToClipboard": "已复制到剪贴板",
- "resourceLauncherCopiedAccessDescription": "资源访问权限已复制到剪贴板。",
- "resourceLauncherViewNamePlaceholder": "查看名称",
- "resourceLauncherViewNameLabel": "查看名称",
- "resourceLauncherViewSaved": "视图已保存",
- "resourceLauncherViewSavedDescription": "您的启动器视图已保存。",
- "resourceLauncherViewSaveFailed": "保存视图失败",
- "resourceLauncherViewSaveFailedDescription": "无法保存启动器视图。请再试一次。",
- "resourceLauncherViewDeleted": "视图已删除",
- "resourceLauncherViewDeletedDescription": "启动器视图已删除。",
- "resourceLauncherViewDeleteFailed": "删除视图失败",
- "resourceLauncherViewDeleteFailedDescription": "无法删除启动器视图。请再试一次。",
"memberPortalPrevious": "上一页",
"memberPortalNext": "下一页",
"httpSettings": "HTTP 设置",
@@ -3772,8 +3568,7 @@
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
"sshPrivateKeyRequired": "需要私钥",
"vncTitle": "VNC",
- "vncSignInDescription": "输入您的VNC凭据以连接",
- "vncUsernameOptional": "用户名(可选)",
+ "vncSignInDescription": "输入您的 VNC 密码以连接",
"vncPasswordOptional": "密码 (可选)",
"vncNoResourceTarget": "没有可用的资源目标",
"vncFailedToLoadNovnc": "加载 noVNC 失败",
diff --git a/messages/zh-TW.json b/messages/zh-TW.json
index c37430567..6eb103d3a 100644
--- a/messages/zh-TW.json
+++ b/messages/zh-TW.json
@@ -1099,7 +1099,6 @@
"actionGenerateAccessToken": "生成訪問令牌",
"actionDeleteAccessToken": "刪除訪問令牌",
"actionListAccessTokens": "訪問令牌",
- "actionCreateResourceSessionToken": "建立資源工作階段權杖",
"actionCreateResourceRule": "創建資源規則",
"actionDeleteResourceRule": "刪除資源規則",
"actionListResourceRules": "列出資源規則",
diff --git a/package-lock.json b/package-lock.json
index 378a1f26c..821e8a95e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@asteasolutions/zod-to-openapi": "8.5.0",
"@aws-sdk/client-s3": "3.1056.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
- "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
+ "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz",
"@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.4.0",
"@monaco-editor/react": "4.7.0",
@@ -70,7 +70,7 @@
"input-otp": "1.4.2",
"ioredis": "5.11.0",
"jmespath": "0.16.0",
- "js-yaml": "4.2.0",
+ "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0",
"maxmind": "5.0.6",
@@ -80,7 +80,7 @@
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
- "nodemailer": "9.0.1",
+ "nodemailer": "8.0.9",
"oslo": "1.2.1",
"pg": "8.21.0",
"posthog-node": "5.35.6",
@@ -142,7 +142,7 @@
"@types/yargs": "17.0.35",
"babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.10",
- "esbuild": "0.28.1",
+ "esbuild": "0.28.0",
"esbuild-node-externals": "1.22.0",
"eslint": "10.4.0",
"eslint-config-next": "16.2.6",
@@ -1124,9 +1124,9 @@
"integrity": "sha512-9o7PkCw9fdvGTPs0hgsUJG10QleGgcdsSCw1ekLpUOlVXtWCuiuPH+0bPDFhLWxqbVA+8pyVhwqdOI+t1T3TNA=="
},
"node_modules/@devolutions/iron-remote-desktop-rdp": {
- "version": "0.0.1",
- "resolved": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
- "integrity": "sha512-sIHllJ6rJfAgClsUFjJfVGFjIxm6ZQRyGfinwCzHYgwnHAMFE+Kvm0YzR9tg811Fhjm5F6gNUSLAll+7Eh0Deg=="
+ "version": "0.0.0",
+ "resolved": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz",
+ "integrity": "sha512-O0YVpOJDwUzekH3N2QKj+48WP+56wI0sj4VmaJkGoW5XgyAj2ONn2k3i+vk17Eavx+Vg6vAg3lwYRAOK4kKIDQ=="
},
"node_modules/@dotenvx/dotenvx": {
"version": "1.69.1",
@@ -1248,9 +1248,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+ "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
@@ -1265,9 +1265,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+ "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
@@ -1282,9 +1282,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+ "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
@@ -1299,9 +1299,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+ "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
@@ -1316,9 +1316,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+ "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
@@ -1333,9 +1333,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+ "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
@@ -1350,9 +1350,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
@@ -1367,9 +1367,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+ "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
@@ -1384,9 +1384,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+ "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
@@ -1401,9 +1401,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+ "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
@@ -1418,9 +1418,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+ "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
@@ -1435,9 +1435,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+ "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
@@ -1452,9 +1452,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+ "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
@@ -1469,9 +1469,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+ "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
@@ -1486,9 +1486,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+ "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
@@ -1503,9 +1503,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+ "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
@@ -1520,9 +1520,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+ "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
@@ -1537,9 +1537,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
@@ -1554,9 +1554,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
@@ -1571,9 +1571,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
@@ -1588,9 +1588,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
@@ -1605,9 +1605,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+ "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
@@ -1622,9 +1622,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+ "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
@@ -1639,9 +1639,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+ "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
@@ -1656,9 +1656,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+ "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
@@ -1673,9 +1673,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+ "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
@@ -2076,6 +2076,9 @@
"cpu": [
"arm"
],
+ "libc": [
+ "glibc"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2092,6 +2095,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2108,6 +2114,9 @@
"cpu": [
"ppc64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2124,6 +2133,9 @@
"cpu": [
"riscv64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2140,6 +2152,9 @@
"cpu": [
"s390x"
],
+ "libc": [
+ "glibc"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2172,6 +2187,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2204,6 +2222,9 @@
"cpu": [
"arm"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2226,6 +2247,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2248,6 +2272,9 @@
"cpu": [
"ppc64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2270,6 +2297,9 @@
"cpu": [
"riscv64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2292,6 +2322,9 @@
"cpu": [
"s390x"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2336,6 +2369,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2628,6 +2664,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2644,6 +2683,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2899,6 +2941,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2915,6 +2960,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3152,6 +3200,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3168,6 +3219,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3528,6 +3582,9 @@
"cpu": [
"arm"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3548,6 +3605,9 @@
"cpu": [
"arm"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3568,6 +3628,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -3588,6 +3651,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -6807,6 +6873,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -6823,6 +6892,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -6839,6 +6911,9 @@
"cpu": [
"ppc64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -6855,6 +6930,9 @@
"cpu": [
"s390x"
],
+ "libc": [
+ "glibc"
+ ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -7122,6 +7200,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -7139,6 +7220,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -7212,72 +7296,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "1.10.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "1.10.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.4",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
- "version": "2.8.1",
- "dev": true,
- "inBundle": true,
- "license": "0BSD",
- "optional": true
- },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
@@ -8430,6 +8448,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -8444,6 +8465,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -8458,6 +8482,9 @@
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -8472,6 +8499,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -8486,6 +8516,9 @@
"riscv64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -8500,6 +8533,9 @@
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -11225,9 +11261,9 @@
]
},
"node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
+ "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -11238,32 +11274,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
+ "@esbuild/aix-ppc64": "0.28.0",
+ "@esbuild/android-arm": "0.28.0",
+ "@esbuild/android-arm64": "0.28.0",
+ "@esbuild/android-x64": "0.28.0",
+ "@esbuild/darwin-arm64": "0.28.0",
+ "@esbuild/darwin-x64": "0.28.0",
+ "@esbuild/freebsd-arm64": "0.28.0",
+ "@esbuild/freebsd-x64": "0.28.0",
+ "@esbuild/linux-arm": "0.28.0",
+ "@esbuild/linux-arm64": "0.28.0",
+ "@esbuild/linux-ia32": "0.28.0",
+ "@esbuild/linux-loong64": "0.28.0",
+ "@esbuild/linux-mips64el": "0.28.0",
+ "@esbuild/linux-ppc64": "0.28.0",
+ "@esbuild/linux-riscv64": "0.28.0",
+ "@esbuild/linux-s390x": "0.28.0",
+ "@esbuild/linux-x64": "0.28.0",
+ "@esbuild/netbsd-arm64": "0.28.0",
+ "@esbuild/netbsd-x64": "0.28.0",
+ "@esbuild/openbsd-arm64": "0.28.0",
+ "@esbuild/openbsd-x64": "0.28.0",
+ "@esbuild/openharmony-arm64": "0.28.0",
+ "@esbuild/sunos-x64": "0.28.0",
+ "@esbuild/win32-arm64": "0.28.0",
+ "@esbuild/win32-ia32": "0.28.0",
+ "@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/esbuild-node-externals": {
@@ -12155,16 +12191,16 @@
}
},
"node_modules/form-data": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
- "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.4",
- "mime-types": "^2.1.35"
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
@@ -12593,9 +12629,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -13389,19 +13425,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
- "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/puzrin"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/nodeca"
- }
- ],
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -13741,6 +13767,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -13762,6 +13791,9 @@
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -14542,9 +14574,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
- "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
+ "version": "8.0.9",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.9.tgz",
+ "integrity": "sha512-5ofa7BUN8+C+Hckh5V2GjeeOGRQBx0CJQA6KxrvuZfC8iU4/q7sLn8XrtEEhJkjV6HdyIiQs7Bba6bTao8JhkA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -14969,6 +15001,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -14985,6 +15020,9 @@
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index 15ef7bda5..a0637aaa5 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
- "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
+ "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz",
"@aws-sdk/client-s3": "3.1056.0",
"@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.4.0",
@@ -93,7 +93,7 @@
"input-otp": "1.4.2",
"ioredis": "5.11.0",
"jmespath": "0.16.0",
- "js-yaml": "4.2.0",
+ "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0",
"maxmind": "5.0.6",
@@ -103,7 +103,7 @@
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
- "nodemailer": "9.0.1",
+ "nodemailer": "8.0.9",
"oslo": "1.2.1",
"pg": "8.21.0",
"posthog-node": "5.35.6",
@@ -165,7 +165,7 @@
"@types/yargs": "17.0.35",
"babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.10",
- "esbuild": "0.28.1",
+ "esbuild": "0.28.0",
"esbuild-node-externals": "1.22.0",
"eslint": "10.4.0",
"eslint-config-next": "16.2.6",
@@ -179,7 +179,7 @@
"typescript-eslint": "8.60.0"
},
"overrides": {
- "esbuild": "0.28.1",
+ "esbuild": "0.28.0",
"dompurify": "3.4.0",
"postcss": "8.5.15"
}
diff --git a/public/screenshots/resource-launcher.png b/public/screenshots/resource-launcher.png
deleted file mode 100644
index 93b803ee6..000000000
Binary files a/public/screenshots/resource-launcher.png and /dev/null differ
diff --git a/server/auth/actions.ts b/server/auth/actions.ts
index 50c98c528..71fb33156 100644
--- a/server/auth/actions.ts
+++ b/server/auth/actions.ts
@@ -21,7 +21,6 @@ export enum ActionsEnum {
getSite = "getSite",
listSites = "listSites",
updateSite = "updateSite",
- restartSite = "restartSite",
resetSiteBandwidth = "resetSiteBandwidth",
reGenerateSecret = "reGenerateSecret",
createResource = "createResource",
@@ -71,7 +70,6 @@ export enum ActionsEnum {
setResourceWhitelist = "setResourceWhitelist",
getResourceWhitelist = "getResourceWhitelist",
generateAccessToken = "generateAccessToken",
- createResourceSessionToken = "createResourceSessionToken",
deleteAcessToken = "deleteAcessToken",
listAccessTokens = "listAccessTokens",
createResourceRule = "createResourceRule",
@@ -180,8 +178,7 @@ export enum ActionsEnum {
setResourcePolicyPincode = "setResourcePolicyPincode",
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
- setResourcePolicyRules = "setResourcePolicyRules",
- createOrgWideLauncherView = "createOrgWideLauncherView"
+ setResourcePolicyRules = "setResourcePolicyRules"
}
export async function checkUserActionPermission(
diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts
index 19875fc68..f6cae441b 100644
--- a/server/auth/sessions/app.ts
+++ b/server/auth/sessions/app.ts
@@ -12,7 +12,7 @@ import {
users
} from "@server/db";
import { db } from "@server/db";
-import { and, eq, inArray, ne } from "drizzle-orm";
+import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config";
import type { RandomReader } from "@oslojs/crypto/random";
import { generateRandomString } from "@oslojs/crypto/random";
@@ -136,45 +136,6 @@ export async function invalidateAllSessions(userId: string): Promise {
}
}
-export async function invalidateAllSessionsExceptCurrent(
- userId: string,
- currentSessionId: string
-): Promise {
- try {
- await db.transaction(async (trx) => {
- const userSessions = await trx
- .select()
- .from(sessions)
- .where(
- and(
- eq(sessions.userId, userId),
- ne(sessions.sessionId, currentSessionId)
- )
- );
-
- if (userSessions.length > 0) {
- await trx.delete(resourceSessions).where(
- inArray(
- resourceSessions.userSessionId,
- userSessions.map((s) => s.sessionId)
- )
- );
- }
-
- await trx
- .delete(sessions)
- .where(
- and(
- eq(sessions.userId, userId),
- ne(sessions.sessionId, currentSessionId)
- )
- );
- });
- } catch (e) {
- logger.error("Failed to invalidate user sessions except current", e);
- }
-}
-
export function serializeSessionCookie(
token: string,
isSecure: boolean,
diff --git a/server/db/countries.ts b/server/db/countries.ts
index c668ca2ae..749f1183f 100644
--- a/server/db/countries.ts
+++ b/server/db/countries.ts
@@ -795,13 +795,10 @@ export const COUNTRIES = [
name: "Serbia",
code: "RS"
},
- // Removed as this is a deprecated ISO country code, not supported anymore
- // Also the individual flags for Serbia & Montenegro are already included in the list
- // more details: https://en.wikipedia.org/wiki/ISO_3166-2:CS
- // {
- // name: "Serbia and Montenegro",
- // code: "CS"
- // },
+ {
+ name: "Serbia and Montenegro",
+ code: "CS"
+ },
{
name: "Seychelles",
code: "SC"
diff --git a/server/db/pg/poolConfig.ts b/server/db/pg/poolConfig.ts
index 2f3f93a62..b893c2159 100644
--- a/server/db/pg/poolConfig.ts
+++ b/server/db/pg/poolConfig.ts
@@ -1,4 +1,3 @@
-import config from "@server/lib/config";
import { Pool, PoolConfig } from "pg";
export function createPoolConfig(
@@ -40,28 +39,6 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void {
`Failed to set statement_timeout on ${label} client: ${err.message}`
);
});
-
- // Disable JIT compilation for this connection. Our hot-path queries
- // (e.g. resource-by-domain lookups) join many tables but only ever
- // return a handful of rows. When planner row estimates drift (e.g.
- // due to autovacuum lag under write-heavy load), Postgres decides
- // these plans are expensive enough to JIT-compile, which can add
- // multiple seconds of pure compilation overhead per query and
- // saturate the connection pool. JIT never pays off for these
- // short-lived OLTP queries, so it's disabled outright rather than
- // relying on statistics staying fresh.
- //
- // Set via a runtime SET command rather than the `options: "-c
- // jit=off"` startup parameter: connections in SaaS mode go through
- // a pooler (e.g. PgBouncer) that rejects arbitrary startup packet
- // options with a protocol_violation (08P01) error.
- if (config.getRawConfig().postgres?.pool.jit_mode == false) {
- client.query("SET jit = off").catch((err: Error) => {
- console.warn(
- `Failed to set jit=off on ${label} client: ${err.message}`
- );
- });
- }
});
}
diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts
index cbe8a4039..229fc9ff0 100644
--- a/server/db/pg/schema/privateSchema.ts
+++ b/server/db/pg/schema/privateSchema.ts
@@ -2,7 +2,6 @@ import {
pgTable,
serial,
varchar,
- unique,
boolean,
integer,
bigint,
@@ -12,7 +11,7 @@ import {
primaryKey,
uniqueIndex
} from "drizzle-orm/pg-core";
-import { InferSelectModel, sql } from "drizzle-orm";
+import { InferSelectModel } from "drizzle-orm";
import {
domains,
orgs,
@@ -20,13 +19,12 @@ import {
roles,
users,
exitNodes,
+ sessions,
+ clients,
resources,
siteResources,
targetHealthCheck,
- sites,
- clients,
- sessions,
- labels
+ sites
} from "./schema";
export const certificates = pgTable("certificates", {
@@ -199,42 +197,6 @@ export const remoteExitNodes = pgTable("remoteExitNode", {
})
});
-export const remoteExitNodeResources = pgTable("remoteExitNodeResources", {
- remoteExitNodeResourceId: serial("remoteExitNodeResourceId").primaryKey(),
- remoteExitNodeId: varchar("remoteExitNodeId")
- .notNull()
- .references(() => remoteExitNodes.remoteExitNodeId, {
- onDelete: "cascade"
- }),
- destination: varchar("destination").notNull() // a cidr range
-});
-
-export const remoteExitNodePreferenceLabels = pgTable(
- // this controls what sites are enforced to connect to this node
- "remoteExitNodePreferenceLabels",
- {
- remoteExitNodePreferenceLabelId: serial(
- "remoteExitNodePreferenceLabelId"
- ).primaryKey(),
- remoteExitNodeId: varchar("remoteExitNodeId")
- .references(() => remoteExitNodes.remoteExitNodeId, {
- onDelete: "cascade"
- })
- .notNull(),
- labelId: integer("labelId")
- .references(() => labels.labelId, {
- onDelete: "cascade"
- })
- .notNull()
- },
- (t) => [
- unique("remote_exit_node_preference_label_uniq").on(
- t.remoteExitNodeId,
- t.labelId
- )
- ]
-);
-
export const remoteExitNodeSessions = pgTable("remoteExitNodeSession", {
sessionId: varchar("id").primaryKey(),
remoteExitNodeId: varchar("remoteExitNodeId")
@@ -245,28 +207,17 @@ export const remoteExitNodeSessions = pgTable("remoteExitNodeSession", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
-export const loginPage = pgTable(
- "loginPage",
- {
- loginPageId: serial("loginPageId").primaryKey(),
- subdomain: varchar("subdomain"),
- fullDomain: varchar("fullDomain"),
- exitNodeId: integer("exitNodeId").references(
- () => exitNodes.exitNodeId,
- {
- onDelete: "set null"
- }
- ),
- domainId: varchar("domainId").references(() => domains.domainId, {
- onDelete: "set null"
- })
- },
- (t) => [
- index("idx_loginpage_fulldomain")
- .on(t.fullDomain)
- .where(sql`${t.fullDomain} IS NOT NULL`)
- ]
-);
+export const loginPage = pgTable("loginPage", {
+ loginPageId: serial("loginPageId").primaryKey(),
+ subdomain: varchar("subdomain"),
+ fullDomain: varchar("fullDomain"),
+ exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
+ onDelete: "set null"
+ }),
+ domainId: varchar("domainId").references(() => domains.domainId, {
+ onDelete: "set null"
+ })
+});
export const loginPageOrg = pgTable("loginPageOrg", {
loginPageId: integer("loginPageId")
diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts
index b207b423b..025bdf923 100644
--- a/server/db/pg/schema/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -1,5 +1,5 @@
import { randomUUID } from "crypto";
-import { InferSelectModel, sql } from "drizzle-orm";
+import { InferSelectModel } from "drizzle-orm";
import {
bigint,
boolean,
@@ -25,8 +25,7 @@ export const domains = pgTable("domains", {
certResolver: varchar("certResolver"),
customCertResolver: varchar("customCertResolver"),
preferWildcardCert: boolean("preferWildcardCert"),
- errorMessage: text("errorMessage"),
- lastCheckedAt: integer("lastCheckedAt")
+ errorMessage: text("errorMessage")
});
export const dnsRecords = pgTable("dnsRecords", {
@@ -83,133 +82,107 @@ export const orgDomains = pgTable("orgDomains", {
.references(() => domains.domainId, { onDelete: "cascade" })
});
-export const sites = pgTable(
- "sites",
- {
- siteId: serial("siteId").primaryKey(),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull(),
- niceId: varchar("niceId").notNull(),
- exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
- onDelete: "set null"
- }),
- name: varchar("name").notNull(),
- pubKey: varchar("pubKey"),
- subnet: varchar("subnet"),
- megabytesIn: real("bytesIn").default(0),
- megabytesOut: real("bytesOut").default(0),
- lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
- type: varchar("type").notNull(), // "newt" or "wireguard"
- online: boolean("online").notNull().default(false),
- lastPing: integer("lastPing"),
- address: varchar("address"),
- endpoint: varchar("endpoint"),
- publicKey: varchar("publicKey"),
- lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
- listenPort: integer("listenPort"),
- dockerSocketEnabled: boolean("dockerSocketEnabled")
- .notNull()
- .default(true),
- autoUpdateEnabled: boolean("autoUpdateEnabled")
- .notNull()
- .default(false),
- autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
- .notNull()
- .default(false),
- status: varchar("status")
- .$type<"pending" | "approved">()
- .default("approved")
- },
- (t) => [
- index("idx_sites_exitnodeid").on(t.exitNodeId),
- index("idx_sites_exitnode_type_siteid").on(
- t.exitNodeId,
- t.type,
- t.siteId
- ),
- index("idx_sites_orgid_niceid").on(t.orgId, t.niceId)
- ]
-);
+export const sites = pgTable("sites", {
+ siteId: serial("siteId").primaryKey(),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
+ onDelete: "cascade"
+ })
+ .notNull(),
+ niceId: varchar("niceId").notNull(),
+ exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
+ onDelete: "set null"
+ }),
+ name: varchar("name").notNull(),
+ pubKey: varchar("pubKey"),
+ subnet: varchar("subnet"),
+ megabytesIn: real("bytesIn").default(0),
+ megabytesOut: real("bytesOut").default(0),
+ lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
+ type: varchar("type").notNull(), // "newt" or "wireguard"
+ online: boolean("online").notNull().default(false),
+ lastPing: integer("lastPing"),
+ address: varchar("address"),
+ endpoint: varchar("endpoint"),
+ publicKey: varchar("publicKey"),
+ lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
+ listenPort: integer("listenPort"),
+ dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
+ autoUpdateEnabled: boolean("autoUpdateEnabled").notNull().default(false),
+ autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
+ .notNull()
+ .default(false),
+ status: varchar("status")
+ .$type<"pending" | "approved">()
+ .default("approved")
+});
-export const resources = pgTable(
- "resources",
- {
- resourceId: serial("resourceId").primaryKey(),
- resourcePolicyId: integer("resourcePolicyId").references(
- () => resourcePolicies.resourcePolicyId,
- { onDelete: "set null" }
- ),
- defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
- () => resourcePolicies.resourcePolicyId,
- {
- onDelete: "restrict"
- }
- ),
- resourceGuid: varchar("resourceGuid", { length: 36 })
- .unique()
- .notNull()
- .$defaultFn(() => randomUUID()),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull(),
- niceId: text("niceId").notNull(),
- name: varchar("name").notNull(),
- subdomain: varchar("subdomain"),
- fullDomain: varchar("fullDomain"),
- domainId: varchar("domainId").references(() => domains.domainId, {
- onDelete: "set null"
- }),
- ssl: boolean("ssl").notNull().default(false),
- blockAccess: boolean("blockAccess").notNull().default(false),
- proxyPort: integer("proxyPort"),
- sso: boolean("sso"),
- emailWhitelistEnabled: boolean("emailWhitelistEnabled"),
- applyRules: boolean("applyRules"),
- enabled: boolean("enabled").notNull().default(true),
- stickySession: boolean("stickySession").notNull().default(false),
- tlsServerName: varchar("tlsServerName"),
- setHostHeader: varchar("setHostHeader"),
- enableProxy: boolean("enableProxy").default(true),
- skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
- onDelete: "set null"
- }),
- headers: text("headers"), // comma-separated list of headers to add to the request
- proxyProtocol: boolean("proxyProtocol").notNull().default(false),
- proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
- maintenanceModeEnabled: boolean("maintenanceModeEnabled")
- .notNull()
- .default(false),
- maintenanceModeType: text("maintenanceModeType", {
- enum: ["forced", "automatic"]
- }).default("forced"), // "forced" = always show, "automatic" = only when down
- maintenanceTitle: text("maintenanceTitle"),
- maintenanceMessage: text("maintenanceMessage"),
- maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
- postAuthPath: text("postAuthPath"),
- health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
- wildcard: boolean("wildcard").notNull().default(false),
- mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
- pamMode: varchar("pamMode", { length: 32 })
- .$type<"passthrough" | "push">()
- .default("passthrough"),
- authDaemonMode: varchar("authDaemonMode", { length: 32 })
- .$type<"site" | "remote" | "native">()
- .default("site"),
- authDaemonPort: integer("authDaemonPort").default(22123)
- },
- (t) => [
- index("idx_resources_fulldomain")
- .on(t.fullDomain)
- .where(sql`${t.fullDomain} IS NOT NULL`),
- index("idx_resources_niceid").on(t.niceId),
- index("idx_resources_orgid_niceid").on(t.orgId, t.niceId)
- ]
-);
+export const resources = pgTable("resources", {
+ resourceId: serial("resourceId").primaryKey(),
+ resourcePolicyId: integer("resourcePolicyId").references(
+ () => resourcePolicies.resourcePolicyId,
+ { onDelete: "set null" }
+ ),
+ defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
+ () => resourcePolicies.resourcePolicyId,
+ {
+ onDelete: "restrict"
+ }
+ ),
+ resourceGuid: varchar("resourceGuid", { length: 36 })
+ .unique()
+ .notNull()
+ .$defaultFn(() => randomUUID()),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
+ onDelete: "cascade"
+ })
+ .notNull(),
+ niceId: text("niceId").notNull(),
+ name: varchar("name").notNull(),
+ subdomain: varchar("subdomain"),
+ fullDomain: varchar("fullDomain"),
+ domainId: varchar("domainId").references(() => domains.domainId, {
+ onDelete: "set null"
+ }),
+ ssl: boolean("ssl").notNull().default(false),
+ blockAccess: boolean("blockAccess").notNull().default(false),
+ proxyPort: integer("proxyPort"),
+ sso: boolean("sso"),
+ emailWhitelistEnabled: boolean("emailWhitelistEnabled"),
+ applyRules: boolean("applyRules"),
+ enabled: boolean("enabled").notNull().default(true),
+ stickySession: boolean("stickySession").notNull().default(false),
+ tlsServerName: varchar("tlsServerName"),
+ setHostHeader: varchar("setHostHeader"),
+ enableProxy: boolean("enableProxy").default(true),
+ skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
+ onDelete: "set null"
+ }),
+ headers: text("headers"), // comma-separated list of headers to add to the request
+ proxyProtocol: boolean("proxyProtocol").notNull().default(false),
+ proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
+ maintenanceModeEnabled: boolean("maintenanceModeEnabled")
+ .notNull()
+ .default(false),
+ maintenanceModeType: text("maintenanceModeType", {
+ enum: ["forced", "automatic"]
+ }).default("forced"), // "forced" = always show, "automatic" = only when down
+ maintenanceTitle: text("maintenanceTitle"),
+ maintenanceMessage: text("maintenanceMessage"),
+ maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
+ postAuthPath: text("postAuthPath"),
+ health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
+ wildcard: boolean("wildcard").notNull().default(false),
+ mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
+ pamMode: varchar("pamMode", { length: 32 })
+ .$type<"passthrough" | "push">()
+ .default("passthrough"),
+ authDaemonMode: varchar("authDaemonMode", { length: 32 })
+ .$type<"site" | "remote" | "native">()
+ .default("site"),
+ authDaemonPort: integer("authDaemonPort").default(22123)
+});
export const labels = pgTable("labels", {
labelId: serial("labelId").primaryKey(),
@@ -222,21 +195,6 @@ export const labels = pgTable("labels", {
.notNull()
});
-export const launcherViews = pgTable("launcherViews", {
- viewId: serial("viewId").primaryKey(),
- orgId: varchar("orgId")
- .notNull()
- .references(() => orgs.orgId, { onDelete: "cascade" }),
- userId: varchar("userId").references(() => users.userId, {
- onDelete: "cascade"
- }),
- name: varchar("name").notNull(),
- config: text("config").notNull(),
- isDefault: boolean("isDefault").notNull().default(false),
- createdAt: varchar("createdAt").notNull(),
- updatedAt: varchar("updatedAt").notNull()
-});
-
export const siteLabels = pgTable(
"siteLabels",
{
@@ -309,84 +267,71 @@ export const clientLabels = pgTable(
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
);
-export const targets = pgTable(
- "targets",
- {
- targetId: serial("targetId").primaryKey(),
- resourceId: integer("resourceId")
- .references(() => resources.resourceId, {
- onDelete: "cascade"
- })
- .notNull(),
- siteId: integer("siteId")
- .references(() => sites.siteId, {
- onDelete: "cascade"
- })
- .notNull(),
- ip: varchar("ip").notNull(),
- method: varchar("method"),
- port: integer("port").notNull(),
- internalPort: integer("internalPort"),
- enabled: boolean("enabled").notNull().default(true),
- path: text("path"),
- pathMatchType: text("pathMatchType"), // exact, prefix, regex
- rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
- rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
- priority: integer("priority").notNull().default(100),
- mode: varchar("mode")
- .$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">()
- .notNull()
- .default("http"),
- authToken: varchar("authToken")
- },
- (t) => [
- index("idx_targets_resourceid_siteid").on(t.resourceId, t.siteId),
- index("idx_targets_site_enabled_priority_target_resource")
- .on(t.siteId, t.priority.desc(), t.targetId, t.resourceId)
- .where(sql`${t.enabled} = true`)
- ]
-);
-
-export const targetHealthCheck = pgTable(
- "targetHealthCheck",
- {
- targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
- targetId: integer("targetId").references(() => targets.targetId, {
+export const targets = pgTable("targets", {
+ targetId: serial("targetId").primaryKey(),
+ resourceId: integer("resourceId")
+ .references(() => resources.resourceId, {
onDelete: "cascade"
- }),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull(),
- siteId: integer("siteId")
- .references(() => sites.siteId, {
- onDelete: "cascade"
- })
- .notNull(),
- name: varchar("name"),
- hcEnabled: boolean("hcEnabled").notNull().default(false),
- hcPath: varchar("hcPath"),
- hcScheme: varchar("hcScheme"),
- hcMode: varchar("hcMode").default("http"),
- hcHostname: varchar("hcHostname"),
- hcPort: integer("hcPort"),
- hcInterval: integer("hcInterval").default(30), // in seconds
- hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
- hcTimeout: integer("hcTimeout").default(5), // in seconds
- hcHeaders: varchar("hcHeaders"),
- hcFollowRedirects: boolean("hcFollowRedirects").default(true),
- hcMethod: varchar("hcMethod").default("GET"),
- hcStatus: integer("hcStatus"), // http code
- hcHealth: text("hcHealth")
- .$type<"unknown" | "healthy" | "unhealthy">()
- .default("unknown"), // "unknown", "healthy", "unhealthy"
- hcTlsServerName: text("hcTlsServerName"),
- hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
- hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
- },
- (t) => [index("idx_targethealthcheck_targetid").on(t.targetId)]
-);
+ })
+ .notNull(),
+ siteId: integer("siteId")
+ .references(() => sites.siteId, {
+ onDelete: "cascade"
+ })
+ .notNull(),
+ ip: varchar("ip").notNull(),
+ method: varchar("method"),
+ port: integer("port").notNull(),
+ internalPort: integer("internalPort"),
+ enabled: boolean("enabled").notNull().default(true),
+ path: text("path"),
+ pathMatchType: text("pathMatchType"), // exact, prefix, regex
+ rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
+ rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
+ priority: integer("priority").notNull().default(100),
+ mode: varchar("mode")
+ .$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">()
+ .notNull()
+ .default("http"),
+ authToken: varchar("authToken")
+});
+
+export const targetHealthCheck = pgTable("targetHealthCheck", {
+ targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
+ targetId: integer("targetId").references(() => targets.targetId, {
+ onDelete: "cascade"
+ }),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
+ onDelete: "cascade"
+ })
+ .notNull(),
+ siteId: integer("siteId")
+ .references(() => sites.siteId, {
+ onDelete: "cascade"
+ })
+ .notNull(),
+ name: varchar("name"),
+ hcEnabled: boolean("hcEnabled").notNull().default(false),
+ hcPath: varchar("hcPath"),
+ hcScheme: varchar("hcScheme"),
+ hcMode: varchar("hcMode").default("http"),
+ hcHostname: varchar("hcHostname"),
+ hcPort: integer("hcPort"),
+ hcInterval: integer("hcInterval").default(30), // in seconds
+ hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
+ hcTimeout: integer("hcTimeout").default(5), // in seconds
+ hcHeaders: varchar("hcHeaders"),
+ hcFollowRedirects: boolean("hcFollowRedirects").default(true),
+ hcMethod: varchar("hcMethod").default("GET"),
+ hcStatus: integer("hcStatus"), // http code
+ hcHealth: text("hcHealth")
+ .$type<"unknown" | "healthy" | "unhealthy">()
+ .default("unknown"), // "unknown", "healthy", "unhealthy"
+ hcTlsServerName: text("hcTlsServerName"),
+ hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
+ hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
+});
export const exitNodes = pgTable("exitNodes", {
exitNodeId: serial("exitNodeId").primaryKey(),
@@ -403,146 +348,101 @@ export const exitNodes = pgTable("exitNodes", {
region: varchar("region")
});
-export const siteResources = pgTable(
- "siteResources",
- {
- // this is for the clients
- siteResourceId: serial("siteResourceId").primaryKey(),
- orgId: varchar("orgId")
- .notNull()
- .references(() => orgs.orgId, { onDelete: "cascade" }),
- networkId: integer("networkId").references(() => networks.networkId, {
- onDelete: "set null"
+export const siteResources = pgTable("siteResources", {
+ // this is for the clients
+ siteResourceId: serial("siteResourceId").primaryKey(),
+ orgId: varchar("orgId")
+ .notNull()
+ .references(() => orgs.orgId, { onDelete: "cascade" }),
+ networkId: integer("networkId").references(() => networks.networkId, {
+ onDelete: "set null"
+ }),
+ defaultNetworkId: integer("defaultNetworkId").references(
+ () => networks.networkId,
+ {
+ onDelete: "restrict"
+ }
+ ),
+ niceId: varchar("niceId").notNull(),
+ name: varchar("name").notNull(),
+ ssl: boolean("ssl").notNull().default(false),
+ mode: varchar("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
+ scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
+ proxyPort: integer("proxyPort"), // only for port mode
+ destinationPort: integer("destinationPort"), // only for port mode
+ destination: varchar("destination"), // ip, cidr, hostname; validate against the mode
+ enabled: boolean("enabled").notNull().default(true),
+ alias: varchar("alias"),
+ aliasAddress: varchar("aliasAddress"),
+ tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
+ udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
+ disableIcmp: boolean("disableIcmp").notNull().default(false),
+ authDaemonPort: integer("authDaemonPort").default(22123),
+ pamMode: varchar("pamMode", { length: 32 })
+ .$type<"passthrough" | "push">()
+ .default("passthrough"),
+ authDaemonMode: varchar("authDaemonMode", { length: 32 })
+ .$type<"site" | "remote" | "native">()
+ .default("site"),
+ domainId: varchar("domainId").references(() => domains.domainId, {
+ onDelete: "set null"
+ }),
+ subdomain: varchar("subdomain"),
+ fullDomain: varchar("fullDomain")
+});
+
+export const networks = pgTable("networks", {
+ networkId: serial("networkId").primaryKey(),
+ niceId: text("niceId"),
+ name: text("name"),
+ scope: varchar("scope")
+ .$type<"global" | "resource">()
+ .notNull()
+ .default("global"),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
+ onDelete: "cascade"
+ })
+ .notNull()
+});
+
+export const siteNetworks = pgTable("siteNetworks", {
+ siteId: integer("siteId")
+ .notNull()
+ .references(() => sites.siteId, {
+ onDelete: "cascade"
}),
- defaultNetworkId: integer("defaultNetworkId").references(
- () => networks.networkId,
- {
- onDelete: "restrict"
- }
- ),
- niceId: varchar("niceId").notNull(),
- name: varchar("name").notNull(),
- ssl: boolean("ssl").notNull().default(false),
- mode: varchar("mode")
- .$type<"host" | "cidr" | "http" | "ssh">()
- .notNull(), // "host" | "cidr" | "http"
- scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
- proxyPort: integer("proxyPort"), // only for port mode
- destinationPort: integer("destinationPort"), // only for port mode
- destination: varchar("destination"), // ip, cidr, hostname; validate against the mode
- enabled: boolean("enabled").notNull().default(true),
- alias: varchar("alias"),
- aliasAddress: varchar("aliasAddress"),
- tcpPortRangeString: varchar("tcpPortRangeString")
- .notNull()
- .default("*"),
- udpPortRangeString: varchar("udpPortRangeString")
- .notNull()
- .default("*"),
- disableIcmp: boolean("disableIcmp").notNull().default(false),
- authDaemonPort: integer("authDaemonPort").default(22123),
- pamMode: varchar("pamMode", { length: 32 })
- .$type<"passthrough" | "push">()
- .default("passthrough"),
- authDaemonMode: varchar("authDaemonMode", { length: 32 })
- .$type<"site" | "remote" | "native">()
- .default("site"),
- domainId: varchar("domainId").references(() => domains.domainId, {
- onDelete: "set null"
- }),
- subdomain: varchar("subdomain"),
- fullDomain: varchar("fullDomain")
- },
- (t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
-);
+ networkId: integer("networkId")
+ .notNull()
+ .references(() => networks.networkId, { onDelete: "cascade" })
+});
-export const networks = pgTable(
- "networks",
- {
- networkId: serial("networkId").primaryKey(),
- niceId: text("niceId"),
- name: text("name"),
- scope: varchar("scope")
- .$type<"global" | "resource">()
- .notNull()
- .default("global"),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull()
- },
- (t) => [index("idx_networks_orgid").on(t.orgId)]
-);
+export const clientSiteResources = pgTable("clientSiteResources", {
+ clientId: integer("clientId")
+ .notNull()
+ .references(() => clients.clientId, { onDelete: "cascade" }),
+ siteResourceId: integer("siteResourceId")
+ .notNull()
+ .references(() => siteResources.siteResourceId, { onDelete: "cascade" })
+});
-export const siteNetworks = pgTable(
- "siteNetworks",
- {
- siteId: integer("siteId")
- .notNull()
- .references(() => sites.siteId, {
- onDelete: "cascade"
- }),
- networkId: integer("networkId")
- .notNull()
- .references(() => networks.networkId, { onDelete: "cascade" })
- },
- (t) => [
- index("idx_sitenetworks_siteid").on(t.siteId),
- index("idx_sitenetworks_networkid").on(t.networkId)
- ]
-);
+export const roleSiteResources = pgTable("roleSiteResources", {
+ roleId: integer("roleId")
+ .notNull()
+ .references(() => roles.roleId, { onDelete: "cascade" }),
+ siteResourceId: integer("siteResourceId")
+ .notNull()
+ .references(() => siteResources.siteResourceId, { onDelete: "cascade" })
+});
-export const clientSiteResources = pgTable(
- "clientSiteResources",
- {
- clientId: integer("clientId")
- .notNull()
- .references(() => clients.clientId, { onDelete: "cascade" }),
- siteResourceId: integer("siteResourceId")
- .notNull()
- .references(() => siteResources.siteResourceId, {
- onDelete: "cascade"
- })
- },
- (t) => [
- index("idx_clientsiteresources_clientid").on(t.clientId),
- index("idx_clientsiteresources_siteresourceid").on(t.siteResourceId)
- ]
-);
-
-export const roleSiteResources = pgTable(
- "roleSiteResources",
- {
- roleId: integer("roleId")
- .notNull()
- .references(() => roles.roleId, { onDelete: "cascade" }),
- siteResourceId: integer("siteResourceId")
- .notNull()
- .references(() => siteResources.siteResourceId, {
- onDelete: "cascade"
- })
- },
- (t) => [index("idx_rolesiteresources_siteresourceid").on(t.siteResourceId)]
-);
-
-export const userSiteResources = pgTable(
- "userSiteResources",
- {
- userId: varchar("userId")
- .notNull()
- .references(() => users.userId, { onDelete: "cascade" }),
- siteResourceId: integer("siteResourceId")
- .notNull()
- .references(() => siteResources.siteResourceId, {
- onDelete: "cascade"
- })
- },
- (t) => [
- index("idx_usersiteresources_userid").on(t.userId),
- index("idx_usersiteresources_siteresourceid").on(t.siteResourceId)
- ]
-);
+export const userSiteResources = pgTable("userSiteResources", {
+ userId: varchar("userId")
+ .notNull()
+ .references(() => users.userId, { onDelete: "cascade" }),
+ siteResourceId: integer("siteResourceId")
+ .notNull()
+ .references(() => siteResources.siteResourceId, { onDelete: "cascade" })
+});
export const users = pgTable("user", {
userId: varchar("id").primaryKey(),
@@ -567,19 +467,15 @@ export const users = pgTable("user", {
locale: varchar("locale")
});
-export const newts = pgTable(
- "newt",
- {
- newtId: varchar("id").primaryKey(),
- secretHash: varchar("secretHash").notNull(),
- dateCreated: varchar("dateCreated").notNull(),
- version: varchar("version"),
- siteId: integer("siteId").references(() => sites.siteId, {
- onDelete: "cascade"
- })
- },
- (t) => [index("idx_newt_siteid").on(t.siteId)]
-);
+export const newts = pgTable("newt", {
+ newtId: varchar("id").primaryKey(),
+ secretHash: varchar("secretHash").notNull(),
+ dateCreated: varchar("dateCreated").notNull(),
+ version: varchar("version"),
+ siteId: integer("siteId").references(() => sites.siteId, {
+ onDelete: "cascade"
+ })
+});
export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
codeId: serial("id").primaryKey(),
@@ -680,49 +576,29 @@ export const userOrgRoles = pgTable(
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
);
-export const roleActions = pgTable(
- "roleActions",
- {
- roleId: integer("roleId")
- .notNull()
- .references(() => roles.roleId, { onDelete: "cascade" }),
- actionId: varchar("actionId")
- .notNull()
- .references(() => actions.actionId, { onDelete: "cascade" }),
- orgId: varchar("orgId")
- .notNull()
- .references(() => orgs.orgId, { onDelete: "cascade" })
- },
- (t) => [
- index("idx_roleActions_roleId_orgId_actionId").on(
- t.roleId,
- t.orgId,
- t.actionId
- )
- ]
-);
+export const roleActions = pgTable("roleActions", {
+ roleId: integer("roleId")
+ .notNull()
+ .references(() => roles.roleId, { onDelete: "cascade" }),
+ actionId: varchar("actionId")
+ .notNull()
+ .references(() => actions.actionId, { onDelete: "cascade" }),
+ orgId: varchar("orgId")
+ .notNull()
+ .references(() => orgs.orgId, { onDelete: "cascade" })
+});
-export const userActions = pgTable(
- "userActions",
- {
- userId: varchar("userId")
- .notNull()
- .references(() => users.userId, { onDelete: "cascade" }),
- actionId: varchar("actionId")
- .notNull()
- .references(() => actions.actionId, { onDelete: "cascade" }),
- orgId: varchar("orgId")
- .notNull()
- .references(() => orgs.orgId, { onDelete: "cascade" })
- },
- (t) => [
- index("idx_userActions_userId_orgId_actionId").on(
- t.userId,
- t.orgId,
- t.actionId
- )
- ]
-);
+export const userActions = pgTable("userActions", {
+ userId: varchar("userId")
+ .notNull()
+ .references(() => users.userId, { onDelete: "cascade" }),
+ actionId: varchar("actionId")
+ .notNull()
+ .references(() => actions.actionId, { onDelete: "cascade" }),
+ orgId: varchar("orgId")
+ .notNull()
+ .references(() => orgs.orgId, { onDelete: "cascade" })
+});
export const roleSites = pgTable("roleSites", {
roleId: integer("roleId")
@@ -999,17 +875,7 @@ export const resourceRules = pgTable("resourceRules", {
enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(),
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
- match: varchar("match")
- .$type<
- | "CIDR"
- | "PATH"
- | "IP"
- | "COUNTRY"
- | "COUNTRY_IS_NOT"
- | "ASN"
- | "REGION"
- >()
- .notNull(), // CIDR, PATH, IP
+ match: varchar("match").notNull(), // CIDR, PATH, IP
value: varchar("value").notNull()
});
@@ -1024,45 +890,33 @@ export const resourcePolicyRules = pgTable("resourcePolicyRules", {
priority: integer("priority").notNull(),
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: varchar("match")
- .$type<
- | "CIDR"
- | "PATH"
- | "IP"
- | "COUNTRY"
- | "COUNTRY_IS_NOT"
- | "ASN"
- | "REGION"
- >()
+ .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">()
.notNull(),
value: varchar("value").notNull()
});
-export const resourcePolicies = pgTable(
- "resourcePolicies",
- {
- resourcePolicyId: serial("resourcePolicyId").primaryKey(),
- sso: boolean("sso").notNull().default(true),
- applyRules: boolean("applyRules").notNull().default(false),
- scope: varchar("scope")
- .$type<"global" | "resource">()
- .notNull()
- .default("global"),
- emailWhitelistEnabled: boolean("emailWhitelistEnabled")
- .notNull()
- .default(false),
- idpId: integer("idpId").references(() => idp.idpId, {
- onDelete: "set null"
- }),
- niceId: text("niceId").notNull(),
- name: varchar("name").notNull(),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull()
- },
- (t) => [index("idx_resourcepolicies_orgid_niceid").on(t.orgId, t.niceId)]
-);
+export const resourcePolicies = pgTable("resourcePolicies", {
+ resourcePolicyId: serial("resourcePolicyId").primaryKey(),
+ sso: boolean("sso").notNull().default(true),
+ applyRules: boolean("applyRules").notNull().default(false),
+ scope: varchar("scope")
+ .$type<"global" | "resource">()
+ .notNull()
+ .default("global"),
+ emailWhitelistEnabled: boolean("emailWhitelistEnabled")
+ .notNull()
+ .default(false),
+ idpId: integer("idpId").references(() => idp.idpId, {
+ onDelete: "set null"
+ }),
+ niceId: text("niceId").notNull(),
+ name: varchar("name").notNull(),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
+ onDelete: "cascade"
+ })
+ .notNull()
+});
export const supporterKey = pgTable("supporterKey", {
keyId: serial("keyId").primaryKey(),
@@ -1150,47 +1004,40 @@ export const idpOrg = pgTable("idpOrg", {
orgMapping: varchar("orgMapping")
});
-export const clients = pgTable(
- "clients",
- {
- clientId: serial("clientId").primaryKey(),
- orgId: varchar("orgId")
- .references(() => orgs.orgId, {
- onDelete: "cascade"
- })
- .notNull(),
- exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
- onDelete: "set null"
- }),
- userId: text("userId").references(() => users.userId, {
- // optionally tied to a user and in this case delete when the user deletes
+export const clients = pgTable("clients", {
+ clientId: serial("clientId").primaryKey(),
+ orgId: varchar("orgId")
+ .references(() => orgs.orgId, {
onDelete: "cascade"
- }),
- niceId: varchar("niceId").notNull(),
- olmId: text("olmId"), // to lock it to a specific olm optionally
- name: varchar("name").notNull(),
- pubKey: varchar("pubKey"),
- subnet: varchar("subnet").notNull(),
- megabytesIn: real("bytesIn"),
- megabytesOut: real("bytesOut"),
- lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
- lastPing: integer("lastPing"),
- type: varchar("type").notNull(), // "olm"
- online: boolean("online").notNull().default(false),
- // endpoint: varchar("endpoint"),
- lastHolePunch: integer("lastHolePunch"),
- maxConnections: integer("maxConnections"),
- archived: boolean("archived").notNull().default(false),
- blocked: boolean("blocked").notNull().default(false),
- approvalState: varchar("approvalState").$type<
- "pending" | "approved" | "denied"
- >()
- },
- (t) => [
- index("idx_clients_userid").on(t.userId),
- index("idx_clients_orgid_niceid").on(t.orgId, t.niceId)
- ]
-);
+ })
+ .notNull(),
+ exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
+ onDelete: "set null"
+ }),
+ userId: text("userId").references(() => users.userId, {
+ // optionally tied to a user and in this case delete when the user deletes
+ onDelete: "cascade"
+ }),
+ niceId: varchar("niceId").notNull(),
+ olmId: text("olmId"), // to lock it to a specific olm optionally
+ name: varchar("name").notNull(),
+ pubKey: varchar("pubKey"),
+ subnet: varchar("subnet").notNull(),
+ megabytesIn: real("bytesIn"),
+ megabytesOut: real("bytesOut"),
+ lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
+ lastPing: integer("lastPing"),
+ type: varchar("type").notNull(), // "olm"
+ online: boolean("online").notNull().default(false),
+ // endpoint: varchar("endpoint"),
+ lastHolePunch: integer("lastHolePunch"),
+ maxConnections: integer("maxConnections"),
+ archived: boolean("archived").notNull().default(false),
+ blocked: boolean("blocked").notNull().default(false),
+ approvalState: varchar("approvalState").$type<
+ "pending" | "approved" | "denied"
+ >()
+});
export const clientSitesAssociationsCache = pgTable(
"clientSitesAssociationsCache",
@@ -1202,11 +1049,7 @@ export const clientSitesAssociationsCache = pgTable(
isJitMode: boolean("isJitMode").notNull().default(false),
endpoint: varchar("endpoint"),
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
- },
- (t) => [
- primaryKey({ columns: [t.clientId, t.siteId] }),
- index("idx_clientsitesassociationscache_siteid").on(t.siteId)
- ]
+ }
);
export const clientSiteResourcesAssociationsCache = pgTable(
@@ -1215,14 +1058,7 @@ export const clientSiteResourcesAssociationsCache = pgTable(
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
.notNull(),
siteResourceId: integer("siteResourceId").notNull()
- },
- (t) => [
- primaryKey({ columns: [t.clientId, t.siteResourceId] }),
- index("idx_clientSiteResourcesAssociationsCache_siteResourceId").on(
- t.siteResourceId,
- t.clientId
- )
- ]
+ }
);
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
@@ -1235,27 +1071,23 @@ export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
collectedAt: integer("collectedAt").notNull()
});
-export const olms = pgTable(
- "olms",
- {
- olmId: varchar("id").primaryKey(),
- secretHash: varchar("secretHash").notNull(),
- dateCreated: varchar("dateCreated").notNull(),
- version: text("version"),
- agent: text("agent"),
- name: varchar("name"),
- clientId: integer("clientId").references(() => clients.clientId, {
- // we will switch this depending on the current org it wants to connect to
- onDelete: "set null"
- }),
- userId: text("userId").references(() => users.userId, {
- // optionally tied to a user and in this case delete when the user deletes
- onDelete: "cascade"
- }),
- archived: boolean("archived").notNull().default(false)
- },
- (t) => [index("idx_olms_clientid").on(t.clientId)]
-);
+export const olms = pgTable("olms", {
+ olmId: varchar("id").primaryKey(),
+ secretHash: varchar("secretHash").notNull(),
+ dateCreated: varchar("dateCreated").notNull(),
+ version: text("version"),
+ agent: text("agent"),
+ name: varchar("name"),
+ clientId: integer("clientId").references(() => clients.clientId, {
+ // we will switch this depending on the current org it wants to connect to
+ onDelete: "set null"
+ }),
+ userId: text("userId").references(() => users.userId, {
+ // optionally tied to a user and in this case delete when the user deletes
+ onDelete: "cascade"
+ }),
+ archived: boolean("archived").notNull().default(false)
+});
export const currentFingerprint = pgTable("currentFingerprint", {
fingerprintId: serial("id").primaryKey(),
@@ -1608,8 +1440,6 @@ export type RoundTripMessageTracker = InferSelectModel<
export type Network = InferSelectModel;
export type StatusHistory = InferSelectModel;
export type Label = InferSelectModel;
-export type LauncherView = InferSelectModel;
export type ResourcePolicy = InferSelectModel;
export type RolePolicy = InferSelectModel;
export type UserPolicy = InferSelectModel;
-export type ResourcePolicyRule = InferSelectModel;
diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts
index a7eee52b7..0e50d1289 100644
--- a/server/db/sqlite/driver.ts
+++ b/server/db/sqlite/driver.ts
@@ -1,5 +1,6 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
+import type BetterSqlite3 from "better-sqlite3";
import * as schema from "./schema/schema";
import path from "path";
import fs from "fs";
@@ -11,31 +12,68 @@ export const exists = checkFileExists(location);
bootstrapVolume();
+/**
+ * Wraps better-sqlite3 Statement to call `finalize()` immediately after
+ * execution, freeing native sqlite3_stmt memory deterministically instead
+ * of waiting for GC. Fixes steady off-heap growth under load (#2120).
+ * WARNING: Finalizes after first execution — incompatible with drizzle's
+ * reusable .prepare() builders. No such usage exists in this codebase.
+ */
+function autoFinalizeStatement(
+ stmt: BetterSqlite3.Statement
+): BetterSqlite3.Statement {
+ const wrapExec = any>(fn: T): T => {
+ return function (this: any, ...args: any[]) {
+ try {
+ return fn.apply(this, args);
+ } finally {
+ try {
+ // finalize() exists on the native Statement at runtime but
+ // is missing from @types/better-sqlite3.
+ (stmt as any).finalize();
+ } catch {
+ // Already finalized — harmless
+ }
+ }
+ } as unknown as T;
+ };
+
+ stmt.run = wrapExec(stmt.run);
+ stmt.get = wrapExec(stmt.get);
+ stmt.all = wrapExec(stmt.all);
+
+ return stmt;
+}
+
function createDb() {
const sqlite = new Database(location);
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
// Enable WAL mode — allows concurrent readers + single writer, preventing
// contention across subsystems (verifySession, Traefik, audit, ping).
- // NOTE: journal_mode persists in the DB file once set; unsetting this
- // env var does NOT revert an existing WAL database.
sqlite.pragma("journal_mode = WAL");
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
sqlite.pragma("synchronous = NORMAL");
}
- // No busy_timeout pragma: better-sqlite3 already arms
- // sqlite3_busy_timeout(db, 5000) via its default `timeout` option
- // (lib/database.js), so an explicit pragma is redundant.
+ // Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
+ // retry loops that accumulate memory.
+ sqlite.pragma("busy_timeout = 5000");
- // Intentionally NOT setting cache_size or mmap_size: a large page cache plus
- // a multi-hundred-MB mmap region inflate RSS and cause page-cache thrashing
- // on small (~1 GB) instances. Leave SQLite on its conservative defaults.
+ // 64 MB page cache (default 2 MB) — reduces I/O round-trips on large
+ // TraefikConfigManager JOINs that block the event loop.
+ sqlite.pragma("cache_size = -65536");
- // Intentionally NOT wrapping prepare()/statements: better-sqlite3 finalizes
- // sqlite3_stmt in the Statement destructor at GC, and drizzle-orm prepares a
- // fresh statement per query (no statement cache), so statements cannot
- // accumulate. better-sqlite3 11.x exposes no Statement.finalize() at all.
+ // 256 MB memory-mapped I/O — OS serves reads from page cache directly,
+ // reducing event-loop blocking.
+ sqlite.pragma("mmap_size = 268435456");
+
+ // Wrap prepare() so every drizzle-orm statement is auto-finalized after
+ // first use, preventing sqlite3_stmt accumulation between GC cycles.
+ const originalPrepare = sqlite.prepare.bind(sqlite);
+ (sqlite as any).prepare = function autoFinalizePrepare(source: string) {
+ return autoFinalizeStatement(originalPrepare(source));
+ };
return DrizzleSqlite(sqlite, {
schema
diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts
index b75836e29..ae7360780 100644
--- a/server/db/sqlite/schema/privateSchema.ts
+++ b/server/db/sqlite/schema/privateSchema.ts
@@ -12,7 +12,6 @@ import {
clients,
domains,
exitNodes,
- labels,
orgs,
resources,
roles,
@@ -22,6 +21,9 @@ import {
targetHealthCheck,
users
} from "./schema";
+import { serial, varchar } from "drizzle-orm/mysql-core";
+import { pgTable } from "drizzle-orm/pg-core";
+import { bigint } from "zod";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
@@ -193,44 +195,6 @@ export const remoteExitNodes = sqliteTable("remoteExitNode", {
})
});
-export const remoteExitNodeResources = sqliteTable("remoteExitNodeResources", {
- remoteExitNodeResourceId: integer("remoteExitNodeResourceId").primaryKey({
- autoIncrement: true
- }),
- remoteExitNodeId: text("remoteExitNodeId")
- .notNull()
- .references(() => remoteExitNodes.remoteExitNodeId, {
- onDelete: "cascade"
- }),
- destination: text("destination").notNull() // a cidr range
-});
-
-export const remoteExitNodePreferenceLabels = sqliteTable(
- // this controls what sites are enforced to connect to this node
- "remoteExitNodePreferenceLabels",
- {
- remoteExitNodePreferenceLabelId: integer(
- "remoteExitNodePreferenceLabelId"
- ).primaryKey({ autoIncrement: true }),
- remoteExitNodeId: text("remoteExitNodeId")
- .references(() => remoteExitNodes.remoteExitNodeId, {
- onDelete: "cascade"
- })
- .notNull(),
- labelId: integer("labelId")
- .references(() => labels.labelId, {
- onDelete: "cascade"
- })
- .notNull()
- },
- (t) => [
- uniqueIndex("remote_exit_node_preference_label_uniq").on(
- t.remoteExitNodeId,
- t.labelId
- )
- ]
-);
-
export const remoteExitNodeSessions = sqliteTable("remoteExitNodeSession", {
sessionId: text("id").primaryKey(),
remoteExitNodeId: text("remoteExitNodeId")
diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts
index 4ec4916b5..0c4a143f5 100644
--- a/server/db/sqlite/schema/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -20,10 +20,8 @@ export const domains = sqliteTable("domains", {
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
tries: integer("tries").notNull().default(0),
certResolver: text("certResolver"),
- customCertResolver: text("customCertResolver"),
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
- errorMessage: text("errorMessage"),
- lastCheckedAt: integer("lastCheckedAt")
+ errorMessage: text("errorMessage")
});
export const dnsRecords = sqliteTable("dnsRecords", {
@@ -223,23 +221,6 @@ export const labels = sqliteTable("labels", {
.notNull()
});
-export const launcherViews = sqliteTable("launcherViews", {
- viewId: integer("viewId").primaryKey({ autoIncrement: true }),
- orgId: text("orgId")
- .notNull()
- .references(() => orgs.orgId, { onDelete: "cascade" }),
- userId: text("userId").references(() => users.userId, {
- onDelete: "cascade"
- }),
- name: text("name").notNull(),
- config: text("config").notNull(),
- isDefault: integer("isDefault", { mode: "boolean" })
- .notNull()
- .default(false),
- createdAt: text("createdAt").notNull(),
- updatedAt: text("updatedAt").notNull()
-});
-
export const siteLabels = sqliteTable(
"siteLabels",
{
@@ -1224,17 +1205,7 @@ export const resourceRules = sqliteTable("resourceRules", {
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull(),
action: text("action").notNull(), // ACCEPT, DROP, PASS
- match: text("match")
- .$type<
- | "CIDR"
- | "PATH"
- | "IP"
- | "COUNTRY"
- | "COUNTRY_IS_NOT"
- | "ASN"
- | "REGION"
- >()
- .notNull(), // CIDR, PATH, IP
+ match: text("match").notNull(), // CIDR, PATH, IP
value: text("value").notNull()
});
@@ -1281,15 +1252,7 @@ export const resourcePolicyRules = sqliteTable("resourcePolicyRules", {
priority: integer("priority").notNull(),
action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: text("match")
- .$type<
- | "CIDR"
- | "PATH"
- | "IP"
- | "COUNTRY"
- | "COUNTRY_IS_NOT"
- | "ASN"
- | "REGION"
- >()
+ .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">()
.notNull(),
value: text("value").notNull()
});
@@ -1586,7 +1549,6 @@ export type RoundTripMessageTracker = InferSelectModel<
>;
export type StatusHistory = InferSelectModel;
export type Label = InferSelectModel;
-export type LauncherView = InferSelectModel;
export type ResourcePolicy = InferSelectModel;
export type ResourcePolicyPincode = InferSelectModel<
typeof resourcePolicyPincode
diff --git a/server/index.ts b/server/index.ts
index 53b3e9a69..99fd20156 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -24,7 +24,6 @@ import license from "#dynamic/license/license";
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
import { fetchServerIp } from "@server/lib/serverIpService";
-import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
async function startServers() {
await setHostMeta();
@@ -42,7 +41,6 @@ async function startServers() {
initLogCleanupInterval();
initAcmeCertSync();
- startRebuildQueueProcessor();
// Start all servers
const apiServer = createApiServer();
diff --git a/server/lib/billing/features.ts b/server/lib/billing/features.ts
index ffe24a4a3..6063b470f 100644
--- a/server/lib/billing/features.ts
+++ b/server/lib/billing/features.ts
@@ -1,39 +1,28 @@
-export enum LimitId {
+export enum FeatureId {
USERS = "users",
SITES = "sites",
EGRESS_DATA_MB = "egressDataMb",
DOMAINS = "domains",
REMOTE_EXIT_NODES = "remoteExitNodes",
- ORGANIZATIONS = "organizations",
- PUBLIC_RESOURCES = "publicResources",
- PRIVATE_RESOURCES = "privateResources",
- MACHINE_CLIENTS = "machineClients",
+ ORGINIZATIONS = "organizations",
TIER1 = "tier1"
}
-export async function getFeatureDisplayName(
- featureId: LimitId
-): Promise {
+export async function getFeatureDisplayName(featureId: FeatureId): Promise {
switch (featureId) {
- case LimitId.USERS:
+ case FeatureId.USERS:
return "Users";
- case LimitId.SITES:
+ case FeatureId.SITES:
return "Sites";
- case LimitId.EGRESS_DATA_MB:
+ case FeatureId.EGRESS_DATA_MB:
return "Egress Data (MB)";
- case LimitId.DOMAINS:
+ case FeatureId.DOMAINS:
return "Domains";
- case LimitId.REMOTE_EXIT_NODES:
+ case FeatureId.REMOTE_EXIT_NODES:
return "Remote Exit Nodes";
- case LimitId.ORGANIZATIONS:
+ case FeatureId.ORGINIZATIONS:
return "Organizations";
- case LimitId.PUBLIC_RESOURCES:
- return "Public Resources";
- case LimitId.PRIVATE_RESOURCES:
- return "Private Resources";
- case LimitId.MACHINE_CLIENTS:
- return "Machine Clients";
- case LimitId.TIER1:
+ case FeatureId.TIER1:
return "Home Lab";
default:
return featureId;
@@ -41,16 +30,15 @@ export async function getFeatureDisplayName(
}
// this is from the old system
-export const FeatureMeterIds: Partial> = {
- // right now we are not charging for any data
+export const FeatureMeterIds: Partial> = { // right now we are not charging for any data
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
};
-export const FeatureMeterIdsSandbox: Partial> = {
+export const FeatureMeterIdsSandbox: Partial> = {
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
};
-export function getFeatureMeterId(featureId: LimitId): string | undefined {
+export function getFeatureMeterId(featureId: FeatureId): string | undefined {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
@@ -61,20 +49,22 @@ export function getFeatureMeterId(featureId: LimitId): string | undefined {
}
}
-export function getFeatureIdByMetricId(metricId: string): LimitId | undefined {
- return (Object.entries(FeatureMeterIds) as [LimitId, string][]).find(
+export function getFeatureIdByMetricId(
+ metricId: string
+): FeatureId | undefined {
+ return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find(
([_, v]) => v === metricId
)?.[0];
}
-export type FeaturePriceSet = Partial>;
+export type FeaturePriceSet = Partial>;
export const tier1FeaturePriceSet: FeaturePriceSet = {
- [LimitId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
+ [FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
};
export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
- [LimitId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
+ [FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
};
export function getTier1FeaturePriceSet(): FeaturePriceSet {
@@ -89,11 +79,11 @@ export function getTier1FeaturePriceSet(): FeaturePriceSet {
}
export const tier2FeaturePriceSet: FeaturePriceSet = {
- [LimitId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
+ [FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
};
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
- [LimitId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
+ [FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
};
export function getTier2FeaturePriceSet(): FeaturePriceSet {
@@ -108,11 +98,11 @@ export function getTier2FeaturePriceSet(): FeaturePriceSet {
}
export const tier3FeaturePriceSet: FeaturePriceSet = {
- [LimitId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
+ [FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
};
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
- [LimitId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
+ [FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
};
export function getTier3FeaturePriceSet(): FeaturePriceSet {
@@ -126,7 +116,7 @@ export function getTier3FeaturePriceSet(): FeaturePriceSet {
}
}
-export function getFeatureIdByPriceId(priceId: string): LimitId | undefined {
+export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
// Check all feature price sets
const allPriceSets = [
getTier1FeaturePriceSet(),
@@ -135,7 +125,7 @@ export function getFeatureIdByPriceId(priceId: string): LimitId | undefined {
];
for (const priceSet of allPriceSets) {
- const entry = (Object.entries(priceSet) as [LimitId, string][]).find(
+ const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
([_, price]) => price === priceId
);
if (entry) {
diff --git a/server/lib/billing/getLineItems.ts b/server/lib/billing/getLineItems.ts
index 5df5fb8a8..d386e5e96 100644
--- a/server/lib/billing/getLineItems.ts
+++ b/server/lib/billing/getLineItems.ts
@@ -1,19 +1,19 @@
import Stripe from "stripe";
-import { LimitId, FeaturePriceSet } from "./features";
+import { FeatureId, FeaturePriceSet } from "./features";
import { usageService } from "./usageService";
export async function getLineItems(
featurePriceSet: FeaturePriceSet,
- orgId: string
+ orgId: string,
): Promise {
- const users = await usageService.getUsage(orgId, LimitId.USERS);
+ const users = await usageService.getUsage(orgId, FeatureId.USERS);
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
let quantity: number | undefined;
- if (featureId === LimitId.USERS) {
+ if (featureId === FeatureId.USERS) {
quantity = users?.instantaneousValue || 1;
- } else if (featureId === LimitId.TIER1) {
+ } else if (featureId === FeatureId.TIER1) {
quantity = 1;
}
diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts
index 87b1a9c17..e45ae637d 100644
--- a/server/lib/billing/limitSet.ts
+++ b/server/lib/billing/limitSet.ts
@@ -1,82 +1,70 @@
-import { LimitId } from "./features";
+import { FeatureId } from "./features";
export type LimitSet = Partial<{
- [key in LimitId]: {
+ [key in FeatureId]: {
value: number | null; // null indicates no limit
description?: string;
};
}>;
export const freeLimitSet: LimitSet = {
- [LimitId.SITES]: { value: 5, description: "Basic limit" },
- [LimitId.USERS]: { value: 5, description: "Basic limit" },
- [LimitId.DOMAINS]: { value: 5, description: "Basic limit" },
- [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
- [LimitId.ORGANIZATIONS]: { value: 1, description: "Basic limit" },
- [LimitId.PUBLIC_RESOURCES]: { value: 15, description: "Basic limit" },
- [LimitId.PRIVATE_RESOURCES]: { value: 15, description: "Basic limit" },
- [LimitId.MACHINE_CLIENTS]: { value: 5, description: "Basic limit" }
+ [FeatureId.SITES]: { value: 5, description: "Basic limit" },
+ [FeatureId.USERS]: { value: 5, description: "Basic limit" },
+ [FeatureId.DOMAINS]: { value: 5, description: "Basic limit" },
+ [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
+ [FeatureId.ORGINIZATIONS]: { value: 1, description: "Basic limit" },
};
export const tier1LimitSet: LimitSet = {
- [LimitId.USERS]: { value: 7, description: "Home limit" },
- [LimitId.SITES]: { value: 10, description: "Home limit" },
- [LimitId.DOMAINS]: { value: 10, description: "Home limit" },
- [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
- [LimitId.ORGANIZATIONS]: { value: 1, description: "Home limit" },
- [LimitId.PUBLIC_RESOURCES]: { value: 30, description: "Home limit" },
- [LimitId.PRIVATE_RESOURCES]: { value: 30, description: "Home limit" },
- [LimitId.MACHINE_CLIENTS]: { value: 10, description: "Home limit" }
+ [FeatureId.USERS]: { value: 7, description: "Home limit" },
+ [FeatureId.SITES]: { value: 10, description: "Home limit" },
+ [FeatureId.DOMAINS]: { value: 10, description: "Home limit" },
+ [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
+ [FeatureId.ORGINIZATIONS]: { value: 1, description: "Home limit" },
};
export const tier2LimitSet: LimitSet = {
- [LimitId.USERS]: {
+ [FeatureId.USERS]: {
value: 50,
description: "Team limit"
},
- [LimitId.SITES]: {
+ [FeatureId.SITES]: {
value: 50,
description: "Team limit"
},
- [LimitId.DOMAINS]: {
+ [FeatureId.DOMAINS]: {
value: 50,
description: "Team limit"
},
- [LimitId.REMOTE_EXIT_NODES]: {
+ [FeatureId.REMOTE_EXIT_NODES]: {
value: 3,
description: "Team limit"
},
- [LimitId.ORGANIZATIONS]: {
+ [FeatureId.ORGINIZATIONS]: {
value: 1,
description: "Team limit"
- },
- [LimitId.PUBLIC_RESOURCES]: { value: 150, description: "Team limit" },
- [LimitId.PRIVATE_RESOURCES]: { value: 150, description: "Team limit" },
- [LimitId.MACHINE_CLIENTS]: { value: 25, description: "Team limit" }
+ }
};
export const tier3LimitSet: LimitSet = {
- [LimitId.USERS]: {
+ [FeatureId.USERS]: {
value: 250,
description: "Business limit"
},
- [LimitId.SITES]: {
+ [FeatureId.SITES]: {
value: 250,
description: "Business limit"
},
- [LimitId.DOMAINS]: {
+ [FeatureId.DOMAINS]: {
value: 100,
description: "Business limit"
},
- [LimitId.REMOTE_EXIT_NODES]: {
+ [FeatureId.REMOTE_EXIT_NODES]: {
value: 20,
description: "Business limit"
},
- [LimitId.ORGANIZATIONS]: {
+ [FeatureId.ORGINIZATIONS]: {
value: 5,
description: "Business limit"
},
- [LimitId.PUBLIC_RESOURCES]: { value: 750, description: "Business limit" },
- [LimitId.PRIVATE_RESOURCES]: { value: 750, description: "Business limit" },
- [LimitId.MACHINE_CLIENTS]: { value: 100, description: "Business limit" }
};
diff --git a/server/lib/billing/limitsService.ts b/server/lib/billing/limitsService.ts
index e08b0fe14..f364d6e00 100644
--- a/server/lib/billing/limitsService.ts
+++ b/server/lib/billing/limitsService.ts
@@ -1,7 +1,7 @@
import { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet";
-import { LimitId } from "./features";
+import { FeatureId } from "./features";
import logger from "@server/logger";
class LimitService {
@@ -38,7 +38,7 @@ class LimitService {
async getOrgLimit(
orgId: string,
- featureId: LimitId
+ featureId: FeatureId
): Promise {
const limitId = `${orgId}-${featureId}`;
const [limit] = await db
diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts
index 7c0b591ca..f0e6dc95a 100644
--- a/server/lib/billing/tierMatrix.ts
+++ b/server/lib/billing/tierMatrix.ts
@@ -23,6 +23,7 @@ export enum TierFeature {
StandaloneHealthChecks = "standaloneHealthChecks",
AlertingRules = "alertingRules",
WildcardSubdomain = "wildcardSubdomain",
+ Labels = "labels",
NewtAutoUpdate = "newtAutoUpdate",
ResourcePolicies = "resourcePolicies",
AdvancedPublicResources = "advancedPublicResources",
@@ -30,6 +31,7 @@ export enum TierFeature {
}
export const tierMatrix: Record = {
+ [TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts
index 413bdf9a3..9cb24bbeb 100644
--- a/server/lib/billing/usageService.ts
+++ b/server/lib/billing/usageService.ts
@@ -9,10 +9,10 @@ import {
Transaction,
orgs
} from "@server/db";
-import { LimitId, getFeatureMeterId } from "./features";
+import { FeatureId, getFeatureMeterId } from "./features";
import logger from "@server/logger";
import { build } from "@server/build";
-import { regionalCache as cache } from "#dynamic/lib/cache";
+import cache from "#dynamic/lib/cache";
export function noop() {
if (build !== "saas") {
@@ -22,6 +22,7 @@ export function noop() {
}
export class UsageService {
+
constructor() {
if (noop()) {
return;
@@ -37,7 +38,7 @@ export class UsageService {
public async add(
orgId: string,
- featureId: LimitId,
+ featureId: FeatureId,
value: number,
transaction: any = null
): Promise {
@@ -56,10 +57,7 @@ export class UsageService {
try {
let usage;
if (transaction) {
- const orgIdToUse = await this.getBillingOrg(
- orgId,
- transaction
- );
+ const orgIdToUse = await this.getBillingOrg(orgId, transaction);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -114,7 +112,7 @@ export class UsageService {
private async internalAddUsage(
orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount
- featureId: LimitId,
+ featureId: FeatureId,
value: number,
trx: Transaction
): Promise {
@@ -163,7 +161,7 @@ export class UsageService {
async updateCount(
orgId: string,
- featureId: LimitId,
+ featureId: FeatureId,
value?: number,
customerId?: string
): Promise {
@@ -227,7 +225,7 @@ export class UsageService {
private async getCustomerId(
orgId: string,
- featureId: LimitId
+ featureId: FeatureId
): Promise {
const orgIdToUse = await this.getBillingOrg(orgId);
@@ -269,19 +267,18 @@ export class UsageService {
public async getUsage(
orgId: string,
- featureId: LimitId,
+ featureId: FeatureId,
trx: Transaction | typeof db = db
): Promise {
if (noop()) {
return null;
}
- let orgIdToUse = orgId;
+ const orgIdToUse = await this.getBillingOrg(orgId, trx);
+
+ const usageId = `${orgIdToUse}-${featureId}`;
+
try {
- orgIdToUse = await this.getBillingOrg(orgId, trx);
-
- const usageId = `${orgIdToUse}-${featureId}`;
-
const [result] = await trx
.select()
.from(usage)
@@ -341,12 +338,8 @@ export class UsageService {
`Failed to get usage for ${orgIdToUse}/${featureId}:`,
error
);
- if (process.env.NODE_ENV !== "development") {
- throw error;
- }
+ throw error;
}
-
- return null;
}
public async getBillingOrg(
@@ -381,7 +374,7 @@ export class UsageService {
public async checkLimitSet(
orgId: string,
- featureId?: LimitId,
+ featureId?: FeatureId,
usage?: Usage,
trx: Transaction | typeof db = db
): Promise {
@@ -389,13 +382,13 @@ export class UsageService {
return false;
}
+ const orgIdToUse = await this.getBillingOrg(orgId, trx);
+
// This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org
let hasExceededLimits = false;
- let orgIdToUse = orgId;
- try {
- orgIdToUse = await this.getBillingOrg(orgId, trx);
+ try {
let orgLimits: Limit[] = [];
if (featureId) {
// Get all limits set for this organization
@@ -429,7 +422,7 @@ export class UsageService {
} else {
currentUsage = await this.getUsage(
orgIdToUse,
- limit.featureId as LimitId,
+ limit.featureId as FeatureId,
trx
);
}
diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts
index c62dd4735..f2bb9b0c8 100644
--- a/server/lib/blueprints/applyBlueprint.ts
+++ b/server/lib/blueprints/applyBlueprint.ts
@@ -3,12 +3,13 @@ import {
newts,
blueprints,
Blueprint,
+ Site,
siteResources,
roleSiteResources,
userSiteResources,
clientSiteResources
} from "@server/db";
-import { Config, ConfigSchema, isTargetsOnlyResource } from "./types";
+import { Config, ConfigSchema } from "./types";
import {
PublicResourcesResults,
updatePublicResources
@@ -29,17 +30,8 @@ import { updateResourcePolicies } from "./resourcePolicies";
import { BlueprintSource } from "@server/routers/blueprints/types";
import { stringify as stringifyYaml } from "yaml";
import { generateName } from "@server/db/names";
-import {
- handleMessagingForUpdatedSiteResource,
- rebuildClientAssociationsFromSiteResource,
- waitForSiteResourceRebuildIdle
-} from "../rebuildClientAssociations";
-import { build } from "@server/build";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import next from "next";
-import { LimitId } from "../billing";
-import { usageService } from "../billing/usageService";
+import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
+import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
type ApplyBlueprintArgs = {
orgId: string;
@@ -56,39 +48,42 @@ export async function applyBlueprint({
name,
source = "API"
}: ApplyBlueprintArgs): Promise {
+ // Validate the input data
+ const validationResult = ConfigSchema.safeParse(configData);
+ if (!validationResult.success) {
+ throw new Error(fromError(validationResult.error).toString());
+ }
+
+ const config: Config = validationResult.data;
let blueprintSucceeded: boolean = false;
- let blueprintMessage = "";
+ let blueprintMessage: string;
let error: any | null = null;
try {
- const validationResult = ConfigSchema.safeParse(configData);
- if (!validationResult.success) {
- throw new Error(fromError(validationResult.error).toString());
- }
-
- const config: Config = validationResult.data;
-
- let publicResourcesResults: PublicResourcesResults = [];
- let privateResourcesResults: ClientResourcesResults = [];
-
+ let proxyResourcesResults: PublicResourcesResults = [];
+ let clientResourcesResults: ClientResourcesResults = [];
await db.transaction(async (trx) => {
await updateResourcePolicies(orgId, config, trx);
- publicResourcesResults = await updatePublicResources(
+ proxyResourcesResults = await updatePublicResources(
orgId,
config,
trx,
siteId
);
- privateResourcesResults = await updatePrivateResources(
+ clientResourcesResults = await updatePrivateResources(
orgId,
config,
trx,
siteId
);
+ logger.debug(
+ `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
+ );
+
// We need to update the targets on the newts from the successfully updated information
- for (const result of publicResourcesResults) {
+ for (const result of proxyResourcesResults) {
for (const target of result.targetsToUpdate) {
const [site] = await trx
.select()
@@ -141,37 +136,166 @@ export async function applyBlueprint({
}
logger.debug(
- `Successfully updated public resources for org ${orgId}: ${JSON.stringify(publicResourcesResults)}`
+ `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
- for (const result of privateResourcesResults) {
- rebuildClientAssociationsFromSiteResource(
- result.newSiteResource
- )
- .then(() =>
- waitForSiteResourceRebuildIdle(
- result.newSiteResource.siteResourceId
+ for (const result of clientResourcesResults) {
+ if (
+ result.oldSiteResource &&
+ JSON.stringify(result.newSites?.sort()) !==
+ JSON.stringify(result.oldSites?.sort())
+ ) {
+ // query existing associations
+ const existingRoleIds = await trx
+ .select()
+ .from(roleSiteResources)
+ .where(
+ eq(
+ roleSiteResources.siteResourceId,
+ result.oldSiteResource.siteResourceId
+ )
)
- )
- .then(() =>
- handleMessagingForUpdatedSiteResource(
- result.oldSiteResource,
- result.newSiteResource,
- result.oldSites.map((s) => s.siteId),
- result.newSites.map((s) => s.siteId)
- )
- )
- .catch((e) => {
- logger.error(
- `Failed to rebuild and handle messaging for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
- );
- });
- }
+ .then((rows) => rows.map((row) => row.roleId));
- logger.debug(
- `Successfully updated private resources for org ${orgId}: ${JSON.stringify(privateResourcesResults)}`
- );
+ const existingUserIds = await trx
+ .select()
+ .from(userSiteResources)
+ .where(
+ eq(
+ userSiteResources.siteResourceId,
+ result.oldSiteResource.siteResourceId
+ )
+ )
+ .then((rows) => rows.map((row) => row.userId));
+
+ const existingClientIds = await trx
+ .select()
+ .from(clientSiteResources)
+ .where(
+ eq(
+ clientSiteResources.siteResourceId,
+ result.oldSiteResource.siteResourceId
+ )
+ )
+ .then((rows) => rows.map((row) => row.clientId));
+
+ // delete the existing site resource
+ await trx
+ .delete(siteResources)
+ .where(
+ and(
+ eq(
+ siteResources.siteResourceId,
+ result.oldSiteResource.siteResourceId
+ )
+ )
+ );
+
+ await rebuildClientAssociationsFromSiteResource(
+ result.oldSiteResource,
+ trx
+ );
+
+ const [insertedSiteResource] = await trx
+ .insert(siteResources)
+ .values({
+ ...result.newSiteResource
+ })
+ .returning();
+
+ // wait some time to allow for messages to be handled
+ await new Promise((resolve) => setTimeout(resolve, 750));
+
+ //////////////////// update the associations ////////////////////
+
+ if (existingRoleIds.length > 0) {
+ await trx.insert(roleSiteResources).values(
+ existingRoleIds.map((roleId) => ({
+ roleId,
+ siteResourceId:
+ insertedSiteResource!.siteResourceId
+ }))
+ );
+ }
+
+ if (existingUserIds.length > 0) {
+ await trx.insert(userSiteResources).values(
+ existingUserIds.map((userId) => ({
+ userId,
+ siteResourceId:
+ insertedSiteResource!.siteResourceId
+ }))
+ );
+ }
+
+ if (existingClientIds.length > 0) {
+ await trx.insert(clientSiteResources).values(
+ existingClientIds.map((clientId) => ({
+ clientId,
+ siteResourceId:
+ insertedSiteResource!.siteResourceId
+ }))
+ );
+ }
+
+ await rebuildClientAssociationsFromSiteResource(
+ insertedSiteResource,
+ trx
+ );
+ } else {
+ let good = true;
+ for (const newSite of result.newSites) {
+ const [site] = await trx
+ .select()
+ .from(sites)
+ .innerJoin(newts, eq(sites.siteId, newts.siteId))
+ .where(
+ and(
+ eq(sites.siteId, newSite.siteId),
+ eq(sites.orgId, orgId),
+ eq(sites.type, "newt"),
+ isNotNull(sites.pubKey)
+ )
+ )
+ .limit(1);
+
+ if (!site) {
+ logger.debug(
+ `No newt sites found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
+ );
+ good = false;
+ break;
+ }
+
+ logger.debug(
+ `Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.siteId}`
+ );
+ }
+
+ if (!good) {
+ continue;
+ }
+
+ await handleMessagingForUpdatedSiteResource(
+ result.oldSiteResource,
+ result.newSiteResource,
+ result.newSites.map((site) => ({
+ siteId: site.siteId,
+ orgId: result.newSiteResource.orgId
+ })),
+ trx
+ );
+ }
+
+ // await addClientTargets(
+ // site.newt.newtId,
+ // result.resource.destination,
+ // result.resource.destinationPort,
+ // result.resource.protocol,
+ // result.resource.proxyPort
+ // );
+ }
});
blueprintSucceeded = true;
@@ -179,9 +303,7 @@ export async function applyBlueprint({
} catch (err) {
blueprintSucceeded = false;
blueprintMessage = `Blueprint applied with errors: ${err}`;
- logger.debug(
- `Org ${orgId} blueprint apply issues: ${blueprintMessage}`
- );
+ logger.error(blueprintMessage);
error = err;
}
diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts
index 59e7103d8..6b91b3462 100644
--- a/server/lib/blueprints/applyNewtDockerBlueprint.ts
+++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts
@@ -116,7 +116,7 @@ export async function applyNewtDockerBlueprint(
source: "NEWT"
});
} catch (error) {
- logger.debug(`Failed to update database from config: ${error}`);
+ logger.error(`Failed to update database from config: ${error}`);
await sendToClient(newtId, {
type: "newt/blueprint/results",
data: {
diff --git a/server/lib/blueprints/privateResources.ts b/server/lib/blueprints/privateResources.ts
index 74a1f618f..8d00701f1 100644
--- a/server/lib/blueprints/privateResources.ts
+++ b/server/lib/blueprints/privateResources.ts
@@ -25,12 +25,6 @@ import { getNextAvailableAliasAddress } from "../ip";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
-import { build } from "@server/build";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import next from "next";
-import { LimitId } from "../billing";
-import { usageService } from "../billing/usageService";
async function getDomainForSiteResource(
siteResourceId: number | undefined,
@@ -419,34 +413,6 @@ export async function updatePrivateResources(
oldSites: existingSiteIds
});
} else {
- // create a brand new resource
-
- if (build == "saas") {
- const usage = await usageService.getUsage(
- orgId,
- LimitId.PRIVATE_RESOURCES
- );
- if (!usage) {
- throw new Error(
- `Usage data not found for org ${orgId} and limit ${LimitId.PRIVATE_RESOURCES}`
- );
- }
- const rejectResource = await usageService.checkLimitSet(
- orgId,
-
- LimitId.PRIVATE_RESOURCES,
- {
- ...usage,
- instantaneousValue: (usage.instantaneousValue || 0) + 1
- } // We need to add one to know if we are violating the limit
- );
- if (rejectResource) {
- throw new Error(
- "Private resource limit exceeded. Please upgrade your plan."
- );
- }
- }
-
let aliasAddress: string | null = null;
let releaseAliasLock: (() => Promise) | null = null;
if (
@@ -643,8 +609,6 @@ export async function updatePrivateResources(
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
);
- await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
-
results.push({
newSiteResource: newResource,
newSites: allSites,
diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts
index df3b3799e..1bbe0a4f1 100644
--- a/server/lib/blueprints/publicResources.ts
+++ b/server/lib/blueprints/publicResources.ts
@@ -1,59 +1,56 @@
-import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
-import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
-import { hashPassword } from "@server/auth/password";
-import { generateId } from "@server/auth/sessions/app";
-import { build } from "@server/build";
import {
- domainNamespaces,
domains,
+ domainNamespaces,
orgDomains,
Resource,
resourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility,
- resourcePassword,
resourcePincode,
- resourcePolicies,
- resourcePolicyHeaderAuth,
- resourcePolicyPassword,
- resourcePolicyPincode,
- resourcePolicyRules,
- resourcePolicyWhiteList,
resourceRules,
- resources,
resourceWhitelist,
roleActions,
- rolePolicies,
roleResources,
roles,
- sites,
Target,
TargetHealthCheck,
targetHealthCheck,
- targets,
Transaction,
userOrgs,
- userPolicies,
userResources,
users,
- type ResourceRule
+ resourcePolicies,
+ resourcePolicyPassword,
+ resourcePolicyPincode,
+ resourcePolicyHeaderAuth,
+ resourcePolicyRules,
+ resourcePolicyWhiteList,
+ rolePolicies,
+ userPolicies
} from "@server/db";
-import { getUniqueResourcePolicyName } from "@server/db/names";
-import { isValidRegionId } from "@server/db/regions";
-import { fireHealthCheckUnknownAlert } from "@server/lib/alerts";
-import serverConfig from "@server/lib/config";
-import { encrypt } from "@server/lib/crypto";
+import { resources, targets, sites } from "@server/db";
+import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
+import {
+ Config,
+ ConfigSchema,
+ isTargetsOnlyResource,
+ TargetData
+} from "./types";
import logger from "@server/logger";
-import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
+import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { pickPort } from "@server/routers/target/helpers";
-import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
-import { tierMatrix } from "../billing/tierMatrix";
+import { resourcePassword } from "@server/db";
+import { getUniqueResourcePolicyName } from "@server/db/names";
+import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
-import { Config, isTargetsOnlyResource, TargetData } from "./types";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import next from "next";
-import { LimitId } from "../billing";
-import { usageService } from "../billing/usageService";
+import { isValidRegionId } from "@server/db/regions";
+import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { fireHealthCheckUnknownAlert } from "@server/lib/alerts";
+import { tierMatrix } from "../billing/tierMatrix";
+import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
+import { build } from "@server/build";
+import { encrypt } from "@server/lib/crypto";
+import { generateId } from "@server/auth/sessions/app";
+import serverConfig from "@server/lib/config";
export type PublicResourcesResults = {
proxyResource: Resource;
@@ -907,7 +904,7 @@ export async function updatePublicResources(
.update(resourceRules)
.set({
action: getRuleAction(rule.action),
- match: rule.match.toUpperCase() as ResourceRule["match"],
+ match: rule.match.toUpperCase(),
value: getRuleValue(
rule.match.toUpperCase(),
rule.value
@@ -926,7 +923,7 @@ export async function updatePublicResources(
await trx.insert(resourceRules).values({
resourceId: existingResource.resourceId,
action: getRuleAction(rule.action),
- match: rule.match.toUpperCase() as ResourceRule["match"],
+ match: rule.match.toUpperCase(),
value: getRuleValue(
rule.match.toUpperCase(),
rule.value
@@ -1008,33 +1005,6 @@ export async function updatePublicResources(
logger.debug(`Updated resource ${existingResource.resourceId}`);
} else {
// create a brand new resource
-
- if (build === "saas") {
- const usage = await usageService.getUsage(
- orgId,
- LimitId.PUBLIC_RESOURCES
- );
- if (!usage) {
- throw new Error(
- `Usage data not found for org ${orgId} and limit ${LimitId.PUBLIC_RESOURCES}`
- );
- }
- const rejectResource = await usageService.checkLimitSet(
- orgId,
-
- LimitId.PUBLIC_RESOURCES,
- {
- ...usage,
- instantaneousValue: (usage.instantaneousValue || 0) + 1
- } // We need to add one to know if we are violating the limit
- );
- if (rejectResource) {
- throw new Error(
- "Public resource limit exceeded. Please upgrade your plan."
- );
- }
- }
-
let domain;
if (
["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "")
@@ -1290,7 +1260,7 @@ export async function updatePublicResources(
await trx.insert(resourceRules).values({
resourceId: newResource.resourceId,
action: getRuleAction(rule.action),
- match: rule.match.toUpperCase() as ResourceRule["match"],
+ match: rule.match.toUpperCase(),
value: getRuleValue(
rule.match.toUpperCase(),
rule.value
@@ -1324,8 +1294,6 @@ export async function updatePublicResources(
await createTarget(newResource.resourceId, targetData);
}
- await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
-
logger.debug(`Created resource ${newResource.resourceId}`);
}
@@ -1353,7 +1321,7 @@ function getRuleAction(input: string) {
function getRuleValue(match: string, value: string) {
// if the match is a country, uppercase the value
- if (match === "COUNTRY" || match === "COUNTRY_IS_NOT") {
+ if (match == "COUNTRY") {
return value.toUpperCase();
}
return value;
diff --git a/server/lib/blueprints/resourcePolicies.ts b/server/lib/blueprints/resourcePolicies.ts
index aa5f4bc9b..f8d8d1269 100644
--- a/server/lib/blueprints/resourcePolicies.ts
+++ b/server/lib/blueprints/resourcePolicies.ts
@@ -347,13 +347,12 @@ function getRuleAction(input: string): "ACCEPT" | "DROP" | "PASS" {
function getRuleMatch(
input: string
-): "CIDR" | "IP" | "PATH" | "COUNTRY" | "COUNTRY_IS_NOT" | "ASN" | "REGION" {
+): "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" | "REGION" {
return input.toUpperCase() as
| "CIDR"
| "IP"
| "PATH"
| "COUNTRY"
- | "COUNTRY_IS_NOT"
| "ASN"
| "REGION";
}
diff --git a/server/lib/calculateUserClientsForOrgs.ts b/server/lib/calculateUserClientsForOrgs.ts
index 39585500a..090bf4d8c 100644
--- a/server/lib/calculateUserClientsForOrgs.ts
+++ b/server/lib/calculateUserClientsForOrgs.ts
@@ -6,7 +6,6 @@ import {
db,
olms,
orgs,
- primaryDb,
roleClients,
roles,
Transaction,
@@ -24,427 +23,415 @@ import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations
import { OlmErrorCodes } from "@server/routers/olm/error";
import { tierMatrix } from "./billing/tierMatrix";
-type ClientRow = typeof clients.$inferSelect;
-
-function runQueuedClientAssociationRebuilds(
- userId: string,
- queuedClients: ClientRow[]
-) {
- if (queuedClients.length === 0) {
- return;
- }
-
- const uniqueClientsById = new Map();
- for (const client of queuedClients) {
- uniqueClientsById.set(client.clientId, client);
- }
-
- for (const client of uniqueClientsById.values()) {
- rebuildClientAssociationsFromClient(client).catch((error) => {
- logger.error(
- `Error rebuilding client associations for client ${client.clientId} (user ${userId}): ${String(
- error
- )}`
- );
- });
- }
-
- logger.debug(
- `Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
- );
-}
-
export async function calculateUserClientsForOrgs(
- userId: string
+ userId: string,
+ trx: Transaction | typeof db = db
): Promise {
- const trx = primaryDb;
+ const execute = async (transaction: Transaction | typeof db) => {
+ const orgCache = new Map();
+ const adminRoleCache = new Map<
+ string,
+ typeof roles.$inferSelect | null
+ >();
+ const exitNodesCache = new Map<
+ string,
+ Awaited>
+ >();
+ const isOrgLicensedCache = new Map();
+ const existingClientCache = new Map<
+ string,
+ typeof clients.$inferSelect | null
+ >();
+ const roleClientAccessCache = new Map();
+ const userClientAccessCache = new Map();
- const queuedAssociationRebuilds: ClientRow[] = [];
- const orgCache = new Map();
- const adminRoleCache = new Map();
- const exitNodesCache = new Map<
- string,
- Awaited>
- >();
- const isOrgLicensedCache = new Map();
- const existingClientCache = new Map<
- string,
- typeof clients.$inferSelect | null
- >();
- const roleClientAccessCache = new Map();
- const userClientAccessCache = new Map();
+ const getOrgOlmKey = (orgId: string, olmId: string) =>
+ `${orgId}:${olmId}`;
+ const getRoleClientKey = (roleId: number, clientId: number) =>
+ `${roleId}:${clientId}`;
+ const getUserClientKey = (cachedUserId: string, clientId: number) =>
+ `${cachedUserId}:${clientId}`;
- const getOrgOlmKey = (orgId: string, olmId: string) => `${orgId}:${olmId}`;
- const getRoleClientKey = (roleId: number, clientId: number) =>
- `${roleId}:${clientId}`;
- const getUserClientKey = (cachedUserId: string, clientId: number) =>
- `${cachedUserId}:${clientId}`;
-
- const getOrg = async (orgId: string) => {
- if (orgCache.has(orgId)) {
- return orgCache.get(orgId) ?? null;
- }
-
- const [org] = await trx
- .select()
- .from(orgs)
- .where(eq(orgs.orgId, orgId));
- orgCache.set(orgId, org ?? null);
-
- return org ?? null;
- };
-
- const getAdminRole = async (orgId: string) => {
- if (adminRoleCache.has(orgId)) {
- return adminRoleCache.get(orgId) ?? null;
- }
-
- const [adminRole] = await trx
- .select()
- .from(roles)
- .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
- .limit(1);
- adminRoleCache.set(orgId, adminRole ?? null);
-
- return adminRole ?? null;
- };
-
- const getExitNodes = async (orgId: string) => {
- if (exitNodesCache.has(orgId)) {
- return exitNodesCache.get(orgId)!;
- }
-
- const exitNodes = await listExitNodes(orgId);
- exitNodesCache.set(orgId, exitNodes);
-
- return exitNodes;
- };
-
- const getIsOrgLicensed = async (orgId: string) => {
- if (isOrgLicensedCache.has(orgId)) {
- return isOrgLicensedCache.get(orgId)!;
- }
-
- const isOrgLicensed = await isLicensedOrSubscribed(
- orgId,
- tierMatrix.deviceApprovals
- );
- isOrgLicensedCache.set(orgId, isOrgLicensed);
-
- return isOrgLicensed;
- };
-
- const getExistingClient = async (orgId: string, olmId: string) => {
- const key = getOrgOlmKey(orgId, olmId);
- if (existingClientCache.has(key)) {
- return existingClientCache.get(key) ?? null;
- }
-
- const [existingClient] = await trx
- .select()
- .from(clients)
- .where(
- and(
- eq(clients.userId, userId),
- eq(clients.orgId, orgId),
- eq(clients.olmId, olmId)
- )
- )
- .limit(1);
-
- existingClientCache.set(key, existingClient ?? null);
-
- return existingClient ?? null;
- };
-
- const hasRoleClientAccess = async (roleId: number, clientId: number) => {
- const key = getRoleClientKey(roleId, clientId);
- if (roleClientAccessCache.has(key)) {
- return roleClientAccessCache.get(key)!;
- }
-
- const [existingRoleClient] = await trx
- .select()
- .from(roleClients)
- .where(
- and(
- eq(roleClients.roleId, roleId),
- eq(roleClients.clientId, clientId)
- )
- )
- .limit(1);
-
- const hasAccess = Boolean(existingRoleClient);
- roleClientAccessCache.set(key, hasAccess);
-
- return hasAccess;
- };
-
- const hasUserClientAccess = async (
- cachedUserId: string,
- clientId: number
- ) => {
- const key = getUserClientKey(cachedUserId, clientId);
- if (userClientAccessCache.has(key)) {
- return userClientAccessCache.get(key)!;
- }
-
- const [existingUserClient] = await trx
- .select()
- .from(userClients)
- .where(
- and(
- eq(userClients.userId, cachedUserId),
- eq(userClients.clientId, clientId)
- )
- )
- .limit(1);
-
- const hasAccess = Boolean(existingUserClient);
- userClientAccessCache.set(key, hasAccess);
-
- return hasAccess;
- };
-
- // Get all OLMs for this user
- const userOlms = await trx
- .select()
- .from(olms)
- .where(eq(olms.userId, userId));
-
- if (userOlms.length === 0) {
- // No OLMs for this user, but we should still clean up any orphaned clients
- await cleanupOrphanedClients(
- userId,
- trx,
- [],
- queuedAssociationRebuilds
- );
- return;
- }
-
- // Get all user orgs with all roles (for org list and role-based logic)
- const userOrgRoleRows = await trx
- .select()
- .from(userOrgs)
- .innerJoin(
- userOrgRoles,
- and(
- eq(userOrgs.userId, userOrgRoles.userId),
- eq(userOrgs.orgId, userOrgRoles.orgId)
- )
- )
- .innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
- .where(eq(userOrgs.userId, userId));
-
- const userOrgIds = [
- ...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
- ];
- const orgIdToRoleRows = new Map();
- for (const r of userOrgRoleRows) {
- const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
- list.push(r);
- orgIdToRoleRows.set(r.userOrgs.orgId, list);
- }
- const orgRequiresDeviceApprovalRole = new Map();
- for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
- orgRequiresDeviceApprovalRole.set(
- orgId,
- roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
- );
- }
-
- // For each OLM, ensure there's a client in each org the user is in
- for (const olm of userOlms) {
- for (const orgId of orgIdToRoleRows.keys()) {
- const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
- const userOrg = roleRowsForOrg[0].userOrgs;
-
- const org = await getOrg(orgId);
-
- if (!org) {
- logger.warn(
- `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
- );
- continue;
+ const getOrg = async (orgId: string) => {
+ if (orgCache.has(orgId)) {
+ return orgCache.get(orgId) ?? null;
}
- if (!org.subnet) {
- logger.warn(
- `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured`
- );
- continue;
+ const [org] = await transaction
+ .select()
+ .from(orgs)
+ .where(eq(orgs.orgId, orgId));
+ orgCache.set(orgId, org ?? null);
+
+ return org ?? null;
+ };
+
+ const getAdminRole = async (orgId: string) => {
+ if (adminRoleCache.has(orgId)) {
+ return adminRoleCache.get(orgId) ?? null;
}
- // Get admin role for this org (needed for access grants)
- const adminRole = await getAdminRole(orgId);
+ const [adminRole] = await transaction
+ .select()
+ .from(roles)
+ .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
+ .limit(1);
+ adminRoleCache.set(orgId, adminRole ?? null);
- if (!adminRole) {
- logger.warn(
- `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
- );
- continue;
+ return adminRole ?? null;
+ };
+
+ const getExitNodes = async (orgId: string) => {
+ if (exitNodesCache.has(orgId)) {
+ return exitNodesCache.get(orgId)!;
}
- // Check if a client already exists for this OLM+user+org combination
- const existingClient = await getExistingClient(orgId, olm.olmId);
+ const exitNodes = await listExitNodes(orgId);
+ exitNodesCache.set(orgId, exitNodes);
- if (existingClient) {
- // Ensure admin role has access to the client
- const hasRoleAccess = await hasRoleClientAccess(
- adminRole.roleId,
- existingClient.clientId
- );
+ return exitNodes;
+ };
- if (!hasRoleAccess) {
- await trx.insert(roleClients).values({
- roleId: adminRole.roleId,
- clientId: existingClient.clientId
- });
- roleClientAccessCache.set(
- getRoleClientKey(
- adminRole.roleId,
- existingClient.clientId
- ),
- true
- );
- logger.debug(
- `Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
+ const getIsOrgLicensed = async (orgId: string) => {
+ if (isOrgLicensedCache.has(orgId)) {
+ return isOrgLicensedCache.get(orgId)!;
+ }
+
+ const isOrgLicensed = await isLicensedOrSubscribed(
+ orgId,
+ tierMatrix.deviceApprovals
+ );
+ isOrgLicensedCache.set(orgId, isOrgLicensed);
+
+ return isOrgLicensed;
+ };
+
+ const getExistingClient = async (orgId: string, olmId: string) => {
+ const key = getOrgOlmKey(orgId, olmId);
+ if (existingClientCache.has(key)) {
+ return existingClientCache.get(key) ?? null;
+ }
+
+ const [existingClient] = await transaction
+ .select()
+ .from(clients)
+ .where(
+ and(
+ eq(clients.userId, userId),
+ eq(clients.orgId, orgId),
+ eq(clients.olmId, olmId)
+ )
+ )
+ .limit(1);
+
+ existingClientCache.set(key, existingClient ?? null);
+
+ return existingClient ?? null;
+ };
+
+ const hasRoleClientAccess = async (
+ roleId: number,
+ clientId: number
+ ) => {
+ const key = getRoleClientKey(roleId, clientId);
+ if (roleClientAccessCache.has(key)) {
+ return roleClientAccessCache.get(key)!;
+ }
+
+ const [existingRoleClient] = await transaction
+ .select()
+ .from(roleClients)
+ .where(
+ and(
+ eq(roleClients.roleId, roleId),
+ eq(roleClients.clientId, clientId)
+ )
+ )
+ .limit(1);
+
+ const hasAccess = Boolean(existingRoleClient);
+ roleClientAccessCache.set(key, hasAccess);
+
+ return hasAccess;
+ };
+
+ const hasUserClientAccess = async (
+ cachedUserId: string,
+ clientId: number
+ ) => {
+ const key = getUserClientKey(cachedUserId, clientId);
+ if (userClientAccessCache.has(key)) {
+ return userClientAccessCache.get(key)!;
+ }
+
+ const [existingUserClient] = await transaction
+ .select()
+ .from(userClients)
+ .where(
+ and(
+ eq(userClients.userId, cachedUserId),
+ eq(userClients.clientId, clientId)
+ )
+ )
+ .limit(1);
+
+ const hasAccess = Boolean(existingUserClient);
+ userClientAccessCache.set(key, hasAccess);
+
+ return hasAccess;
+ };
+
+ // Get all OLMs for this user
+ const userOlms = await transaction
+ .select()
+ .from(olms)
+ .where(eq(olms.userId, userId));
+
+ if (userOlms.length === 0) {
+ // No OLMs for this user, but we should still clean up any orphaned clients
+ await cleanupOrphanedClients(userId, transaction);
+ return;
+ }
+
+ // Get all user orgs with all roles (for org list and role-based logic)
+ const userOrgRoleRows = await transaction
+ .select()
+ .from(userOrgs)
+ .innerJoin(
+ userOrgRoles,
+ and(
+ eq(userOrgs.userId, userOrgRoles.userId),
+ eq(userOrgs.orgId, userOrgRoles.orgId)
+ )
+ )
+ .innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
+ .where(eq(userOrgs.userId, userId));
+
+ const userOrgIds = [
+ ...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))
+ ];
+ const orgIdToRoleRows = new Map<
+ string,
+ (typeof userOrgRoleRows)[0][]
+ >();
+ for (const r of userOrgRoleRows) {
+ const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
+ list.push(r);
+ orgIdToRoleRows.set(r.userOrgs.orgId, list);
+ }
+ const orgRequiresDeviceApprovalRole = new Map();
+ for (const [orgId, roleRowsForOrg] of orgIdToRoleRows.entries()) {
+ orgRequiresDeviceApprovalRole.set(
+ orgId,
+ roleRowsForOrg.some((r) => r.roles.requireDeviceApproval)
+ );
+ }
+
+ // For each OLM, ensure there's a client in each org the user is in
+ for (const olm of userOlms) {
+ for (const orgId of orgIdToRoleRows.keys()) {
+ const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
+ const userOrg = roleRowsForOrg[0].userOrgs;
+
+ const org = await getOrg(orgId);
+
+ if (!org) {
+ logger.warn(
+ `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org not found`
);
+ continue;
}
- // Ensure user has access to the client
- const hasUserAccess = await hasUserClientAccess(
- userId,
- existingClient.clientId
+ if (!org.subnet) {
+ logger.warn(
+ `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): org has no subnet configured`
+ );
+ continue;
+ }
+
+ // Get admin role for this org (needed for access grants)
+ const adminRole = await getAdminRole(orgId);
+
+ if (!adminRole) {
+ logger.warn(
+ `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no admin role found`
+ );
+ continue;
+ }
+
+ // Check if a client already exists for this OLM+user+org combination
+ const existingClient = await getExistingClient(
+ orgId,
+ olm.olmId
);
- if (!hasUserAccess) {
- await trx.insert(userClients).values({
+ if (existingClient) {
+ // Ensure admin role has access to the client
+ const hasRoleAccess = await hasRoleClientAccess(
+ adminRole.roleId,
+ existingClient.clientId
+ );
+
+ if (!hasRoleAccess) {
+ await transaction.insert(roleClients).values({
+ roleId: adminRole.roleId,
+ clientId: existingClient.clientId
+ });
+ roleClientAccessCache.set(
+ getRoleClientKey(
+ adminRole.roleId,
+ existingClient.clientId
+ ),
+ true
+ );
+ logger.debug(
+ `Granted admin role access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
+ );
+ }
+
+ // Ensure user has access to the client
+ const hasUserAccess = await hasUserClientAccess(
userId,
- clientId: existingClient.clientId
- });
- userClientAccessCache.set(
- getUserClientKey(userId, existingClient.clientId),
- true
+ existingClient.clientId
);
+
+ if (!hasUserAccess) {
+ await transaction.insert(userClients).values({
+ userId,
+ clientId: existingClient.clientId
+ });
+ userClientAccessCache.set(
+ getUserClientKey(userId, existingClient.clientId),
+ true
+ );
+ logger.debug(
+ `Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
+ );
+ }
+
logger.debug(
- `Granted user access to existing client ${existingClient.clientId} for OLM ${olm.olmId} in org ${orgId} (user ${userId})`
+ `Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation`
);
+ continue;
}
+ // Get exit nodes for this org
+ const exitNodesList = await getExitNodes(orgId);
+
+ if (exitNodesList.length === 0) {
+ logger.warn(
+ `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
+ );
+ continue;
+ }
+
+ const randomExitNode =
+ exitNodesList[
+ Math.floor(Math.random() * exitNodesList.length)
+ ];
+
+ // Get next available subnet
+ const { value: newSubnet, release: releaseSubnetLock } =
+ await getNextAvailableClientSubnet(orgId, transaction);
+
+ const subnet = newSubnet.split("/")[0];
+ const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
+
+ const niceId = await getUniqueClientName(orgId);
+
+ const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
+ const requireApproval =
+ build !== "oss" &&
+ isOrgLicensed &&
+ orgRequiresDeviceApprovalRole.get(orgId) === true;
+
+ const newClientData: InferInsertModel = {
+ userId,
+ orgId: userOrg.orgId,
+ exitNodeId: randomExitNode.exitNodeId,
+ name: olm.name || "User Client",
+ subnet: updatedSubnet,
+ olmId: olm.olmId,
+ type: "olm",
+ niceId,
+ approvalState: requireApproval ? "pending" : null
+ };
+
+ // Create the client
+ const [newClient] = await transaction
+ .insert(clients)
+ .values(newClientData)
+ .returning();
+ await releaseSubnetLock();
+ existingClientCache.set(
+ getOrgOlmKey(orgId, olm.olmId),
+ newClient
+ );
+
+ // create approval request
+ if (requireApproval) {
+ await transaction
+ .insert(approvals)
+ .values({
+ timestamp: Math.floor(new Date().getTime() / 1000),
+ orgId: userOrg.orgId,
+ clientId: newClient.clientId,
+ userId,
+ type: "user_device"
+ })
+ .returning();
+ }
+
+ await rebuildClientAssociationsFromClient(
+ newClient,
+ transaction
+ );
+
+ // Grant admin role access to the client
+ await transaction.insert(roleClients).values({
+ roleId: adminRole.roleId,
+ clientId: newClient.clientId
+ });
+ roleClientAccessCache.set(
+ getRoleClientKey(adminRole.roleId, newClient.clientId),
+ true
+ );
+
+ // Grant user access to the client
+ await transaction.insert(userClients).values({
+ userId,
+ clientId: newClient.clientId
+ });
+ userClientAccessCache.set(
+ getUserClientKey(userId, newClient.clientId),
+ true
+ );
+
logger.debug(
- `Client already exists for OLM ${olm.olmId} in org ${orgId} (user ${userId}), skipping creation`
+ `Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
);
- continue;
}
-
- // Get exit nodes for this org
- const exitNodesList = await getExitNodes(orgId);
-
- if (exitNodesList.length === 0) {
- logger.warn(
- `Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
- );
- continue;
- }
-
- const randomExitNode =
- exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
-
- // Get next available subnet
- const { value: newSubnet, release: releaseSubnetLock } =
- await getNextAvailableClientSubnet(orgId, trx);
-
- const subnet = newSubnet.split("/")[0];
- const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
-
- const niceId = await getUniqueClientName(orgId);
-
- const isOrgLicensed = await getIsOrgLicensed(userOrg.orgId);
- const requireApproval =
- build !== "oss" &&
- isOrgLicensed &&
- orgRequiresDeviceApprovalRole.get(orgId) === true;
-
- const newClientData: InferInsertModel = {
- userId,
- orgId: userOrg.orgId,
- exitNodeId: randomExitNode.exitNodeId,
- name: olm.name || "User Client",
- subnet: updatedSubnet,
- olmId: olm.olmId,
- type: "olm",
- niceId,
- approvalState: requireApproval ? "pending" : null
- };
-
- // Create the client
- const [newClient] = await trx
- .insert(clients)
- .values(newClientData)
- .returning();
- await releaseSubnetLock();
- existingClientCache.set(getOrgOlmKey(orgId, olm.olmId), newClient);
-
- // create approval request
- if (requireApproval) {
- await trx
- .insert(approvals)
- .values({
- timestamp: Math.floor(new Date().getTime() / 1000),
- orgId: userOrg.orgId,
- clientId: newClient.clientId,
- userId,
- type: "user_device"
- })
- .returning();
- }
-
- queuedAssociationRebuilds.push(newClient);
-
- // Grant admin role access to the client
- await trx.insert(roleClients).values({
- roleId: adminRole.roleId,
- clientId: newClient.clientId
- });
- roleClientAccessCache.set(
- getRoleClientKey(adminRole.roleId, newClient.clientId),
- true
- );
-
- // Grant user access to the client
- await trx.insert(userClients).values({
- userId,
- clientId: newClient.clientId
- });
- userClientAccessCache.set(
- getUserClientKey(userId, newClient.clientId),
- true
- );
-
- logger.debug(
- `Created client for OLM ${olm.olmId} in org ${orgId} (user ${userId}) with access granted to admin role and user`
- );
}
+
+ // Clean up clients in orgs the user is no longer in
+ await cleanupOrphanedClients(userId, transaction, userOrgIds);
+ };
+
+ if (trx) {
+ // Use provided transaction
+ await execute(trx);
+ } else {
+ // Create new transaction
+ await db.transaction(async (transaction) => {
+ await execute(transaction);
+ });
}
-
- // Clean up clients in orgs the user is no longer in
- await cleanupOrphanedClients(
- userId,
- trx,
- userOrgIds,
- queuedAssociationRebuilds
- );
-
- runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
}
async function cleanupOrphanedClients(
userId: string,
trx: Transaction | typeof db,
- userOrgIds: string[] = [],
- queuedAssociationRebuilds: ClientRow[] = []
+ userOrgIds: string[] = []
): Promise {
// Find all OLM clients for this user that should be deleted
// If userOrgIds is empty, delete all OLM clients (user has no orgs)
@@ -474,9 +461,9 @@ async function cleanupOrphanedClients(
)
.returning();
- // Queue deleted clients for post-trx association cleanup.
+ // Rebuild associations for each deleted client to clean up related data
for (const deletedClient of deletedClients) {
- queuedAssociationRebuilds.push(deletedClient);
+ await rebuildClientAssociationsFromClient(deletedClient, trx);
if (deletedClient.olmId) {
await sendTerminateClient(
diff --git a/server/lib/consts.ts b/server/lib/consts.ts
index 57a4836b8..6f401662e 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.20.0";
+export const APP_VERSION = "1.19.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
diff --git a/server/lib/dbRetry.ts b/server/lib/dbRetry.ts
deleted file mode 100644
index 7f8d1eea8..000000000
--- a/server/lib/dbRetry.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import logger from "@server/logger";
-
-const MAX_RETRIES = 5;
-const BASE_DELAY_MS = 50;
-
-/**
- * Detect transient errors that are safe to retry (connection drops, deadlocks,
- * serialization failures). 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.
- */
-export function isTransientError(error: any): boolean {
- if (!error) return false;
-
- const message = (error.message || "").toLowerCase();
- const causeMessage = (error.cause?.message || "").toLowerCase();
- const code = error.code || error.cause?.code || "";
-
- // Connection timeout / terminated
- if (
- message.includes("connection timeout") ||
- message.includes("connection terminated") ||
- message.includes("timeout exceeded when trying to connect") ||
- causeMessage.includes("connection terminated unexpectedly") ||
- causeMessage.includes("connection timeout")
- ) {
- return true;
- }
-
- // PostgreSQL deadlock detected - always safe to retry (one winner guaranteed)
- if (code === "40P01" || message.includes("deadlock")) {
- return true;
- }
-
- // PostgreSQL serialization failure
- if (code === "40001") {
- return true;
- }
-
- // ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
- if (
- code === "ECONNRESET" ||
- code === "ECONNREFUSED" ||
- code === "EPIPE" ||
- code === "ETIMEDOUT"
- ) {
- return true;
- }
-
- return false;
-}
-
-/**
- * Simple retry wrapper with exponential backoff for transient errors
- * (deadlocks, connection timeouts, unexpected disconnects).
- */
-export async function withRetry(
- operation: () => Promise,
- context: string,
- maxRetries: number = MAX_RETRIES,
- baseDelayMs: number = BASE_DELAY_MS
-): Promise {
- let attempt = 0;
- while (true) {
- try {
- return await operation();
- } catch (error: any) {
- if (isTransientError(error) && attempt < maxRetries) {
- attempt++;
- const baseDelay = Math.pow(2, attempt - 1) * baseDelayMs;
- const jitter = Math.random() * baseDelay;
- const delay = baseDelay + jitter;
- logger.warn(
- `Transient DB error in ${context}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`,
- { code: error?.code ?? error?.cause?.code }
- );
- await new Promise((resolve) => setTimeout(resolve, delay));
- continue;
- }
- throw error;
- }
- }
-}
diff --git a/server/lib/deleteOrg.ts b/server/lib/deleteOrg.ts
index 20c8a3e23..065f216a1 100644
--- a/server/lib/deleteOrg.ts
+++ b/server/lib/deleteOrg.ts
@@ -24,7 +24,7 @@ import { deletePeer } from "@server/routers/gerbil/peers";
import { OlmErrorCodes } from "@server/routers/olm/error";
import { sendTerminateClient } from "@server/routers/client/terminate";
import { usageService } from "./billing/usageService";
-import { LimitId } from "./billing";
+import { FeatureId } from "./billing";
export type DeleteOrgByIdResult = {
deletedNewtIds: string[];
@@ -140,9 +140,7 @@ export async function deleteOrgById(
.select({ count: count() })
.from(orgDomains)
.where(eq(orgDomains.domainId, domainId));
- logger.info(
- `Found ${orgCount.count} orgs using domain ${domainId}`
- );
+ logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`);
if (orgCount.count === 1) {
domainIdsToDelete.push(domainId);
}
@@ -154,7 +152,7 @@ export async function deleteOrgById(
.where(inArray(domains.domainId, domainIdsToDelete));
}
- await usageService.add(orgId, LimitId.ORGANIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
+ await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
await trx.delete(orgs).where(eq(orgs.orgId, orgId));
@@ -201,22 +199,22 @@ export async function deleteOrgById(
if (org.billingOrgId) {
usageService.updateCount(
org.billingOrgId,
- LimitId.DOMAINS,
+ FeatureId.DOMAINS,
domainCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
- LimitId.SITES,
+ FeatureId.SITES,
siteCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
- LimitId.USERS,
+ FeatureId.USERS,
userCount ?? 0
);
usageService.updateCount(
org.billingOrgId,
- LimitId.REMOTE_EXIT_NODES,
+ FeatureId.REMOTE_EXIT_NODES,
remoteExitNodeCount ?? 0
);
}
diff --git a/server/lib/deleteResource.ts b/server/lib/deleteResource.ts
deleted file mode 100644
index b2ffa0f0f..000000000
--- a/server/lib/deleteResource.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { eq, inArray } from "drizzle-orm";
-import {
- db,
- newts,
- resourcePolicies,
- resources,
- sites,
- targetHealthCheck,
- targets,
- type Resource,
- type Target,
- type TargetHealthCheck,
- type Transaction
-} from "@server/db";
-import logger from "@server/logger";
-import { removeTargets } from "@server/routers/newt/targets";
-import createHttpError from "http-errors";
-import HttpCode from "@server/types/HttpCode";
-
-export type DeleteResourceResult = {
- deletedResource: Resource;
- targetsToBeRemoved: Target[];
- healthChecksToBeRemoved: TargetHealthCheck[];
-};
-
-export async function performDeleteResources(
- resourceIds: number[],
- trx: Transaction | typeof db = db
-): Promise {
- if (resourceIds.length === 0) {
- return [];
- }
-
- const targetsToBeRemoved = await trx
- .select()
- .from(targets)
- .where(inArray(targets.resourceId, resourceIds));
-
- const targetIds = targetsToBeRemoved.map((t) => t.targetId);
- const healthChecksToBeRemoved =
- targetIds.length > 0
- ? await trx
- .select()
- .from(targetHealthCheck)
- .where(inArray(targetHealthCheck.targetId, targetIds))
- : [];
-
- const deletedResources = await trx
- .delete(resources)
- .where(inArray(resources.resourceId, resourceIds))
- .returning();
-
- const policyIds = deletedResources
- .map((resource) => resource.defaultResourcePolicyId)
- .filter((id): id is number => id != null);
-
- if (policyIds.length > 0) {
- await trx
- .delete(resourcePolicies)
- .where(inArray(resourcePolicies.resourcePolicyId, policyIds));
- }
-
- if (deletedResources.length > 0) {
- logger.debug(`Deleted ${deletedResources.length} resources`);
- }
-
- const targetsByResourceId = new Map();
- for (const target of targetsToBeRemoved) {
- const existing = targetsByResourceId.get(target.resourceId) ?? [];
- existing.push(target);
- targetsByResourceId.set(target.resourceId, existing);
- }
-
- const targetIdToResourceId = new Map(
- targetsToBeRemoved.map((target) => [target.targetId, target.resourceId])
- );
-
- const healthChecksByResourceId = new Map();
- for (const healthCheck of healthChecksToBeRemoved) {
- const resourceId = targetIdToResourceId.get(healthCheck.targetId!);
- if (resourceId == null) {
- continue;
- }
- const existing = healthChecksByResourceId.get(resourceId) ?? [];
- existing.push(healthCheck);
- healthChecksByResourceId.set(resourceId, existing);
- }
-
- return deletedResources.map((deletedResource) => ({
- deletedResource,
- targetsToBeRemoved:
- targetsByResourceId.get(deletedResource.resourceId) ?? [],
- healthChecksToBeRemoved:
- healthChecksByResourceId.get(deletedResource.resourceId) ?? []
- }));
-}
-
-export async function performDeleteResource(
- resourceId: number,
- trx: Transaction | typeof db = db
-): Promise {
- const [result] = await performDeleteResources([resourceId], trx);
- return result ?? null;
-}
-
-export async function runResourceDeleteSideEffects(
- result: DeleteResourceResult
-): Promise {
- const { deletedResource, targetsToBeRemoved, healthChecksToBeRemoved } =
- result;
-
- for (const target of targetsToBeRemoved) {
- const [site] = await db
- .select()
- .from(sites)
- .where(eq(sites.siteId, target.siteId))
- .limit(1);
-
- if (!site) {
- throw createHttpError(
- HttpCode.NOT_FOUND,
- `Site with ID ${target.siteId} not found`
- );
- }
-
- if (site.pubKey && site.type === "newt") {
- const [newt] = await db
- .select()
- .from(newts)
- .where(eq(newts.siteId, site.siteId))
- .limit(1);
-
- if (newt) {
- await removeTargets(
- newt.newtId,
- [],
- healthChecksToBeRemoved,
- deletedResource.mode === "udp" ? "udp" : "tcp",
- newt.version
- );
- }
- }
- }
-}
diff --git a/server/lib/deleteSiteAssociatedResources.ts b/server/lib/deleteSiteAssociatedResources.ts
deleted file mode 100644
index 61da82f58..000000000
--- a/server/lib/deleteSiteAssociatedResources.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { and, eq, sql } from "drizzle-orm";
-import {
- db,
- siteNetworks,
- siteResources,
- targets,
- type SiteResource,
- type Transaction
-} from "@server/db";
-import {
- performDeleteResources,
- runResourceDeleteSideEffects,
- type DeleteResourceResult
-} from "@server/lib/deleteResource";
-import {
- performDeleteSiteResources,
- runSiteResourceDeleteSideEffects
-} from "@server/lib/deleteSiteResource";
-import logger from "@server/logger";
-
-export const MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE = 250;
-
-export type DeleteSiteAssociatedResourcesSideEffects = {
- resources: DeleteResourceResult[];
- siteResources: SiteResource[];
-};
-
-export async function getResourceIdsForSite(
- siteId: number,
- trx: Transaction | typeof db = db
-): Promise {
- const rows = await trx
- .selectDistinct({ resourceId: targets.resourceId })
- .from(targets)
- .where(eq(targets.siteId, siteId));
-
- return rows.map((row) => row.resourceId);
-}
-
-export async function getSiteResourceIdsForSite(
- siteId: number,
- orgId: string,
- trx: Transaction | typeof db = db
-): Promise {
- const rows = await trx
- .selectDistinct({ siteResourceId: siteResources.siteResourceId })
- .from(siteNetworks)
- .innerJoin(
- siteResources,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .where(
- and(eq(siteNetworks.siteId, siteId), eq(siteResources.orgId, orgId))
- );
-
- return rows.map((row) => row.siteResourceId);
-}
-
-export async function getAssociatedResourceCountForSite(
- siteId: number,
- orgId: string,
- trx: Transaction | typeof db = db
-): Promise {
- const [publicCountResult, privateCountResult] = await Promise.all([
- trx
- .select({
- count: sql`count(distinct ${targets.resourceId})`
- })
- .from(targets)
- .where(eq(targets.siteId, siteId)),
- trx
- .select({
- count: sql`count(distinct ${siteResources.siteResourceId})`
- })
- .from(siteNetworks)
- .innerJoin(
- siteResources,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .where(
- and(
- eq(siteNetworks.siteId, siteId),
- eq(siteResources.orgId, orgId)
- )
- )
- ]);
-
- return (
- Number(publicCountResult[0]?.count ?? 0) +
- Number(privateCountResult[0]?.count ?? 0)
- );
-}
-
-export function exceedsSiteAssociatedResourceDeleteLimit(
- resourceCount: number
-): boolean {
- return resourceCount > MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE;
-}
-
-export async function deleteAssociatedResourcesForSite(
- siteId: number,
- orgId: string,
- trx: Transaction | typeof db = db
-): Promise {
- const resourceIds = await getResourceIdsForSite(siteId, trx);
- const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
-
- const [resources, siteResourcesDeleted] = await Promise.all([
- performDeleteResources(resourceIds, trx),
- performDeleteSiteResources(siteResourceIds, trx)
- ]);
-
- return { resources, siteResources: siteResourcesDeleted };
-}
-
-export async function runDeleteSiteAssociatedResourcesSideEffects(
- sideEffects: DeleteSiteAssociatedResourcesSideEffects
-): Promise {
- for (const result of sideEffects.resources) {
- await runResourceDeleteSideEffects(result);
- }
-
- for (const removed of sideEffects.siteResources) {
- runSiteResourceDeleteSideEffects(removed);
- }
-}
diff --git a/server/lib/deleteSiteResource.ts b/server/lib/deleteSiteResource.ts
deleted file mode 100644
index 9db5bd902..000000000
--- a/server/lib/deleteSiteResource.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { inArray } from "drizzle-orm";
-import {
- db,
- siteResources,
- type SiteResource,
- type Transaction
-} from "@server/db";
-import logger from "@server/logger";
-import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
-
-export async function performDeleteSiteResources(
- siteResourceIds: number[],
- trx: Transaction | typeof db = db
-): Promise {
- if (siteResourceIds.length === 0) {
- return [];
- }
-
- const removedSiteResources = await trx
- .delete(siteResources)
- .where(inArray(siteResources.siteResourceId, siteResourceIds))
- .returning();
-
- if (removedSiteResources.length > 0) {
- logger.debug(`Deleted ${removedSiteResources.length} site resources`);
- }
-
- return removedSiteResources;
-}
-
-export async function performDeleteSiteResource(
- siteResourceId: number,
- trx: Transaction | typeof db = db
-): Promise {
- const [removedSiteResource] = await performDeleteSiteResources(
- [siteResourceId],
- trx
- );
- return removedSiteResource ?? null;
-}
-
-export function runSiteResourceDeleteSideEffects(
- removedSiteResource: SiteResource
-): void {
- rebuildClientAssociationsFromSiteResource(removedSiteResource).catch(
- (err) => {
- logger.error(
- `Error rebuilding client associations for site resource ${removedSiteResource.siteResourceId}:`,
- err
- );
- }
- );
-}
diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts
index f405a1114..fb32f4f72 100644
--- a/server/lib/exitNodes/exitNodes.ts
+++ b/server/lib/exitNodes/exitNodes.ts
@@ -19,11 +19,7 @@ export async function verifyExitNodeOrgAccess(
export async function listExitNodes(
orgId: string,
filterOnline = false,
- noCloud = false,
- // Accepted for parity with the enterprise implementation (used there for
- // site-label filtering of remote exit nodes). The OSS build has no remote
- // exit nodes, so it is unused here.
- siteId?: number
+ noCloud = false
) {
// TODO: pick which nodes to send and ping better than just all of them that are not remote
const allExitNodes = await db
diff --git a/server/lib/exitNodes/subnet.ts b/server/lib/exitNodes/subnet.ts
index 8c4f3e99e..49e28bd57 100644
--- a/server/lib/exitNodes/subnet.ts
+++ b/server/lib/exitNodes/subnet.ts
@@ -1,55 +1,30 @@
-import { db, exitNodes, Transaction } from "@server/db";
+import { db, exitNodes } from "@server/db";
import config from "@server/lib/config";
import { findNextAvailableCidr } from "@server/lib/ip";
-import { lockManager } from "#dynamic/lib/lock";
-/**
- * Reserves the next available exit node subnet.
- *
- * Exit node subnets must never overlap with one another - regardless of
- * which org(s) they belong to - since HA exit nodes can end up routing for
- * the same org. This acquires a lock that the caller MUST release (via the
- * returned `release`) only after the chosen address has been durably
- * persisted (e.g. after the enclosing transaction commits), otherwise
- * concurrent callers can race and pick the same subnet.
- */
-export async function getNextAvailableSubnet(
- trx: Transaction | typeof db = db
-): Promise<{ value: string; release: () => Promise }> {
- const lockKey = "exit-node-subnet-allocation";
- const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
- if (!acquired) {
- throw new Error(`Failed to acquire lock: ${lockKey}`);
+export async function getNextAvailableSubnet(): Promise {
+ // Get all existing subnets from routes table
+ const existingAddresses = await db
+ .select({
+ address: exitNodes.address
+ })
+ .from(exitNodes);
+
+ const addresses = existingAddresses.map((a) => a.address);
+ let subnet = findNextAvailableCidr(
+ addresses,
+ config.getRawConfig().gerbil.block_size,
+ config.getRawConfig().gerbil.subnet_group
+ );
+ if (!subnet) {
+ throw new Error("No available subnets remaining in space");
}
- const release = () => lockManager.releaseLock(lockKey, acquired);
- try {
- // Get all existing subnets from routes table
- const existingAddresses = await trx
- .select({
- address: exitNodes.address
- })
- .from(exitNodes);
-
- const addresses = existingAddresses.map((a) => a.address);
- let subnet = findNextAvailableCidr(
- addresses,
- config.getRawConfig().gerbil.block_size,
- config.getRawConfig().gerbil.subnet_group
- );
- if (!subnet) {
- throw new Error("No available subnets remaining in space");
- }
-
- // replace the last octet with 1
- subnet =
- subnet.split(".").slice(0, 3).join(".") +
- ".1" +
- "/" +
- subnet.split("/")[1];
- return { value: subnet, release };
- } catch (e) {
- await release();
- throw e;
- }
+ // replace the last octet with 1
+ subnet =
+ subnet.split(".").slice(0, 3).join(".") +
+ ".1" +
+ "/" +
+ subnet.split("/")[1];
+ return subnet;
}
diff --git a/server/lib/ip.ts b/server/lib/ip.ts
index 56576282a..6da8bf887 100644
--- a/server/lib/ip.ts
+++ b/server/lib/ip.ts
@@ -333,7 +333,7 @@ export async function getNextAvailableClientSubnet(
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
- const release = () => lockManager.releaseLock(lockKey, acquired);
+ const release = () => lockManager.releaseLock(lockKey);
try {
const [org] = await transaction
@@ -395,7 +395,7 @@ export async function getNextAvailableAliasAddress(
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
- const release = () => lockManager.releaseLock(lockKey, acquired);
+ const release = () => lockManager.releaseLock(lockKey);
try {
const [org] = await trx
@@ -463,7 +463,7 @@ export async function getNextAvailableOrgSubnet(): Promise<{
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
- const release = () => lockManager.releaseLock(lockKey, acquired);
+ const release = () => lockManager.releaseLock(lockKey);
try {
const existingAddresses = await db
diff --git a/server/lib/lock.ts b/server/lib/lock.ts
index 2f6fad673..7eea89084 100644
--- a/server/lib/lock.ts
+++ b/server/lib/lock.ts
@@ -1,87 +1,28 @@
-import { randomUUID } from "crypto";
-
-const instanceId = `local-${Math.random().toString(36).slice(2)}-${Date.now()}`;
-
-type LocalLockRecord = {
- owner: string;
- expiresAt: number;
-};
-
-const localLocks = new Map();
-
export class LockManager {
- private clearExpiredLocalLock(lockKey: string): void {
- const current = localLocks.get(lockKey);
- if (current && current.expiresAt <= Date.now()) {
- localLocks.delete(lockKey);
- }
- }
-
/**
- * Acquire a local in-process lock using an optimistic Map-based check.
+ * Acquire a distributed lock using Redis SET with NX and PX options
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
- * @returns Promise - a token identifying this specific acquisition
- * (truthy) on success, or null if the lock could not be acquired.
+ * @returns Promise - true if lock acquired, false otherwise
*/
async acquireLock(
lockKey: string,
- ttlMs: number = 30000,
- maxRetries: number = 3,
- retryDelayMs: number = 100
- ): Promise {
- for (let attempt = 0; attempt < maxRetries; attempt++) {
- this.clearExpiredLocalLock(lockKey);
-
- const existing = localLocks.get(lockKey);
- if (!existing) {
- const token = `${instanceId}:${randomUUID()}`;
- localLocks.set(lockKey, {
- owner: token,
- expiresAt: Date.now() + ttlMs
- });
- return token;
- }
-
- // The lock is currently held -- possibly by a different, unrelated
- // caller in this same process. We intentionally do NOT treat
- // same-process holders as automatically reentrant here: two
- // independent logical operations (e.g. two different API requests)
- // running concurrently in the same process must not both believe
- // they hold the lock, or their writes under it can interleave
- // unguarded. Just retry with backoff like any other contended lock.
- if (attempt < maxRetries - 1) {
- const delay = retryDelayMs * Math.pow(2, attempt);
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
- }
-
- return null;
+ ttlMs: number = 30000
+ ): Promise {
+ return true;
}
/**
- * Release a lock previously acquired via acquireLock/acquireLockWithRetry.
+ * Release a lock using Lua script to ensure atomicity
* @param lockKey - Unique identifier for the lock
- * @param token - the exact token returned by the acquisition being released.
- * Required so a caller whose TTL already expired can't delete a
- * different, currently-active holder's lock.
*/
- async releaseLock(lockKey: string, token: string): Promise {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
-
- if (existing && existing.owner === token) {
- localLocks.delete(lockKey);
- }
- }
+ async releaseLock(lockKey: string): Promise {}
/**
* Force release a lock regardless of owner (use with caution)
* @param lockKey - Unique identifier for the lock
*/
- async forceReleaseLock(lockKey: string): Promise {
- localLocks.delete(lockKey);
- }
+ async forceReleaseLock(lockKey: string): Promise {}
/**
* Check if a lock exists and get its info
@@ -94,44 +35,16 @@ export class LockManager {
ttl: number;
owner?: string;
}> {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
-
- if (!existing) {
- return { exists: false, ownedByMe: false, ttl: 0 };
- }
-
- const ttl = Math.max(0, existing.expiresAt - Date.now());
- return {
- exists: true,
- ownedByMe: existing.owner.startsWith(`${instanceId}:`),
- ttl,
- owner: existing.owner.split(":")[0]
- };
+ return { exists: true, ownedByMe: true, ttl: 0 };
}
/**
- * Extend the TTL of an existing lock, provided the token matches the
- * acquisition currently holding it.
+ * Extend the TTL of an existing lock owned by this worker
* @param lockKey - Unique identifier for the lock
* @param ttlMs - New TTL in milliseconds
- * @param token - the token returned by the acquisition being extended
* @returns Promise - true if extended successfully
*/
- async extendLock(
- lockKey: string,
- ttlMs: number,
- token: string
- ): Promise {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
-
- if (!existing || existing.owner !== token) {
- return false;
- }
-
- existing.expiresAt = Date.now() + ttlMs;
- localLocks.set(lockKey, existing);
+ async extendLock(lockKey: string, ttlMs: number): Promise {
return true;
}
@@ -141,34 +54,15 @@ export class LockManager {
* @param ttlMs - Time to live in milliseconds
* @param maxRetries - Maximum number of retry attempts
* @param baseDelayMs - Base delay between retries in milliseconds
- * @returns Promise - token if acquired, null otherwise
+ * @returns Promise - true if lock acquired
*/
async acquireLockWithRetry(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 5,
baseDelayMs: number = 100
- ): Promise {
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
- const acquired = await this.acquireLock(
- lockKey,
- ttlMs,
- 1,
- baseDelayMs
- );
-
- if (acquired) {
- return acquired;
- }
-
- if (attempt < maxRetries) {
- const delay =
- baseDelayMs * Math.pow(2, attempt) + Math.random() * 100;
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
- }
-
- return null;
+ ): Promise {
+ return true;
}
/**
@@ -183,16 +77,16 @@ export class LockManager {
fn: () => Promise,
ttlMs: number = 30000
): Promise {
- const token = await this.acquireLock(lockKey, ttlMs);
+ const acquired = await this.acquireLock(lockKey, ttlMs);
- if (!token) {
+ if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
try {
return await fn();
} finally {
- await this.releaseLock(lockKey, token);
+ await this.releaseLock(lockKey);
}
}
@@ -205,21 +99,7 @@ export class LockManager {
activeLocksCount: number;
locksOwnedByMe: number;
}> {
- const now = Date.now();
- for (const [key, value] of localLocks.entries()) {
- if (value.expiresAt <= now) {
- localLocks.delete(key);
- }
- }
-
- let locksOwnedByMe = 0;
- for (const value of localLocks.values()) {
- if (value.owner.startsWith(`${instanceId}:`)) {
- locksOwnedByMe++;
- }
- }
-
- return { activeLocksCount: localLocks.size, locksOwnedByMe };
+ return { activeLocksCount: 0, locksOwnedByMe: 0 };
}
/**
diff --git a/server/lib/orgRebuildCounter.ts b/server/lib/orgRebuildCounter.ts
deleted file mode 100644
index 40edd0aa0..000000000
--- a/server/lib/orgRebuildCounter.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export const ORG_REBUILD_CONCURRENCY_LIMIT = 10;
-
-const orgActiveRebuilds = new Map();
-
-export async function incrementOrgRebuildCount(orgId: string): Promise {
- orgActiveRebuilds.set(orgId, (orgActiveRebuilds.get(orgId) ?? 0) + 1);
-}
-
-export async function decrementOrgRebuildCount(orgId: string): Promise {
- const current = orgActiveRebuilds.get(orgId) ?? 0;
- if (current <= 1) {
- orgActiveRebuilds.delete(orgId);
- } else {
- orgActiveRebuilds.set(orgId, current - 1);
- }
-}
-
-export async function getOrgActiveRebuildCount(orgId: string): Promise {
- return orgActiveRebuilds.get(orgId) ?? 0;
-}
-
-export async function checkOrgRebuildRateLimit(orgId: string): Promise {
- return (orgActiveRebuilds.get(orgId) ?? 0) >= ORG_REBUILD_CONCURRENCY_LIMIT;
-}
diff --git a/server/lib/pathMatch.ts b/server/lib/pathMatch.ts
deleted file mode 100644
index a007f9ec3..000000000
--- a/server/lib/pathMatch.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-const MAX_RECURSION_DEPTH = 100;
-
-const segmentRegexCache = new Map();
-
-function getSegmentRegex(patternPart: string): RegExp {
- let regex = segmentRegexCache.get(patternPart);
- if (!regex) {
- const regexPattern = patternPart
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
- .replace(/\*/g, ".*")
- .replace(/\?/g, ".");
- regex = new RegExp(`^${regexPattern}$`);
- segmentRegexCache.set(patternPart, regex);
- }
- return regex;
-}
-
-export function isPathAllowed(pattern: string, path: string): boolean {
- const normalize = (p: string) => p.split("/").filter(Boolean);
- const patternParts = normalize(pattern);
- const pathParts = normalize(path);
-
- function matchSegments(
- patternIndex: number,
- pathIndex: number,
- depth: number = 0
- ): boolean {
- if (depth > MAX_RECURSION_DEPTH) {
- return false;
- }
-
- const currentPatternPart = patternParts[patternIndex];
- const currentPathPart = pathParts[pathIndex];
-
- if (patternIndex >= patternParts.length) {
- return pathIndex >= pathParts.length;
- }
-
- if (pathIndex >= pathParts.length) {
- return patternParts.slice(patternIndex).every((p) => p === "*");
- }
-
- if (currentPatternPart === "*") {
- if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
- return true;
- }
- if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
- return true;
- }
- return false;
- }
-
- if (currentPatternPart.includes("*")) {
- const regex = getSegmentRegex(currentPatternPart);
-
- if (regex.test(currentPathPart)) {
- return matchSegments(
- patternIndex + 1,
- pathIndex + 1,
- depth + 1
- );
- }
- return false;
- }
-
- if (currentPatternPart !== currentPathPart) {
- return false;
- }
-
- return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
- }
-
- return matchSegments(0, 0, 0);
-}
diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts
index 6b6ac95aa..c3e796fc1 100644
--- a/server/lib/readConfigFile.ts
+++ b/server/lib/readConfigFile.ts
@@ -184,8 +184,7 @@ export const configSchema = z
.number()
.positive()
.optional()
- .default(5000),
- jit_mode: z.boolean().default(true)
+ .default(5000)
})
.optional()
.prefault({})
diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts
index efb856825..4efc72476 100644
--- a/server/lib/rebuildClientAssociations.ts
+++ b/server/lib/rebuildClientAssociations.ts
@@ -8,7 +8,6 @@ import {
exitNodes,
newts,
olms,
- primaryDb,
roleSiteResources,
Site,
SiteResource,
@@ -21,10 +20,10 @@ import {
} from "@server/db";
import { and, count, eq, inArray, ne } from "drizzle-orm";
-import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
+import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers";
import {
- initPeerAddHandshakeBatch,
- deletePeersBatch as olmDeletePeersBatch
+ initPeerAddHandshake,
+ deletePeer as olmDeletePeer
} from "@server/routers/olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import logger from "@server/logger";
@@ -36,139 +35,16 @@ import {
} from "@server/lib/ip";
import {
addPeerData,
- addPeerDataBatch,
- addTargetsBatch as addSubnetProxyTargetsBatch,
- removePeerDataBatch,
- removeTargetsBatch as removeSubnetProxyTargetsBatch,
- updatePeerDataBatch,
- updateTargets
+ addTargets as addSubnetProxyTargets,
+ removePeerData,
+ removeTargets as removeSubnetProxyTargets
} from "@server/routers/client/targets";
import { lockManager } from "#dynamic/lib/lock";
-import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
-import { withRetry, isTransientError } from "@server/lib/dbRetry";
-import {
- checkOrgRebuildRateLimit,
- decrementOrgRebuildCount,
- incrementOrgRebuildCount,
- ORG_REBUILD_CONCURRENCY_LIMIT
-} from "#dynamic/lib/orgRebuildCounter";
-
-export { ORG_REBUILD_CONCURRENCY_LIMIT };
// TTL for rebuild-association locks. These functions can fan out into many
// peer/proxy updates, so give them a generous window.
const REBUILD_ASSOCIATIONS_LOCK_TTL_MS = 120000;
-export async function isOrgRebuildRateLimited(orgId: string): Promise {
- return checkOrgRebuildRateLimit(orgId);
-}
-
-const REBUILD_IDLE_POLL_INTERVAL_MS = 300;
-const REBUILD_IDLE_DEFAULT_TIMEOUT_MS = 130_000; // slightly longer than lock TTL
-const REBUILD_IDLE_HANDLER_TIMEOUT_MS = 5_000;
-
-/**
- * Returns true if a rebuild for the given site resource is currently active
- * (holding the distributed lock) or is pending in the rebuild queue.
- */
-export async function hasActiveSiteResourceRebuild(
- siteResourceId: number
-): Promise {
- const lockKey = `rebuild-client-associations:site-resource:${siteResourceId}`;
- const lockInfo = await lockManager.getLockInfo(lockKey);
- if (lockInfo.exists) return true;
- return rebuildQueue.isQueued({ type: "site-resource", id: siteResourceId });
-}
-
-/**
- * Resolves once there is no active or queued rebuild for the given site resource.
- * Logs a warning and resolves early if the timeout is reached.
- */
-export async function waitForSiteResourceRebuildIdle(
- siteResourceId: number,
- timeoutMs = REBUILD_IDLE_DEFAULT_TIMEOUT_MS
-): Promise {
- const deadline = Date.now() + timeoutMs;
- while (Date.now() < deadline) {
- if (!(await hasActiveSiteResourceRebuild(siteResourceId))) return;
- await new Promise((r) =>
- setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
- );
- }
- logger.warn(
- `waitForSiteResourceRebuildIdle: timed out after ${timeoutMs}ms waiting for siteResourceId=${siteResourceId}`
- );
-}
-
-/**
- * Resolves once there are no active or queued rebuilds for any site resource
- * associated with the given site.
- */
-export async function waitForSiteRebuildIdle(
- siteId: number,
- timeoutMs = REBUILD_IDLE_HANDLER_TIMEOUT_MS
-): Promise {
- const deadline = Date.now() + timeoutMs;
- while (Date.now() < deadline) {
- const resourceRows = await db
- .select({ siteResourceId: siteResources.siteResourceId })
- .from(siteResources)
- .innerJoin(
- siteNetworks,
- eq(siteNetworks.networkId, siteResources.networkId)
- )
- .where(eq(siteNetworks.siteId, siteId));
- let allIdle = true;
- for (const { siteResourceId } of resourceRows) {
- if (await hasActiveSiteResourceRebuild(siteResourceId)) {
- allIdle = false;
- break;
- }
- }
- if (allIdle) return;
- await new Promise((r) =>
- setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
- );
- }
- logger.warn(
- `waitForSiteRebuildIdle: timed out after ${timeoutMs}ms waiting for siteId=${siteId}`
- );
-}
-
-/**
- * Resolves once there are no active or queued rebuilds for any site resource
- * associated with the given client.
- */
-export async function waitForClientRebuildIdle(
- clientId: number,
- timeoutMs = REBUILD_IDLE_HANDLER_TIMEOUT_MS
-): Promise {
- const deadline = Date.now() + timeoutMs;
- while (Date.now() < deadline) {
- const resourceRows = await db
- .select({
- siteResourceId:
- clientSiteResourcesAssociationsCache.siteResourceId
- })
- .from(clientSiteResourcesAssociationsCache)
- .where(eq(clientSiteResourcesAssociationsCache.clientId, clientId));
- let allIdle = true;
- for (const { siteResourceId } of resourceRows) {
- if (await hasActiveSiteResourceRebuild(siteResourceId)) {
- allIdle = false;
- break;
- }
- }
- if (allIdle) return;
- await new Promise((r) =>
- setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
- );
- }
- logger.warn(
- `waitForClientRebuildIdle: timed out after ${timeoutMs}ms waiting for clientId=${clientId}`
- );
-}
-
export async function getClientSiteResourceAccess(
siteResource: SiteResource,
trx: Transaction | typeof db = db
@@ -282,61 +158,32 @@ export async function getClientSiteResourceAccess(
}
export async function rebuildClientAssociationsFromSiteResource(
- siteResource: SiteResource
-) {
- await incrementOrgRebuildCount(siteResource.orgId);
- try {
- // The whole locked rebuild is idempotent (it diffs full expected vs.
- // actual state each time), so on a transient DB error it's safe to
- // retry the entire thing rather than just the failed query.
- return await withRetry(
- () =>
- lockManager.withLock(
- `rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
- () =>
- rebuildClientAssociationsFromSiteResourceImpl(
- siteResource
- ),
- REBUILD_ASSOCIATIONS_LOCK_TTL_MS
- ),
- `rebuildClientAssociationsFromSiteResource:${siteResource.siteResourceId}`
- );
- } catch (err: any) {
- if (
- typeof err?.message === "string" &&
- err.message.startsWith("Failed to acquire lock")
- ) {
- logger.warn(
- `rebuildClientAssociations: could not acquire lock for site resource ${siteResource.siteResourceId}, queuing for deferred processing`
- );
- await rebuildQueue.enqueue({
- type: "site-resource",
- id: siteResource.siteResourceId
- });
- return { mergedAllClients: [] };
- }
- if (isTransientError(err)) {
- logger.warn(
- `rebuildClientAssociations: transient DB error rebuilding site resource ${siteResource.siteResourceId} persisted after retries, queuing for deferred processing:`,
- err
- );
- await rebuildQueue.enqueue({
- type: "site-resource",
- id: siteResource.siteResourceId
- });
- return { mergedAllClients: [] };
- }
- throw err;
- } finally {
- await decrementOrgRebuildCount(siteResource.orgId);
- }
+ siteResource: SiteResource,
+ trx: Transaction | typeof db = db
+): Promise<{
+ mergedAllClients: {
+ clientId: number;
+ pubKey: string | null;
+ subnet: string | null;
+ }[];
+}> {
+ return await lockManager.withLock(
+ `rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
+ () => rebuildClientAssociationsFromSiteResourceImpl(siteResource, trx),
+ REBUILD_ASSOCIATIONS_LOCK_TTL_MS
+ );
}
async function rebuildClientAssociationsFromSiteResourceImpl(
- siteResource: SiteResource
-) {
- const trx = primaryDb;
-
+ siteResource: SiteResource,
+ trx: Transaction | typeof db = db
+): Promise<{
+ mergedAllClients: {
+ clientId: number;
+ pubKey: string | null;
+ subnet: string | null;
+ }[];
+}> {
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] START siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} orgId=${siteResource.orgId}`
);
@@ -350,62 +197,14 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
/////////// process the client-siteResource associations ///////////
- const existingClientSiteResources = await trx
- .select({
- clientId: clientSiteResourcesAssociationsCache.clientId
- })
- .from(clientSiteResourcesAssociationsCache)
- .where(
- eq(
- clientSiteResourcesAssociationsCache.siteResourceId,
- siteResource.siteResourceId
- )
- );
-
- const existingClientSiteResourceIds = existingClientSiteResources.map(
- (row) => row.clientId
- );
-
// get all of the clients associated with other site resources that share
// any of the same sites as this site resource (via siteNetworks). We can't
// simply filter by networkId since each site resource has its own network;
// two site resources serving the same site typically belong to different
// networks that both happen to include the site through siteNetworks.
const sitesListSiteIds = sitesList.map((s) => s.siteId);
-
- // We must also consider sites where these clients are currently cached,
- // otherwise removing a site from this resource can leave stale
- // client-site cache entries behind for the removed site.
- const cachedSiteRowsForResourceClients =
- existingClientSiteResourceIds.length > 0
- ? await trx
- .select({ siteId: clientSitesAssociationsCache.siteId })
- .from(clientSitesAssociationsCache)
- .where(
- inArray(
- clientSitesAssociationsCache.clientId,
- existingClientSiteResourceIds
- )
- )
- : [];
-
- const allCandidateSiteIds = Array.from(
- new Set([
- ...sitesListSiteIds,
- ...cachedSiteRowsForResourceClients.map((r) => r.siteId)
- ])
- );
-
- const sitesToProcess =
- allCandidateSiteIds.length > 0
- ? await trx
- .select()
- .from(sites)
- .where(inArray(sites.siteId, allCandidateSiteIds))
- : [];
- const currentSiteIdSet = new Set(sitesListSiteIds);
const allUpdatedClientsFromOtherResourcesOnThisSite =
- allCandidateSiteIds.length > 0
+ sitesListSiteIds.length > 0
? await trx
.select({
clientId: clientSiteResourcesAssociationsCache.clientId,
@@ -425,7 +224,7 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
)
.where(
and(
- inArray(siteNetworks.siteId, allCandidateSiteIds),
+ inArray(siteNetworks.siteId, sitesListSiteIds),
ne(
siteResources.siteResourceId,
siteResource.siteResourceId
@@ -444,6 +243,22 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
clientsFromOtherResourcesBySite.get(row.siteId)!.add(row.clientId);
}
+ const existingClientSiteResources = await trx
+ .select({
+ clientId: clientSiteResourcesAssociationsCache.clientId
+ })
+ .from(clientSiteResourcesAssociationsCache)
+ .where(
+ eq(
+ clientSiteResourcesAssociationsCache.siteResourceId,
+ siteResource.siteResourceId
+ )
+ );
+
+ const existingClientSiteResourceIds = existingClientSiteResources.map(
+ (row) => row.clientId
+ );
+
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} existingResourceClientIds=[${existingClientSiteResourceIds.join(", ")}]`
);
@@ -485,7 +300,6 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
await trx
.insert(clientSiteResourcesAssociationsCache)
.values(clientSiteResourcesToInsert)
- .onConflictDoNothing()
.returning();
logger.debug(
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} inserted clientSiteResource associations`
@@ -527,154 +341,121 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
/////////// process the client-site associations ///////////
logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} beginning client-site association loop over ${sitesToProcess.length} site(s) (current=${sitesList.length})`
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} beginning client-site association loop over ${sitesList.length} site(s)`
);
- for (const site of sitesToProcess) {
+ for (const site of sitesList) {
const siteId = site.siteId;
- try {
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] processing siteId=${siteId} for siteResourceId=${siteResource.siteResourceId}`
- );
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] processing siteId=${siteId} for siteResourceId=${siteResource.siteResourceId}`
+ );
- const existingClientSites = await trx
- .select({
- clientId: clientSitesAssociationsCache.clientId
- })
- .from(clientSitesAssociationsCache)
- .where(eq(clientSitesAssociationsCache.siteId, siteId));
+ const existingClientSites = await trx
+ .select({
+ clientId: clientSitesAssociationsCache.clientId
+ })
+ .from(clientSitesAssociationsCache)
+ .where(eq(clientSitesAssociationsCache.siteId, siteId));
- const existingClientSiteIds = existingClientSites.map(
- (row) => row.clientId
- );
+ const existingClientSiteIds = existingClientSites.map(
+ (row) => row.clientId
+ );
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} existingClientSiteIds=[${existingClientSiteIds.join(", ")}]`
- );
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} existingClientSiteIds=[${existingClientSiteIds.join(", ")}]`
+ );
- // Get full client details for existing clients (needed for sending delete messages)
- const existingClients =
- existingClientSiteIds.length > 0
- ? await trx
- .select({
- clientId: clients.clientId,
- pubKey: clients.pubKey,
- subnet: clients.subnet
- })
- .from(clients)
- .where(
- inArray(clients.clientId, existingClientSiteIds)
- )
- : [];
-
- const otherResourceClientIds =
- clientsFromOtherResourcesBySite.get(siteId) ??
- new Set();
-
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
- );
-
- // Expected clients from this resource are site-scoped: if this site is
- // no longer attached to the resource, the expected set is empty.
- const expectedClientIdsForSite = currentSiteIdSet.has(siteId)
- ? mergedAllClientIds
+ // Get full client details for existing clients (needed for sending delete messages)
+ const existingClients =
+ existingClientSiteIds.length > 0
+ ? await trx
+ .select({
+ clientId: clients.clientId,
+ pubKey: clients.pubKey,
+ subnet: clients.subnet
+ })
+ .from(clients)
+ .where(inArray(clients.clientId, existingClientSiteIds))
: [];
- // Note: we deliberately do NOT exclude clients covered by another
- // site resource here (unlike clientSitesToRemove below). Doing so
- // previously caused a permanent gap: if resource A saw resource B's
- // cache row and skipped adding (assuming B would maintain it), and
- // B's own rebuild made the same assumption about A, the site-level
- // row could end up never inserted by anyone even though both
- // resources' client associations were otherwise correct.
- // onConflictDoNothing makes a redundant insert harmless, so there's
- // no correctness reason to skip here.
- const clientSitesToAdd = expectedClientIdsForSite.filter(
- (clientId) => !existingClientSiteIds.includes(clientId)
- );
+ const otherResourceClientIds =
+ clientsFromOtherResourcesBySite.get(siteId) ?? new Set();
- const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
- clientId,
- siteId
- }));
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
+ );
+ const clientSitesToAdd = mergedAllClientIds.filter(
+ (clientId) =>
+ !existingClientSiteIds.includes(clientId) &&
+ !otherResourceClientIds.has(clientId) // dont add if already connected via another site resource
+ );
+
+ const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
+ clientId,
+ siteId
+ }));
+
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toAdd=[${clientSitesToAdd.join(", ")}]`
+ );
+
+ if (clientSitesToInsert.length > 0) {
logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toAdd=[${clientSitesToAdd.join(", ")}]`
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserting ${clientSitesToInsert.length} clientSite association(s)`
);
-
- if (clientSitesToInsert.length > 0) {
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserting ${clientSitesToInsert.length} clientSite association(s)`
- );
- await trx
- .insert(clientSitesAssociationsCache)
- .values(clientSitesToInsert)
- .onConflictDoNothing()
- .returning();
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserted clientSite associations`
- );
- } else {
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} no clientSite associations to insert`
- );
- }
-
- // Now remove any client-site associations that should no longer exist
- const clientSitesToRemove = existingClientSiteIds.filter(
- (clientId) =>
- !expectedClientIdsForSite.includes(clientId) &&
- !otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
- );
-
+ await trx
+ .insert(clientSitesAssociationsCache)
+ .values(clientSitesToInsert)
+ .returning();
logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toRemove=[${clientSitesToRemove.join(", ")}]`
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserted clientSite associations`
);
-
- if (clientSitesToRemove.length > 0) {
- logger.debug(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} deleting ${clientSitesToRemove.length} clientSite association(s)`
- );
- await trx
- .delete(clientSitesAssociationsCache)
- .where(
- and(
- eq(clientSitesAssociationsCache.siteId, siteId),
- inArray(
- clientSitesAssociationsCache.clientId,
- clientSitesToRemove
- )
- )
- );
- }
-
- // Now handle the messages to add/remove peers on both the newt and olm sides
- await handleMessagesForSiteClients(
- site,
- siteId,
- mergedAllClients,
- existingClients,
- clientSitesToAdd,
- clientSitesToRemove,
- trx
+ } else {
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} no clientSite associations to insert`
);
- } catch (err) {
- // Don't let a failure on one site abort processing of every
- // other site queued after it in this run. Since we're not
- // re-throwing, the outer wrapper's retry/requeue logic never
- // sees this failure, so explicitly queue this resource for a
- // follow-up pass to reconcile whatever this site didn't get to.
- logger.error(
- `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} failed while processing site for siteResourceId=${siteResource.siteResourceId}, continuing with remaining sites and queuing a follow-up pass:`,
- err
- );
- await rebuildQueue.enqueue({
- type: "site-resource",
- id: siteResource.siteResourceId
- });
}
+
+ // Now remove any client-site associations that should no longer exist
+ const clientSitesToRemove = existingClientSiteIds.filter(
+ (clientId) =>
+ !mergedAllClientIds.includes(clientId) &&
+ !otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
+ );
+
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toRemove=[${clientSitesToRemove.join(", ")}]`
+ );
+
+ if (clientSitesToRemove.length > 0) {
+ logger.debug(
+ `rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} deleting ${clientSitesToRemove.length} clientSite association(s)`
+ );
+ await trx
+ .delete(clientSitesAssociationsCache)
+ .where(
+ and(
+ eq(clientSitesAssociationsCache.siteId, siteId),
+ inArray(
+ clientSitesAssociationsCache.clientId,
+ clientSitesToRemove
+ )
+ )
+ );
+ }
+
+ // Now handle the messages to add/remove peers on both the newt and olm sides
+ await handleMessagesForSiteClients(
+ site,
+ siteId,
+ mergedAllClients,
+ existingClients,
+ clientSitesToAdd,
+ clientSitesToRemove,
+ trx
+ );
}
// Handle subnet proxy target updates for the resource associations
@@ -687,6 +468,10 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
clientSiteResourcesToRemove,
trx
);
+
+ return {
+ mergedAllClients
+ };
}
async function handleMessagesForSiteClients(
@@ -707,7 +492,7 @@ async function handleMessagesForSiteClients(
trx: Transaction | typeof db = db
): Promise {
if (!site.exitNodeId) {
- logger.debug(
+ logger.warn(
`Exit node ID not on site ${site.siteId} so there is no reason to update clients because it must be offline`
);
return;
@@ -721,14 +506,14 @@ async function handleMessagesForSiteClients(
.limit(1);
if (!exitNode) {
- logger.debug(
+ logger.warn(
`Exit node not found for site ${site.siteId} so there is no reason to update clients because it must be offline`
);
return;
}
if (!site.publicKey) {
- logger.debug(
+ logger.warn(
`Site publicKey not set for site ${site.siteId} so cannot add peers to clients`
);
return;
@@ -742,7 +527,7 @@ async function handleMessagesForSiteClients(
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
- logger.debug(
+ logger.warn(
`Newt not found for site ${siteId} so cannot add peers to clients`
);
return;
@@ -751,28 +536,6 @@ async function handleMessagesForSiteClients(
const newtJobs: Promise[] = [];
const olmJobs: Promise[] = [];
const exitNodeJobs: Promise[] = [];
- const newtPeerDeletes: {
- siteId: number;
- publicKey: string;
- newtId: string;
- }[] = [];
- const olmPeerDeletes: {
- clientId: number;
- siteId: number;
- publicKey: string;
- olmId: string;
- }[] = [];
- const olmPeerAddHandshakes: {
- clientId: number;
- peer: {
- siteId: number;
- exitNode: {
- publicKey: string;
- endpoint: string;
- };
- };
- olmId: string;
- }[] = [];
// Combine all clients that need processing (those being added or removed)
const clientsToProcess = new Map<
@@ -821,21 +584,6 @@ async function handleMessagesForSiteClients(
}
}
- // Batch-fetch all olm IDs for the clients we need to process
- const clientIdsToProcess = Array.from(clientsToProcess.keys());
- const olmRows =
- clientIdsToProcess.length > 0
- ? await trx
- .select({ olmId: olms.olmId, clientId: olms.clientId })
- .from(olms)
- .where(inArray(olms.clientId, clientIdsToProcess))
- : [];
- const olmByClientId = new Map(
- olmRows
- .filter((r) => r.clientId !== null)
- .map((r) => [r.clientId as number, r.olmId])
- );
-
for (const client of clientsToProcess.values()) {
// UPDATE THE NEWT
if (!client.subnet || !client.pubKey) {
@@ -852,8 +600,14 @@ async function handleMessagesForSiteClients(
continue;
}
- const olmId = olmByClientId.get(client.clientId);
- if (!olmId) {
+ const [olm] = await trx
+ .select({
+ olmId: olms.olmId
+ })
+ .from(olms)
+ .where(eq(olms.clientId, client.clientId))
+ .limit(1);
+ if (!olm) {
logger.warn(
`Olm not found for client ${client.clientId} so cannot add/delete peers`
);
@@ -861,17 +615,15 @@ async function handleMessagesForSiteClients(
}
if (isDelete) {
- newtPeerDeletes.push({
- siteId,
- publicKey: client.pubKey,
- newtId: newt.newtId
- });
- olmPeerDeletes.push({
- clientId: client.clientId,
- siteId,
- publicKey: site.publicKey,
- olmId
- });
+ newtJobs.push(newtDeletePeer(siteId, client.pubKey, newt.newtId));
+ olmJobs.push(
+ olmDeletePeer(
+ client.clientId,
+ siteId,
+ site.publicKey,
+ olm.olmId
+ )
+ );
}
if (isAdd) {
@@ -883,34 +635,23 @@ async function handleMessagesForSiteClients(
continue;
}
- olmPeerAddHandshakes.push({
- clientId: client.clientId,
- peer: {
+ await initPeerAddHandshake(
+ // this will kick off the add peer process for the client
+ client.clientId,
+ {
siteId,
exitNode: {
publicKey: exitNode.publicKey,
endpoint: exitNode.endpoint
}
},
- olmId
- });
+ olm.olmId
+ );
}
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
}
- if (newtPeerDeletes.length > 0) {
- newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
- }
-
- if (olmPeerDeletes.length > 0) {
- olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
- }
-
- if (olmPeerAddHandshakes.length > 0) {
- olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
- }
-
Promise.all(exitNodeJobs).catch((error) => {
logger.error(
`rebuildClientAssociations: Error updating client site destinations for site ${site.siteId}:`,
@@ -967,7 +708,7 @@ export async function updateClientSiteDestinations(
for (const site of sitesData) {
if (!site.sites.subnet) {
- logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
+ logger.warn(`Site ${site.sites.siteId} has no subnet, skipping`);
continue;
}
@@ -1071,20 +812,6 @@ async function handleSubnetProxyTargetUpdates(
): Promise {
const proxyJobs: Promise[] = [];
const olmJobs: Promise[] = [];
- const targetsToAddBatch: {
- newtId: string;
- targets: NonNullable<
- Awaited>
- >;
- version: string | null;
- }[] = [];
- const targetsToRemoveBatch: {
- newtId: string;
- targets: NonNullable<
- Awaited>
- >;
- version: string | null;
- }[] = [];
for (const siteData of sitesList) {
const siteId = siteData.siteId;
@@ -1116,25 +843,25 @@ async function handleSubnetProxyTargetUpdates(
);
if (targetsToAdd) {
- targetsToAddBatch.push({
- newtId: newt.newtId,
- targets: targetsToAdd,
- version: newt.version
- });
+ proxyJobs.push(
+ addSubnetProxyTargets(
+ newt.newtId,
+ targetsToAdd,
+ newt.version
+ )
+ );
}
- olmJobs.push(
- addPeerDataBatch(
- addedClients.map((client) => ({
- clientId: client.clientId,
+ for (const client of addedClients) {
+ olmJobs.push(
+ addPeerData(
+ client.clientId,
siteId,
- remoteSubnets: generateRemoteSubnets([
- siteResource
- ]),
- aliases: generateAliasConfig([siteResource])
- }))
- )
- );
+ generateRemoteSubnets([siteResource]),
+ generateAliasConfig([siteResource])
+ )
+ );
+ }
}
}
@@ -1153,20 +880,15 @@ async function handleSubnetProxyTargetUpdates(
);
if (targetsToRemove) {
- targetsToRemoveBatch.push({
- newtId: newt.newtId,
- targets: targetsToRemove,
- version: newt.version
- });
+ proxyJobs.push(
+ removeSubnetProxyTargets(
+ newt.newtId,
+ targetsToRemove,
+ newt.version
+ )
+ );
}
- const peerDataRemovals: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: ReturnType;
- }[] = [];
-
for (const client of removedClients) {
if (!siteResource.destination) {
continue;
@@ -1214,539 +936,31 @@ async function handleSubnetProxyTargetUpdates(
? []
: generateRemoteSubnets([siteResource]);
- peerDataRemovals.push({
- clientId: client.clientId,
- siteId,
- remoteSubnets: remoteSubnetsToRemove,
- aliases: generateAliasConfig([siteResource])
- });
- }
-
- if (peerDataRemovals.length > 0) {
- olmJobs.push(removePeerDataBatch(peerDataRemovals));
- }
- }
- }
- }
-
- if (targetsToAddBatch.length > 0) {
- proxyJobs.push(addSubnetProxyTargetsBatch(targetsToAddBatch));
- }
-
- if (targetsToRemoveBatch.length > 0) {
- proxyJobs.push(removeSubnetProxyTargetsBatch(targetsToRemoveBatch));
- }
-
- await Promise.all([...proxyJobs, ...olmJobs]);
-}
-
-export async function handleMessagingForUpdatedSiteResource(
- existingSiteResource: SiteResource | undefined,
- updatedSiteResource: SiteResource,
- existingSiteIds: number[],
- updatedSiteIds: number[]
-) {
- const trx = primaryDb;
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: START siteResourceId=${updatedSiteResource.siteResourceId} existingSiteIds=[${existingSiteIds.join(", ")}] updatedSiteIds=[${updatedSiteIds.join(", ")}]`
- );
-
- logger.debug(
- "handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
- existingSiteResource
- );
- logger.debug(
- "handleMessagingForUpdatedSiteResource: updatedSiteResource is: ",
- updatedSiteResource
- );
-
- const allSiteIds = [...new Set([...existingSiteIds, ...updatedSiteIds])];
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: allSiteIds=[${allSiteIds.join(", ")}] count=${allSiteIds.length}`
- );
-
- const newtsForSites =
- allSiteIds.length > 0
- ? await trx
- .select()
- .from(newts)
- .where(inArray(newts.siteId, allSiteIds))
- : [];
- const newtBySiteId = new Map(
- newtsForSites.map((newt) => [newt.siteId, newt])
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: fetched newts for ${newtsForSites.length}/${allSiteIds.length} site(s)`
- );
-
- // WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED
- const mergedAllClients = await trx
- .select({
- clientId: clientSiteResourcesAssociationsCache.clientId,
- pubKey: clients.pubKey,
- subnet: clients.subnet
- })
- .from(clientSiteResourcesAssociationsCache)
- .innerJoin(
- clients,
- eq(clientSiteResourcesAssociationsCache.clientId, clients.clientId)
- )
- .where(
- eq(
- clientSiteResourcesAssociationsCache.siteResourceId,
- updatedSiteResource.siteResourceId
- )
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: resolved merged clients count=${mergedAllClients.length} clientIds=[${mergedAllClients.map((c) => c.clientId).join(", ")}]`
- );
-
- const targets = await generateSubnetProxyTargetV2(
- updatedSiteResource,
- mergedAllClients
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: generated updated targets count=${targets ? targets.length : 0}`
- );
-
- const oldDestinationStillInUseClientSitePairs = new Set();
- if (
- existingSiteResource?.destination &&
- allSiteIds.length > 0 &&
- mergedAllClients.length > 0
- ) {
- logger.debug(
- `handleMessagingForUpdatedSiteResource: checking old destination reuse destination=${existingSiteResource.destination} across siteCount=${allSiteIds.length} clientCount=${mergedAllClients.length}`
- );
-
- // we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
- const oldDestinationStillInUseRows = await trx
- .select({
- clientId: clientSiteResourcesAssociationsCache.clientId,
- siteId: siteNetworks.siteId
- })
- .from(siteResources)
- .innerJoin(
- clientSiteResourcesAssociationsCache,
- eq(
- clientSiteResourcesAssociationsCache.siteResourceId,
- siteResources.siteResourceId
- )
- )
- .innerJoin(
- siteNetworks,
- eq(siteNetworks.networkId, siteResources.networkId)
- )
- .where(
- and(
- inArray(
- clientSiteResourcesAssociationsCache.clientId,
- mergedAllClients.map((c) => c.clientId)
- ),
- inArray(siteNetworks.siteId, allSiteIds),
- eq(
- siteResources.destination,
- existingSiteResource.destination
- ),
- ne(
- siteResources.siteResourceId,
- existingSiteResource.siteResourceId
- )
- )
- );
-
- for (const row of oldDestinationStillInUseRows) {
- oldDestinationStillInUseClientSitePairs.add(
- `${row.clientId}:${row.siteId}`
- );
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: old destination still in use rows=${oldDestinationStillInUseRows.length} uniqueClientSitePairs=${oldDestinationStillInUseClientSitePairs.size}`
- );
- } else {
- logger.debug(
- "handleMessagingForUpdatedSiteResource: skipping old destination reuse check (missing existing destination or no sites/clients)"
- );
- }
-
- //////////////////////////// FROM HERE DOWN WE ARE DEALING WITH REMOVING SITES
- const removedSiteIds = existingSiteIds.filter(
- (id) => !updatedSiteIds.includes(id)
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: removing sites removedSiteIds=[${removedSiteIds.join(", ")}] count=${removedSiteIds.length}`
- );
-
- const targetsToRemoveBatch: {
- newtId: string;
- targets: any[];
- version: string | null;
- }[] = [];
- const peerDataRemoves: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: ReturnType;
- }[] = [];
- if (targets) {
- for (const siteId of removedSiteIds) {
- const newt = newtBySiteId.get(siteId);
- if (!newt) {
- logger.debug(
- `handleMessagingForUpdatedSiteResource: skipping remove for siteId=${siteId} because no newt found`
- );
- continue;
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: preparing remove batches for siteId=${siteId} newtId=${newt.newtId}`
- );
-
- targetsToRemoveBatch.push({
- newtId: newt.newtId,
- targets: targets,
- version: newt.version
- });
- for (const client of mergedAllClients) {
- // we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
- const oldDestinationStillInUseBySite =
- oldDestinationStillInUseClientSitePairs.has(
- `${client.clientId}:${siteId}`
+ olmJobs.push(
+ removePeerData(
+ client.clientId,
+ siteId,
+ remoteSubnetsToRemove,
+ generateAliasConfig([siteResource])
+ )
);
-
- if (existingSiteResource) {
- peerDataRemoves.push({
- // this might happen twice after the rebuild function but that is okay
- clientId: client.clientId,
- siteId,
- remoteSubnets: !oldDestinationStillInUseBySite
- ? generateRemoteSubnets([existingSiteResource])
- : [],
- aliases: generateAliasConfig([existingSiteResource])
- });
}
}
}
- } else {
- logger.debug(
- "handleMessagingForUpdatedSiteResource: skipping removal batch generation because targets were empty"
- );
}
- logger.debug(
- `handleMessagingForUpdatedSiteResource: remove batches prepared targetBatchCount=${targetsToRemoveBatch.length} peerDataCount=${peerDataRemoves.length}`
- );
-
- logger.debug(
- "handleMessagingForUpdatedSiteResource: dispatching removeSubnetProxyTargetsBatch"
- );
-
- removeSubnetProxyTargetsBatch(targetsToRemoveBatch);
-
- logger.debug(
- "handleMessagingForUpdatedSiteResource: dispatching removePeerDataBatch"
- );
-
- removePeerDataBatch(peerDataRemoves);
-
- //////////////////////////// FROM HERE DOWN WE ARE DEALING WITH ADDING NEW SITES
- const addedSiteIds = updatedSiteIds.filter(
- (id) => !existingSiteIds.includes(id)
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: adding sites addedSiteIds=[${addedSiteIds.join(", ")}] count=${addedSiteIds.length}`
- );
-
- const targetsToAddBatch: {
- newtId: string;
- targets: any[];
- version: string | null;
- }[] = [];
- const peerDataAdds: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: ReturnType;
- }[] = [];
- if (targets) {
- for (const siteId of addedSiteIds) {
- const newt = newtBySiteId.get(siteId);
- if (!newt) {
- logger.debug(
- `handleMessagingForUpdatedSiteResource: skipping add for siteId=${siteId} because no newt found`
- );
- continue;
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: preparing add batches for siteId=${siteId} newtId=${newt.newtId}`
- );
-
- targetsToAddBatch.push({
- newtId: newt.newtId,
- targets: targets,
- version: newt.version
- });
- for (const client of mergedAllClients) {
- peerDataAdds.push({
- clientId: client.clientId,
- siteId,
- remoteSubnets: generateRemoteSubnets([updatedSiteResource]),
- aliases: generateAliasConfig([updatedSiteResource])
- });
- }
- }
- } else {
- logger.debug(
- "handleMessagingForUpdatedSiteResource: skipping add batch generation because targets were empty"
- );
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: add batches prepared targetBatchCount=${targetsToAddBatch.length} peerDataCount=${peerDataAdds.length}`
- );
-
- logger.debug(
- "handleMessagingForUpdatedSiteResource: dispatching addSubnetProxyTargetsBatch"
- );
-
- addSubnetProxyTargetsBatch(targetsToAddBatch);
-
- logger.debug(
- "handleMessagingForUpdatedSiteResource: dispatching addPeerDataBatch"
- );
-
- addPeerDataBatch(peerDataAdds);
-
- //////////////////////////// FROM HERE DOWN WE ARE DEALING WITH UPDATING THE EXISTING SITES
-
- const unchangedSiteIds = existingSiteIds.filter((id) =>
- updatedSiteIds.includes(id)
- );
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: unchangedSiteIds=[${unchangedSiteIds.join(", ")}] count=${unchangedSiteIds.length}`
- );
-
- // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
- const destinationChanged =
- existingSiteResource &&
- existingSiteResource.destination !== updatedSiteResource.destination;
- const destinationPortChanged =
- existingSiteResource &&
- existingSiteResource.destinationPort !==
- updatedSiteResource.destinationPort;
- const aliasChanged =
- existingSiteResource &&
- existingSiteResource.alias !== updatedSiteResource.alias;
- const fullDomainChanged =
- existingSiteResource &&
- existingSiteResource.fullDomain !== updatedSiteResource.fullDomain;
- const sslChanged =
- existingSiteResource &&
- existingSiteResource.ssl !== updatedSiteResource.ssl;
- const portRangesChanged =
- existingSiteResource &&
- (existingSiteResource.tcpPortRangeString !==
- updatedSiteResource.tcpPortRangeString ||
- existingSiteResource.udpPortRangeString !==
- updatedSiteResource.udpPortRangeString ||
- existingSiteResource.disableIcmp !==
- updatedSiteResource.disableIcmp);
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)}`
- );
-
- // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
-
- if (
- destinationChanged ||
- aliasChanged ||
- fullDomainChanged ||
- sslChanged ||
- portRangesChanged ||
- destinationPortChanged
- ) {
- const shouldUpdateTargets =
- destinationChanged ||
- sslChanged ||
- portRangesChanged ||
- fullDomainChanged ||
- destinationPortChanged;
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: entering unchanged-site update path shouldUpdateTargets=${shouldUpdateTargets}`
- );
-
- const oldTargets = shouldUpdateTargets
- ? await generateSubnetProxyTargetV2(
- existingSiteResource,
- mergedAllClients
- )
- : [];
- const newTargets = shouldUpdateTargets
- ? await generateSubnetProxyTargetV2(
- updatedSiteResource,
- mergedAllClients
- )
- : [];
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: target update payload sizes oldTargets=${oldTargets ? oldTargets.length : 0} newTargets=${newTargets ? newTargets.length : 0}`
- );
-
- const peerDataUpdateBatch: Parameters[0] =
- [];
-
- for (const siteId of unchangedSiteIds) {
- const newt = newtBySiteId.get(siteId);
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: processing unchanged siteId=${siteId}`
- );
-
- if (!newt) {
- logger.error(
- `handleMessagingForUpdatedSiteResource: missing newt for unchanged siteId=${siteId}`
- );
- throw new Error(
- "Newt not found for site during site resource update"
- );
- }
-
- // Only update targets on newt if these items change
- if (shouldUpdateTargets) {
- logger.debug(
- `handleMessagingForUpdatedSiteResource: updating targets for siteId=${siteId} newtId=${newt.newtId}`
- );
- await updateTargets(
- newt.newtId,
- {
- oldTargets: oldTargets ? oldTargets : [],
- newTargets: newTargets ? newTargets : []
- },
- newt.version
- );
- }
-
- for (const client of mergedAllClients) {
- // does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet
- if (!existingSiteResource.destination) {
- logger.debug(
- `handleMessagingForUpdatedSiteResource: skipping peerData update for clientId=${client.clientId} siteId=${siteId} because existing destination is empty`
- );
- continue;
- }
-
- // we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
- const oldDestinationStillInUseBySite =
- oldDestinationStillInUseClientSitePairs.has(
- `${client.clientId}:${siteId}`
- );
-
- // we also need to update the remote subnets on the olms for each client that has access to this site
- peerDataUpdateBatch.push({
- clientId: client.clientId,
- siteId,
- remoteSubnets: destinationChanged
- ? {
- oldRemoteSubnets: !oldDestinationStillInUseBySite
- ? generateRemoteSubnets([
- existingSiteResource
- ])
- : [],
- newRemoteSubnets: generateRemoteSubnets([
- updatedSiteResource
- ])
- }
- : undefined,
- aliases:
- aliasChanged || fullDomainChanged // the full domain is sent down as an alias
- ? {
- oldAliases: generateAliasConfig([
- existingSiteResource
- ]),
- newAliases: generateAliasConfig([
- updatedSiteResource
- ])
- }
- : undefined
- });
- }
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: dispatching updatePeerDataBatch count=${peerDataUpdateBatch.length}`
- );
-
- updatePeerDataBatch(peerDataUpdateBatch);
- } else {
- logger.debug(
- "handleMessagingForUpdatedSiteResource: no unchanged-site update required because no relevant fields changed"
- );
- }
-
- logger.debug(
- `handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
- );
+ await Promise.all(proxyJobs);
}
export async function rebuildClientAssociationsFromClient(
- client: Client
+ client: Client,
+ trx: Transaction | typeof db = db
): Promise {
- await incrementOrgRebuildCount(client.orgId);
- try {
- const trx = primaryDb;
- // The whole locked rebuild is idempotent (it diffs full expected vs.
- // actual state each time), so on a transient DB error it's safe to
- // retry the entire thing rather than just the failed query.
- return await withRetry(
- () =>
- lockManager.withLock(
- `rebuild-client-associations:client:${client.clientId}`,
- () => rebuildClientAssociationsFromClientImpl(client, trx),
- REBUILD_ASSOCIATIONS_LOCK_TTL_MS
- ),
- `rebuildClientAssociationsFromClient:${client.clientId}`
- );
- } catch (err: any) {
- if (
- typeof err?.message === "string" &&
- err.message.startsWith("Failed to acquire lock")
- ) {
- logger.warn(
- `rebuildClientAssociations: could not acquire lock for client ${client.clientId}, queuing for deferred processing`
- );
- await rebuildQueue.enqueue({
- type: "client",
- id: client.clientId
- });
- return;
- }
- if (isTransientError(err)) {
- logger.warn(
- `rebuildClientAssociations: transient DB error rebuilding client ${client.clientId} persisted after retries, queuing for deferred processing:`,
- err
- );
- await rebuildQueue.enqueue({
- type: "client",
- id: client.clientId
- });
- return;
- }
- throw err;
- } finally {
- await decrementOrgRebuildCount(client.orgId);
- }
+ return await lockManager.withLock(
+ `rebuild-client-associations:client:${client.clientId}`,
+ () => rebuildClientAssociationsFromClientImpl(client, trx),
+ REBUILD_ASSOCIATIONS_LOCK_TTL_MS
+ );
}
async function rebuildClientAssociationsFromClientImpl(
@@ -1894,15 +1108,12 @@ async function rebuildClientAssociationsFromClientImpl(
// Insert new associations
if (resourcesToAdd.length > 0) {
- await trx
- .insert(clientSiteResourcesAssociationsCache)
- .values(
- resourcesToAdd.map((siteResourceId) => ({
- clientId: client.clientId,
- siteResourceId
- }))
- )
- .onConflictDoNothing();
+ await trx.insert(clientSiteResourcesAssociationsCache).values(
+ resourcesToAdd.map((siteResourceId) => ({
+ clientId: client.clientId,
+ siteResourceId
+ }))
+ );
}
// Remove old associations
@@ -1940,15 +1151,12 @@ async function rebuildClientAssociationsFromClientImpl(
// Insert new site associations
if (sitesToAdd.length > 0) {
- await trx
- .insert(clientSitesAssociationsCache)
- .values(
- sitesToAdd.map((siteId) => ({
- clientId: client.clientId,
- siteId
- }))
- )
- .onConflictDoNothing();
+ await trx.insert(clientSitesAssociationsCache).values(
+ sitesToAdd.map((siteId) => ({
+ clientId: client.clientId,
+ siteId
+ }))
+ );
}
// Remove old site associations
@@ -2029,28 +1237,6 @@ async function handleMessagesForClientSites(
const newtJobs: Promise[] = [];
const olmJobs: Promise[] = [];
const exitNodeJobs: Promise[] = [];
- const newtPeerDeletes: {
- siteId: number;
- publicKey: string;
- newtId: string;
- }[] = [];
- const olmPeerDeletes: {
- clientId: number;
- siteId: number;
- publicKey: string;
- olmId: string;
- }[] = [];
- const olmPeerAddHandshakes: {
- clientId: number;
- peer: {
- siteId: number;
- exitNode: {
- publicKey: string;
- endpoint: string;
- };
- };
- olmId: string;
- }[] = [];
const totalSitesOnClient = await trx
.select({ count: count(clientSitesAssociationsCache.siteId) })
@@ -2082,19 +1268,19 @@ async function handleMessagesForClientSites(
if (isRemove) {
// Remove peer from newt
- newtPeerDeletes.push({
- siteId: site.siteId,
- publicKey: client.pubKey,
- newtId: newt.newtId
- });
+ newtJobs.push(
+ newtDeletePeer(site.siteId, client.pubKey, newt.newtId)
+ );
try {
// Remove peer from olm
- olmPeerDeletes.push({
- clientId: client.clientId,
- siteId: site.siteId,
- publicKey: site.publicKey,
- olmId
- });
+ olmJobs.push(
+ olmDeletePeer(
+ client.clientId,
+ site.siteId,
+ site.publicKey,
+ olmId
+ )
+ );
} catch (error) {
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
if (
@@ -2126,9 +1312,10 @@ async function handleMessagesForClientSites(
continue;
}
- olmPeerAddHandshakes.push({
- clientId: client.clientId,
- peer: {
+ await initPeerAddHandshake(
+ // this will kick off the add peer process for the client
+ client.clientId,
+ {
siteId: site.siteId,
exitNode: {
publicKey: exitNode.publicKey,
@@ -2136,7 +1323,7 @@ async function handleMessagesForClientSites(
}
},
olmId
- });
+ );
}
// Update exit node destinations
@@ -2152,18 +1339,6 @@ async function handleMessagesForClientSites(
);
}
- if (newtPeerDeletes.length > 0) {
- newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
- }
-
- if (olmPeerDeletes.length > 0) {
- olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
- }
-
- if (olmPeerAddHandshakes.length > 0) {
- olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
- }
-
Promise.all(exitNodeJobs).catch((error) => {
logger.error(
`rebuildClientAssociations: Error updating client site destinations for client ${client.clientId}:`,
@@ -2262,20 +1437,6 @@ async function handleMessagesForClientResources(
continue;
}
- const targetsToAddBatch: {
- newtId: string;
- targets: NonNullable<
- Awaited>
- >;
- version: string | null;
- }[] = [];
- const peerDataAdds: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: ReturnType;
- }[] = [];
-
for (const resource of resources) {
const targets = await generateSubnetProxyTargetV2(resource, [
{
@@ -2286,21 +1447,25 @@ async function handleMessagesForClientResources(
]);
if (targets) {
- targetsToAddBatch.push({
- newtId: newt.newtId,
- targets,
- version: newt.version
- });
+ proxyJobs.push(
+ addSubnetProxyTargets(
+ newt.newtId,
+ targets,
+ newt.version
+ )
+ );
}
try {
// Add peer data to olm
- peerDataAdds.push({
- clientId: client.clientId,
- siteId,
- remoteSubnets: generateRemoteSubnets([resource]),
- aliases: generateAliasConfig([resource])
- });
+ olmJobs.push(
+ addPeerData(
+ client.clientId,
+ siteId,
+ generateRemoteSubnets([resource]),
+ generateAliasConfig([resource])
+ )
+ );
} catch (error) {
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
if (
@@ -2315,14 +1480,6 @@ async function handleMessagesForClientResources(
}
}
}
-
- if (targetsToAddBatch.length > 0) {
- proxyJobs.push(addSubnetProxyTargetsBatch(targetsToAddBatch));
- }
-
- if (peerDataAdds.length > 0) {
- olmJobs.push(addPeerDataBatch(peerDataAdds));
- }
}
}
@@ -2389,20 +1546,6 @@ async function handleMessagesForClientResources(
continue;
}
- const targetsToRemoveBatch: {
- newtId: string;
- targets: NonNullable<
- Awaited>
- >;
- version: string | null;
- }[] = [];
- const peerDataRemovals: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: ReturnType;
- }[] = [];
-
for (const resource of resources) {
const targets = await generateSubnetProxyTargetV2(resource, [
{
@@ -2413,11 +1556,13 @@ async function handleMessagesForClientResources(
]);
if (targets) {
- targetsToRemoveBatch.push({
- newtId: newt.newtId,
- targets,
- version: newt.version
- });
+ proxyJobs.push(
+ removeSubnetProxyTargets(
+ newt.newtId,
+ targets,
+ newt.version
+ )
+ );
}
try {
@@ -2461,19 +1606,21 @@ async function handleMessagesForClientResources(
)
);
- // Only remove remote subnet if no other resource uses the same destination on the same site
+ // Only remove remote subnet if no other resource uses the same destination
const remoteSubnetsToRemove =
destinationStillInUse.length > 0
? []
: generateRemoteSubnets([resource]);
// Remove peer data from olm
- peerDataRemovals.push({
- clientId: client.clientId,
- siteId,
- remoteSubnets: remoteSubnetsToRemove,
- aliases: generateAliasConfig([resource])
- });
+ olmJobs.push(
+ removePeerData(
+ client.clientId,
+ siteId,
+ remoteSubnetsToRemove,
+ generateAliasConfig([resource])
+ )
+ );
} catch (error) {
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
if (
@@ -2488,16 +1635,6 @@ async function handleMessagesForClientResources(
}
}
}
-
- if (targetsToRemoveBatch.length > 0) {
- proxyJobs.push(
- removeSubnetProxyTargetsBatch(targetsToRemoveBatch)
- );
- }
-
- if (peerDataRemovals.length > 0) {
- olmJobs.push(removePeerDataBatch(peerDataRemovals));
- }
}
}
@@ -2747,20 +1884,11 @@ export async function cleanupSiteAssociations(
// 7. Fire all removal messages in parallel.
const jobs: Promise[] = [];
- const olmPeerDeletes: {
- clientId: number;
- siteId: number;
- publicKey: string;
- }[] = [];
for (const client of allClients) {
// Tell each olm to drop the site's WireGuard peer.
if (site.publicKey) {
- olmPeerDeletes.push({
- clientId: client.clientId,
- siteId,
- publicKey: site.publicKey
- });
+ jobs.push(olmDeletePeer(client.clientId, siteId, site.publicKey));
}
// Recompute and push updated relay destinations (now excluding this site).
@@ -2769,10 +1897,6 @@ export async function cleanupSiteAssociations(
}
}
- if (olmPeerDeletes.length > 0) {
- jobs.push(olmDeletePeersBatch(olmPeerDeletes));
- }
-
await Promise.all(jobs).catch((error) => {
logger.error(
`cleanupSiteAssociations: error sending cleanup messages for siteId=${siteId}:`,
@@ -2782,44 +1906,3 @@ export async function cleanupSiteAssociations(
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
}
-
-/**
- * Start the background rebuild queue processor. This should be called once
- * during server startup. Only one server instance at a time will actively
- * consume the queue (enforced via a distributed Redis lock); all other
- * instances will poll and wait until the lock becomes available.
- */
-export function startRebuildQueueProcessor(): void {
- rebuildQueue.startProcessing({
- onSiteResource: async (siteResourceId: number) => {
- const [siteResource] = await primaryDb
- .select()
- .from(siteResources)
- .where(eq(siteResources.siteResourceId, siteResourceId));
-
- if (!siteResource) {
- logger.warn(
- `Rebuild queue: site resource ${siteResourceId} not found, skipping`
- );
- return;
- }
-
- await rebuildClientAssociationsFromSiteResource(siteResource);
- },
- onClient: async (clientId: number) => {
- const [client] = await primaryDb
- .select()
- .from(clients)
- .where(eq(clients.clientId, clientId));
-
- if (!client) {
- logger.warn(
- `Rebuild queue: client ${clientId} not found, skipping`
- );
- return;
- }
-
- await rebuildClientAssociationsFromClient(client);
- }
- });
-}
diff --git a/server/lib/rebuildQueue.ts b/server/lib/rebuildQueue.ts
deleted file mode 100644
index 7491f5f52..000000000
--- a/server/lib/rebuildQueue.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import logger from "@server/logger";
-import { isTransientError } from "@server/lib/dbRetry";
-
-export type RebuildJobType = "site-resource" | "client";
-
-export interface RebuildJob {
- type: RebuildJobType;
- id: number;
- // Number of times this job has already been re-queued after a transient
- // failure. Absent/0 means it has not failed yet.
- attempt?: number;
-}
-
-export interface RebuildJobHandlers {
- onSiteResource(siteResourceId: number): Promise;
- onClient(clientId: number): Promise;
-}
-
-export interface RebuildQueueManager {
- enqueue(job: RebuildJob): Promise;
- startProcessing(handlers: RebuildJobHandlers): void;
- isQueued(job: RebuildJob): Promise;
-}
-
-// In-process FIFO used when there is no Redis to back a distributed queue
-// (OSS build, or Redis unavailable). A job that loses the per-resource
-// rebuild lock race lands here instead of being silently dropped, and gets
-// retried shortly after against fresh DB state.
-const POLL_INTERVAL_MS = 500;
-const BATCH_SIZE = 5;
-// A job that fails with a transient DB error gets re-queued with backoff
-// instead of being dropped, up to this many times.
-const MAX_JOB_ATTEMPTS = 5;
-const JOB_RETRY_BASE_DELAY_MS = 1000;
-
-function dedupeKey(job: RebuildJob): string {
- return `${job.type}:${job.id}`;
-}
-
-class InMemoryRebuildQueue implements RebuildQueueManager {
- private queue: RebuildJob[] = [];
- private queuedSet = new Set();
- private processing = false;
- private processingStarted = false;
- private handlers: RebuildJobHandlers | null = null;
-
- async isQueued(job: RebuildJob): Promise {
- return this.queuedSet.has(dedupeKey(job));
- }
-
- async enqueue(job: RebuildJob): Promise {
- const key = dedupeKey(job);
- if (this.queuedSet.has(key)) {
- logger.debug(
- `Rebuild queue: skipped duplicate queued job ${job.type}:${job.id}`
- );
- return;
- }
- this.queuedSet.add(key);
- this.queue.push(job);
- logger.debug(
- `Rebuild queue: enqueued ${job.type}:${job.id} (queue position: tail)`
- );
- }
-
- startProcessing(handlers: RebuildJobHandlers): void {
- if (this.processingStarted) return;
- this.processingStarted = true;
- this.handlers = handlers;
-
- setInterval(() => {
- this.tryProcessBatch().catch((err) => {
- logger.error(
- "Rebuild queue: unhandled error in process loop:",
- err
- );
- });
- }, POLL_INTERVAL_MS);
-
- logger.info("Rebuild queue processor started (in-memory)");
- }
-
- private async tryProcessBatch(): Promise {
- if (this.processing || !this.handlers || this.queue.length === 0) {
- return;
- }
-
- this.processing = true;
- try {
- for (let i = 0; i < BATCH_SIZE; i++) {
- const job = this.queue.shift();
- if (!job) break; // queue drained
-
- // Remove from the dedupe set once dequeued so the same job
- // can be re-queued while this one is in progress.
- this.queuedSet.delete(dedupeKey(job));
-
- logger.debug(
- `Rebuild queue: processing ${job.type}:${job.id}`
- );
-
- try {
- if (job.type === "site-resource") {
- await this.handlers.onSiteResource(job.id);
- } else if (job.type === "client") {
- await this.handlers.onClient(job.id);
- } else {
- logger.warn(
- `Rebuild queue: unknown job type "${(job as any).type}", discarding`
- );
- }
-
- logger.debug(
- `Rebuild queue: completed ${job.type}:${job.id}`
- );
- } catch (err) {
- const attempt = (job.attempt ?? 0) + 1;
- if (isTransientError(err) && attempt <= MAX_JOB_ATTEMPTS) {
- const delay =
- JOB_RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
- logger.warn(
- `Rebuild queue: job ${job.type}:${job.id} hit a transient error (attempt ${attempt}/${MAX_JOB_ATTEMPTS}), re-queuing in ${delay}ms:`,
- err
- );
- setTimeout(() => {
- this.enqueue({ ...job, attempt }).catch(
- (enqueueErr) =>
- logger.error(
- `Rebuild queue: failed to re-queue ${job.type}:${job.id} after transient error:`,
- enqueueErr
- )
- );
- }, delay);
- } else {
- logger.error(
- `Rebuild queue: job ${job.type}:${job.id} threw an error:`,
- err
- );
- }
- }
- }
- } finally {
- this.processing = false;
- }
- }
-}
-
-export const rebuildQueue: RebuildQueueManager = new InMemoryRebuildQueue();
diff --git a/server/lib/statusHistory.ts b/server/lib/statusHistory.ts
index 7c5b5c370..8bb7e6a0c 100644
--- a/server/lib/statusHistory.ts
+++ b/server/lib/statusHistory.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { db, logsDb, statusHistory } from "@server/db";
-import { and, eq, gte, lt, asc, desc } from "drizzle-orm";
+import { and, eq, gte, asc } from "drizzle-orm";
import { regionalCache as cache } from "#dynamic/lib/cache";
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
@@ -42,29 +42,7 @@ export async function getCachedStatusHistory(
)
.orderBy(asc(statusHistory.timestamp));
- // Fetch the last known state before the window so that entities that
- // haven't changed status recently still show the correct status rather
- // than appearing as "no_data".
- const [lastKnownEvent] = await logsDb
- .select()
- .from(statusHistory)
- .where(
- and(
- eq(statusHistory.entityType, entityType),
- eq(statusHistory.entityId, entityId),
- lt(statusHistory.timestamp, startSec)
- )
- )
- .orderBy(desc(statusHistory.timestamp))
- .limit(1);
-
- const priorStatus = lastKnownEvent?.status ?? null;
-
- const { buckets, totalDowntime } = computeBuckets(
- events,
- days,
- priorStatus
- );
+ const { buckets, totalDowntime } = computeBuckets(events, days);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
@@ -132,8 +110,7 @@ export function computeBuckets(
timestamp: number;
id: number;
}[],
- days: number,
- priorStatus: string | null = null
+ days: number
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
const nowSec = Math.floor(Date.now() / 1000);
@@ -159,10 +136,7 @@ export function computeBuckets(
.filter((e) => e.timestamp < dayStartSec)
.at(-1);
- // Fall back to the last known state before the entire query window
- // so that entities that haven't generated events recently still show
- // as their actual status rather than "no_data".
- const currentStatus = lastBeforeDay?.status ?? priorStatus ?? null;
+ const currentStatus = lastBeforeDay?.status ?? null;
const windows: { start: number; end: number | null; status: string }[] =
[];
diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts
index cc7299ff7..42baf41b5 100644
--- a/server/lib/traefik/TraefikConfigManager.ts
+++ b/server/lib/traefik/TraefikConfigManager.ts
@@ -511,12 +511,6 @@ export class TraefikConfigManager {
let traefikConfig;
try {
const currentExitNode = await getCurrentExitNodeId();
-
- const maintenancePort = config.getRawConfig().server.next_port;
- const maintenanceHost =
- config.getRawConfig().server.internal_hostname;
- const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
-
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
traefikConfig = await getTraefikConfig(
// this is called by the local exit node to get its own config
@@ -527,8 +521,7 @@ export class TraefikConfigManager {
build == "saas"
? false
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
- pangolinUIUrl, // generate maintenance pages on cloud and hybrid
- pangolinUIUrl // generate browser gateway targets on cloud and hybrid
+ build != "oss" // generate browser gateway targets on cloud and enterprise
);
const domains = new Set();
diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts
index c63b5b718..48eb03638 100644
--- a/server/lib/traefik/getTraefikConfig.ts
+++ b/server/lib/traefik/getTraefikConfig.ts
@@ -44,8 +44,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
allowRawResources = true,
- maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
- browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
+ allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
+ allowBrowserGatewayResources = true
): Promise {
// Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources
diff --git a/server/lib/userOrg.ts b/server/lib/userOrg.ts
index 4bff40c13..809266b73 100644
--- a/server/lib/userOrg.ts
+++ b/server/lib/userOrg.ts
@@ -14,7 +14,7 @@ import {
} from "@server/db";
import { eq, and, inArray, ne, exists } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
export async function assignUserToOrg(
org: Org,
@@ -61,7 +61,7 @@ export async function assignUserToOrg(
);
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
- await usageService.add(org.orgId, LimitId.USERS, 1, trx);
+ await usageService.add(org.orgId, FeatureId.USERS, 1, trx);
}
}
}
@@ -157,7 +157,7 @@ export async function removeUserFromOrg(
);
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
- await usageService.add(org.orgId, LimitId.USERS, -1, trx);
+ await usageService.add(org.orgId, FeatureId.USERS, -1, trx);
}
}
}
diff --git a/server/lib/validators.test.ts b/server/lib/validators.test.ts
index ac95184a5..00b6c75db 100644
--- a/server/lib/validators.test.ts
+++ b/server/lib/validators.test.ts
@@ -1,7 +1,4 @@
-import {
- getResourceRuleValueValidationError,
- isValidUrlGlobPattern
-} from "./validators";
+import { isValidUrlGlobPattern } from "./validators";
import { assertEquals } from "@test/assert";
function runTests() {
@@ -239,43 +236,6 @@ function runTests() {
"Path with isolated percent sign should be invalid"
);
- // ASN validation tests
- assertEquals(
- getResourceRuleValueValidationError("ASN", "AS15169"),
- null,
- "Standard ASN should be valid"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", " As15169 "),
- null,
- "Standard ASN should be valid with mixed case and whitespace"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", "ALL"),
- null,
- "ALL ASN selector should be valid"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", " all "),
- null,
- "ALL ASN selector should be valid with mixed case and whitespace"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", "AS0"),
- null,
- "AS0 alias should be valid"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", " as0 "),
- null,
- "AS0 alias should be valid with mixed case and whitespace"
- );
- assertEquals(
- getResourceRuleValueValidationError("ASN", "not-an-asn"),
- "Invalid ASN provided",
- "Invalid ASN should return an error"
- );
-
console.log("All tests passed!");
}
diff --git a/server/lib/validators.ts b/server/lib/validators.ts
index ff0c5fb3d..c179d3c91 100644
--- a/server/lib/validators.ts
+++ b/server/lib/validators.ts
@@ -74,7 +74,6 @@ export const RESOURCE_RULE_MATCH_TYPES = [
"IP",
"PATH",
"COUNTRY",
- "COUNTRY_IS_NOT",
"ASN",
"REGION"
] as const;
@@ -97,15 +96,11 @@ export function getResourceRuleValueValidationError(
case "REGION":
return isValidRegionId(value) ? null : "Invalid region ID provided";
case "COUNTRY":
- case "COUNTRY_IS_NOT":
return COUNTRIES.some((country) => country.code === value)
? null
: "Invalid country code provided";
case "ASN":
- const normalizedValue = value.trim().toUpperCase();
- return /^AS\d+$/.test(normalizedValue) ||
- normalizedValue === "ALL" ||
- normalizedValue === "AS0"
+ return /^AS\d+$/i.test(value.trim())
? null
: "Invalid ASN provided";
default:
diff --git a/server/middlewares/verifyAccessTokenAccess.ts b/server/middlewares/verifyAccessTokenAccess.ts
index 07786e87d..528298727 100644
--- a/server/middlewares/verifyAccessTokenAccess.ts
+++ b/server/middlewares/verifyAccessTokenAccess.ts
@@ -119,7 +119,8 @@ export async function verifyAccessTokenAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyAdmin.ts b/server/middlewares/verifyAdmin.ts
index da4f88af9..0dbeac2cb 100644
--- a/server/middlewares/verifyAdmin.ts
+++ b/server/middlewares/verifyAdmin.ts
@@ -56,7 +56,8 @@ export async function verifyAdmin(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts
index ea1bdac18..2522a1e8b 100644
--- a/server/middlewares/verifyApiKeyAccess.ts
+++ b/server/middlewares/verifyApiKeyAccess.ts
@@ -113,7 +113,8 @@ export async function verifyApiKeyAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyClientAccess.ts b/server/middlewares/verifyClientAccess.ts
index 3aee4f38c..1d994b53f 100644
--- a/server/middlewares/verifyClientAccess.ts
+++ b/server/middlewares/verifyClientAccess.ts
@@ -107,7 +107,8 @@ export async function verifyClientAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
@@ -128,7 +129,10 @@ export async function verifyClientAccess(
.where(
and(
eq(roleClients.clientId, client.clientId),
- inArray(roleClients.roleId, req.userOrgRoleIds!)
+ inArray(
+ roleClients.roleId,
+ req.userOrgRoleIds!
+ )
)
)
.limit(1)
diff --git a/server/middlewares/verifyDomainAccess.ts b/server/middlewares/verifyDomainAccess.ts
index 173c3a54b..783132a1a 100644
--- a/server/middlewares/verifyDomainAccess.ts
+++ b/server/middlewares/verifyDomainAccess.ts
@@ -88,7 +88,8 @@ export async function verifyDomainAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts
index be6242f6d..e464f7b89 100644
--- a/server/middlewares/verifyOrgAccess.ts
+++ b/server/middlewares/verifyOrgAccess.ts
@@ -7,7 +7,6 @@ import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getFirstString } from "@server/lib/requestParams";
-import logger from "@server/logger";
export async function verifyOrgAccess(
req: Request,
@@ -60,7 +59,8 @@ export async function verifyOrgAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyResourceAccess.ts b/server/middlewares/verifyResourceAccess.ts
index 2689cdb2d..f790a481a 100644
--- a/server/middlewares/verifyResourceAccess.ts
+++ b/server/middlewares/verifyResourceAccess.ts
@@ -105,7 +105,8 @@ export async function verifyResourceAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyResourcePolicyAccess.ts b/server/middlewares/verifyResourcePolicyAccess.ts
index 667680c0f..30fe48e8c 100644
--- a/server/middlewares/verifyResourcePolicyAccess.ts
+++ b/server/middlewares/verifyResourcePolicyAccess.ts
@@ -102,7 +102,8 @@ export async function verifyResourcePolicyAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyRoleAccess.ts b/server/middlewares/verifyRoleAccess.ts
index 3264a3bd9..380b82048 100644
--- a/server/middlewares/verifyRoleAccess.ts
+++ b/server/middlewares/verifyRoleAccess.ts
@@ -132,7 +132,8 @@ export async function verifyRoleAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifySetResourceClients.ts b/server/middlewares/verifySetResourceClients.ts
index 443483a28..8f9c1ecaf 100644
--- a/server/middlewares/verifySetResourceClients.ts
+++ b/server/middlewares/verifySetResourceClients.ts
@@ -45,7 +45,8 @@ export async function verifySetResourceClients(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifySetResourceUsers.ts b/server/middlewares/verifySetResourceUsers.ts
index cc9375e4a..94600b9b4 100644
--- a/server/middlewares/verifySetResourceUsers.ts
+++ b/server/middlewares/verifySetResourceUsers.ts
@@ -40,7 +40,8 @@ export async function verifySetResourceUsers(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifySiteAccess.ts b/server/middlewares/verifySiteAccess.ts
index 50a940855..c4d35a52f 100644
--- a/server/middlewares/verifySiteAccess.ts
+++ b/server/middlewares/verifySiteAccess.ts
@@ -115,7 +115,8 @@ export async function verifySiteAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifySiteProvisioningKeyAccess.ts b/server/middlewares/verifySiteProvisioningKeyAccess.ts
index 9cb9a28f3..73393e1e9 100644
--- a/server/middlewares/verifySiteProvisioningKeyAccess.ts
+++ b/server/middlewares/verifySiteProvisioningKeyAccess.ts
@@ -115,7 +115,8 @@ export async function verifySiteProvisioningKeyAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifySiteResourceAccess.ts b/server/middlewares/verifySiteResourceAccess.ts
index c87518a9e..8d5bd656f 100644
--- a/server/middlewares/verifySiteResourceAccess.ts
+++ b/server/middlewares/verifySiteResourceAccess.ts
@@ -103,7 +103,8 @@ export async function verifySiteResourceAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyTargetAccess.ts b/server/middlewares/verifyTargetAccess.ts
index 24b8abd22..8bbed6fca 100644
--- a/server/middlewares/verifyTargetAccess.ts
+++ b/server/middlewares/verifyTargetAccess.ts
@@ -122,7 +122,8 @@ export async function verifyTargetAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/middlewares/verifyUserAccess.ts b/server/middlewares/verifyUserAccess.ts
index 83c344ae0..fcc4d0cb9 100644
--- a/server/middlewares/verifyUserAccess.ts
+++ b/server/middlewares/verifyUserAccess.ts
@@ -59,7 +59,8 @@ export async function verifyUserAccess(
return next(
createHttpError(
HttpCode.FORBIDDEN,
- "" + (policyCheck.error || "Unknown error")
+ "Failed organization access policy check: " +
+ (policyCheck.error || "Unknown error")
)
);
}
diff --git a/server/private/lib/acmeCertSync.ts b/server/private/lib/acmeCertSync.ts
index 56105ac38..fb99e934e 100644
--- a/server/private/lib/acmeCertSync.ts
+++ b/server/private/lib/acmeCertSync.ts
@@ -693,9 +693,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise {
);
continue;
}
- // logger.debug(
- // `acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
- // );
+ logger.debug(
+ `acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
+ );
for (const cert of resolverData.Certificates) {
allCerts.push(cert);
}
diff --git a/server/private/lib/certificates.ts b/server/private/lib/certificates.ts
index 03ea6a58c..31e40ed55 100644
--- a/server/private/lib/certificates.ts
+++ b/server/private/lib/certificates.ts
@@ -17,7 +17,7 @@ import { certificates, db } from "@server/db";
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto";
import logger from "@server/logger";
-import { regionalCache as cache } from "#private/lib/cache";
+import cache from "#private/lib/cache";
import { build } from "@server/build";
// Define the return type for clarity and type safety
diff --git a/server/private/lib/checkOrgAccessPolicy.ts b/server/private/lib/checkOrgAccessPolicy.ts
index 9a03f9e09..b861c1ae6 100644
--- a/server/private/lib/checkOrgAccessPolicy.ts
+++ b/server/private/lib/checkOrgAccessPolicy.ts
@@ -21,49 +21,6 @@ import {
} from "@server/lib/checkOrgAccessPolicy";
import { UserType } from "@server/types/UserTypes";
-function formatMaxSessionLengthRequirement(
- maxSessionLengthHours: number
-): string {
- if (maxSessionLengthHours < 24) {
- return `This organization requires you to log in every ${maxSessionLengthHours} hours.`;
- }
-
- const maxDays = Math.round(maxSessionLengthHours / 24);
- return `This organization requires you to log in every ${maxDays} days.`;
-}
-
-function buildOrgAccessPolicyError(
- policies: CheckOrgAccessPolicyResult["policies"]
-): string | undefined {
- if (!policies) {
- return undefined;
- }
-
- const errors: string[] = [];
-
- if (policies.requiredTwoFactor === false) {
- errors.push(
- "This organization requires two-factor authentication. Enable two-factor authentication on your account to continue."
- );
- }
-
- if (policies.maxSessionLength?.compliant === false) {
- errors.push(
- `Your session has expired. ${formatMaxSessionLengthRequirement(
- policies.maxSessionLength.maxSessionLengthHours
- )}`
- );
- }
-
- if (policies.passwordAge?.compliant === false) {
- errors.push(
- `Your password has expired. This organization requires you to change your password every ${policies.passwordAge.maxPasswordAgeDays} days.`
- );
- }
-
- return errors.length > 0 ? errors.join(" ") : undefined;
-}
-
export function enforceResourceSessionLength(
resourceSession: ResourceSession,
org: Org
@@ -79,17 +36,13 @@ export function enforceResourceSessionLength(
if (sessionAgeMs > maxSessionLengthMs) {
return {
valid: false,
- error: `Your resource session has expired. ${formatMaxSessionLengthRequirement(
- maxSessionLengthHours
- )}`
+ error: `Resource session has expired due to organization policy (max session length: ${maxSessionLengthHours} hours)`
};
}
} else {
return {
valid: false,
- error: `Your resource session is invalid. ${formatMaxSessionLengthRequirement(
- maxSessionLengthHours
- )}`
+ error: `Resource session is invalid due to organization policy (max session length: ${maxSessionLengthHours} hours)`
};
}
}
@@ -107,20 +60,14 @@ export async function checkOrgAccessPolicy(
if (!orgId) {
return {
allowed: false,
- error: "Unable to verify organization access. Organization information is missing."
+ error: "Organization ID is required"
};
}
if (!userId) {
- return {
- allowed: false,
- error: "Unable to verify organization access. User information is missing."
- };
+ return { allowed: false, error: "User ID is required" };
}
if (!sessionId) {
- return {
- allowed: false,
- error: "Your session is invalid. Please log in again."
- };
+ return { allowed: false, error: "Session ID is required" };
}
if (build === "enterprise") {
@@ -142,10 +89,7 @@ export async function checkOrgAccessPolicy(
.where(eq(orgs.orgId, orgId));
props.org = orgQuery;
if (!props.org) {
- return {
- allowed: false,
- error: "This organization could not be found."
- };
+ return { allowed: false, error: "Organization not found" };
}
}
@@ -156,10 +100,7 @@ export async function checkOrgAccessPolicy(
.where(eq(users.userId, userId));
props.user = userQuery;
if (!props.user) {
- return {
- allowed: false,
- error: "Your account could not be found."
- };
+ return { allowed: false, error: "User not found" };
}
}
@@ -170,17 +111,14 @@ export async function checkOrgAccessPolicy(
.where(eq(sessions.sessionId, sessionId));
props.session = sessionQuery;
if (!props.session) {
- return {
- allowed: false,
- error: "Your session has expired. Please log in again."
- };
+ return { allowed: false, error: "Session not found" };
}
}
if (props.session.userId !== props.user.userId) {
return {
allowed: false,
- error: "Your session is invalid. Please log in again."
+ error: "Session does not belong to the user"
};
}
@@ -249,14 +187,8 @@ export async function checkOrgAccessPolicy(
allowed = false;
}
- const policyError = buildOrgAccessPolicyError(policies);
-
return {
allowed,
- policies,
- error: allowed
- ? undefined
- : (policyError ??
- "You do not meet this organization's security requirements.")
+ policies
};
}
diff --git a/server/private/lib/exitNodes/exitNodes.ts b/server/private/lib/exitNodes/exitNodes.ts
index 1f9517725..f6417dae2 100644
--- a/server/private/lib/exitNodes/exitNodes.ts
+++ b/server/private/lib/exitNodes/exitNodes.ts
@@ -18,15 +18,12 @@ import {
resources,
targets,
sites,
- siteLabels,
- remoteExitNodes,
- remoteExitNodePreferenceLabels,
targetHealthCheck,
Transaction
} from "@server/db";
import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt";
-import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
+import { eq, and, or, ne, isNull } from "drizzle-orm";
import axios from "axios";
import config from "../config";
@@ -153,8 +150,7 @@ export async function verifyExitNodeOrgAccess(
export async function listExitNodes(
orgId: string,
filterOnline = false,
- noCloud = false,
- siteId?: number
+ noCloud = false
) {
const allExitNodes = await db
.select({
@@ -241,7 +237,7 @@ export async function listExitNodes(
// })
// );
- let remoteExitNodesList = allExitNodes.filter(
+ const remoteExitNodes = allExitNodes.filter(
(node) =>
node.type === "remoteExitNode" && (!filterOnline || node.online)
);
@@ -250,82 +246,9 @@ export async function listExitNodes(
node.type === "gerbil" && (!filterOnline || node.online) && !noCloud
);
- // Apply label-based filtering to remote exit nodes if siteId is provided
- if (siteId !== undefined && remoteExitNodesList.length > 0) {
- // Get the site's labels
- const siteLabelRows = await db
- .select({ labelId: siteLabels.labelId })
- .from(siteLabels)
- .where(eq(siteLabels.siteId, siteId));
- const siteLabelIds = new Set(siteLabelRows.map((r) => r.labelId));
-
- // Get the remoteExitNode records for these exit nodes so we have the remoteExitNodeId
- const exitNodeIds = remoteExitNodesList.map((n) => n.exitNodeId);
- const remoteNodeRows = await db
- .select({
- exitNodeId: remoteExitNodes.exitNodeId,
- remoteExitNodeId: remoteExitNodes.remoteExitNodeId
- })
- .from(remoteExitNodes)
- .where(inArray(remoteExitNodes.exitNodeId, exitNodeIds));
-
- const exitNodeIdToRemoteId = new Map(
- remoteNodeRows
- .filter((r) => r.exitNodeId !== null)
- .map((r) => [r.exitNodeId!, r.remoteExitNodeId])
- );
-
- // Get preference labels for all remote exit nodes
- const remoteExitNodeIds = remoteNodeRows.map((r) => r.remoteExitNodeId);
- const prefLabelRows =
- remoteExitNodeIds.length > 0
- ? await db
- .select({
- remoteExitNodeId:
- remoteExitNodePreferenceLabels.remoteExitNodeId,
- labelId: remoteExitNodePreferenceLabels.labelId
- })
- .from(remoteExitNodePreferenceLabels)
- .where(
- inArray(
- remoteExitNodePreferenceLabels.remoteExitNodeId,
- remoteExitNodeIds
- )
- )
- : [];
-
- // Build a map of remoteExitNodeId -> Set of labelIds
- const prefLabelsMap = new Map>();
- for (const row of prefLabelRows) {
- if (!prefLabelsMap.has(row.remoteExitNodeId)) {
- prefLabelsMap.set(row.remoteExitNodeId, new Set());
- }
- prefLabelsMap.get(row.remoteExitNodeId)!.add(row.labelId);
- }
-
- // Filter: include node if it has no preference labels, or if site shares at least one label
- const filtered = remoteExitNodesList.filter((node) => {
- const remoteId = exitNodeIdToRemoteId.get(node.exitNodeId);
- if (!remoteId) return true; // no remoteExitNode record, don't filter
- const prefLabels = prefLabelsMap.get(remoteId);
- if (!prefLabels || prefLabels.size === 0) return true; // no preference labels, include
- // include only if site has at least one matching label
- for (const labelId of siteLabelIds) {
- if (prefLabels.has(labelId)) return true;
- }
- return false;
- });
-
- // Only apply the filtered list if at least one remote node remains;
- // otherwise fall through to the gerbil fallback below
- if (filtered.length > 0 || remoteExitNodesList.length === 0) {
- remoteExitNodesList = filtered;
- }
- }
-
// THIS PROVIDES THE FALL
const exitNodesList =
- remoteExitNodesList.length > 0 ? remoteExitNodesList : gerbilExitNodes;
+ remoteExitNodes.length > 0 ? remoteExitNodes : gerbilExitNodes;
return exitNodesList;
}
diff --git a/server/private/lib/lock.ts b/server/private/lib/lock.ts
index 94b27a337..a59bbc051 100644
--- a/server/private/lib/lock.ts
+++ b/server/private/lib/lock.ts
@@ -11,80 +11,37 @@
* This file is not licensed under the AGPLv3.
*/
+import { config } from "@server/lib/config";
import logger from "@server/logger";
import { redis } from "#private/lib/redis";
import { v4 as uuidv4 } from "uuid";
const instanceId = uuidv4();
-type LocalLockRecord = {
- owner: string;
- expiresAt: number;
-};
-
-const localLocks = new Map();
-
export class LockManager {
- private clearExpiredLocalLock(lockKey: string): void {
- const current = localLocks.get(lockKey);
- if (current && current.expiresAt <= Date.now()) {
- localLocks.delete(lockKey);
- }
- }
-
/**
* Acquire a distributed lock using Redis SET with NX and PX options
* @param lockKey - Unique identifier for the lock
* @param ttlMs - Time to live in milliseconds
- * @returns Promise - a token identifying this specific acquisition
- * (truthy) on success, or null if the lock could not be acquired.
+ * @returns Promise - true if lock acquired, false otherwise
*/
async acquireLock(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 3,
retryDelayMs: number = 100
- ): Promise {
+ ): Promise {
if (!redis || !redis.status || redis.status !== "ready") {
- for (let attempt = 0; attempt < maxRetries; attempt++) {
- this.clearExpiredLocalLock(lockKey);
-
- const existing = localLocks.get(lockKey);
- if (!existing) {
- const token = `${instanceId}:${uuidv4()}`;
- localLocks.set(lockKey, {
- owner: token,
- expiresAt: Date.now() + ttlMs
- });
- return token;
- }
-
- // Do not treat a same-process holder as automatically
- // reentrant -- see the note in the Redis branch below.
- if (attempt < maxRetries - 1) {
- const delay = retryDelayMs * Math.pow(2, attempt);
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
- }
-
- return null;
+ return true;
}
+ const lockValue = `${
+ instanceId
+ }:${Date.now()}`;
const redisKey = `lock:${lockKey}`;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
- // Every acquisition attempt gets its own unique token, even
- // within the same process. Two independent logical operations
- // (e.g. two different API requests handled by the same server)
- // racing for this key must never both believe they hold the
- // lock -- if we treated "existing value starts with my
- // instanceId" as reentrant success, a second unrelated caller
- // on this process could barge in while the first is still
- // mid-flight, and their writes under the lock would interleave
- // unguarded.
- const lockValue = `${instanceId}:${uuidv4()}`;
-
// Use SET with NX (only set if not exists) and PX (expire in milliseconds)
// This is atomic and handles both setting and expiration
const result = await redis.set(
@@ -96,8 +53,30 @@ export class LockManager {
);
if (result === "OK") {
- logger.debug(`Lock acquired: ${lockKey} by ${instanceId}`);
- return lockValue;
+ logger.debug(
+ `Lock acquired: ${lockKey} by ${
+ instanceId
+ }`
+ );
+ return true;
+ }
+
+ // Check if the existing lock is from this worker (reentrant behavior)
+ const existingValue = await redis.get(redisKey);
+ if (
+ existingValue &&
+ existingValue.startsWith(
+ `${instanceId}:`
+ )
+ ) {
+ // Extend the lock TTL since it's the same worker
+ await redis.pexpire(redisKey, ttlMs);
+ logger.debug(
+ `Lock extended: ${lockKey} by ${
+ instanceId
+ }`
+ );
+ return true;
}
// If this isn't our last attempt, wait before retrying with exponential backoff
@@ -109,10 +88,7 @@ export class LockManager {
await new Promise((resolve) => setTimeout(resolve, delay));
}
} catch (error) {
- logger.error(
- `Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`,
- error
- );
+ logger.error(`Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`, error);
// On error, still retry if we have attempts left
if (attempt < maxRetries - 1) {
const delay = retryDelayMs * Math.pow(2, attempt);
@@ -124,36 +100,27 @@ export class LockManager {
logger.debug(
`Failed to acquire lock ${lockKey} after ${maxRetries} attempts`
);
- return null;
+ return false;
}
/**
- * Release a lock previously acquired via acquireLock/acquireLockWithRetry,
- * using a Lua script to ensure we only delete it if it still matches the
- * exact token from that acquisition (not just "owned by this process") --
- * this ensures a caller whose TTL already expired can't delete a
- * different, currently-active holder's lock.
+ * Release a lock using Lua script to ensure atomicity
* @param lockKey - Unique identifier for the lock
- * @param token - the exact token returned by the acquisition being released
*/
- async releaseLock(lockKey: string, token: string): Promise {
+ async releaseLock(lockKey: string): Promise {
if (!redis || !redis.status || redis.status !== "ready") {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
- if (existing && existing.owner === token) {
- localLocks.delete(lockKey);
- }
return;
}
const redisKey = `lock:${lockKey}`;
+ // Lua script to ensure we only delete the lock if it belongs to this worker
const luaScript = `
local key = KEYS[1]
- local expected_value = ARGV[1]
+ local worker_prefix = ARGV[1]
local current_value = redis.call('GET', key)
- if current_value and current_value == expected_value then
+ if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
return redis.call('DEL', key)
else
return 0
@@ -165,14 +132,20 @@ export class LockManager {
luaScript,
1,
redisKey,
- token
+ `${instanceId}:`
)) as number;
if (result === 1) {
- logger.debug(`Lock released: ${lockKey} by ${instanceId}`);
+ logger.debug(
+ `Lock released: ${lockKey} by ${
+ instanceId
+ }`
+ );
} else {
logger.warn(
- `Lock not released - token did not match current holder: ${lockKey} (attempted by ${instanceId})`
+ `Lock not released - not owned by worker: ${lockKey} by ${
+ instanceId
+ }`
);
}
} catch (error) {
@@ -186,7 +159,6 @@ export class LockManager {
*/
async forceReleaseLock(lockKey: string): Promise {
if (!redis || !redis.status || redis.status !== "ready") {
- localLocks.delete(lockKey);
return;
}
@@ -214,20 +186,7 @@ export class LockManager {
owner?: string;
}> {
if (!redis || !redis.status || redis.status !== "ready") {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
-
- if (!existing) {
- return { exists: false, ownedByMe: false, ttl: 0 };
- }
-
- const ttl = Math.max(0, existing.expiresAt - Date.now());
- return {
- exists: true,
- ownedByMe: existing.owner.startsWith(`${instanceId}:`),
- ttl,
- owner: existing.owner.split(":")[0]
- };
+ return { exists: false, ownedByMe: true, ttl: 0 };
}
const redisKey = `lock:${lockKey}`;
@@ -239,7 +198,11 @@ export class LockManager {
]);
const exists = value !== null;
- const ownedByMe = exists && value!.startsWith(`${instanceId}:`);
+ const ownedByMe =
+ exists &&
+ value!.startsWith(
+ `${instanceId}:`
+ );
const owner = exists ? value!.split(":")[0] : undefined;
return {
@@ -255,40 +218,26 @@ export class LockManager {
}
/**
- * Extend the TTL of an existing lock, provided the token matches the
- * acquisition currently holding it.
+ * Extend the TTL of an existing lock owned by this worker
* @param lockKey - Unique identifier for the lock
* @param ttlMs - New TTL in milliseconds
- * @param token - the token returned by the acquisition being extended
* @returns Promise - true if extended successfully
*/
- async extendLock(
- lockKey: string,
- ttlMs: number,
- token: string
- ): Promise {
+ async extendLock(lockKey: string, ttlMs: number): Promise {
if (!redis || !redis.status || redis.status !== "ready") {
- this.clearExpiredLocalLock(lockKey);
- const existing = localLocks.get(lockKey);
-
- if (!existing || existing.owner !== token) {
- return false;
- }
-
- existing.expiresAt = Date.now() + ttlMs;
- localLocks.set(lockKey, existing);
return true;
}
const redisKey = `lock:${lockKey}`;
+ // Lua script to extend TTL only if lock is owned by this worker
const luaScript = `
local key = KEYS[1]
- local expected_value = ARGV[1]
+ local worker_prefix = ARGV[1]
local ttl = tonumber(ARGV[2])
local current_value = redis.call('GET', key)
- if current_value and current_value == expected_value then
+ if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
return redis.call('PEXPIRE', key, ttl)
else
return 0
@@ -300,13 +249,15 @@ export class LockManager {
luaScript,
1,
redisKey,
- token,
+ `${instanceId}:`,
ttlMs.toString()
)) as number;
if (result === 1) {
logger.debug(
- `Lock extended: ${lockKey} by ${instanceId} for ${ttlMs}ms`
+ `Lock extended: ${lockKey} by ${
+ instanceId
+ } for ${ttlMs}ms`
);
return true;
}
@@ -323,24 +274,23 @@ export class LockManager {
* @param ttlMs - Time to live in milliseconds
* @param maxRetries - Maximum number of retry attempts
* @param baseDelayMs - Base delay between retries in milliseconds
- * @returns Promise - token if acquired, null otherwise
+ * @returns Promise - true if lock acquired
*/
async acquireLockWithRetry(
lockKey: string,
ttlMs: number = 30000,
maxRetries: number = 5,
baseDelayMs: number = 100
- ): Promise {
+ ): Promise {
+ if (!redis || !redis.status || redis.status !== "ready") {
+ return true;
+ }
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
- const acquired = await this.acquireLock(
- lockKey,
- ttlMs,
- 1,
- baseDelayMs
- );
+ const acquired = await this.acquireLock(lockKey, ttlMs);
if (acquired) {
- return acquired;
+ return true;
}
if (attempt < maxRetries) {
@@ -354,7 +304,7 @@ export class LockManager {
logger.warn(
`Failed to acquire lock ${lockKey} after ${maxRetries + 1} attempts`
);
- return null;
+ return false;
}
/**
@@ -369,16 +319,20 @@ export class LockManager {
fn: () => Promise,
ttlMs: number = 30000
): Promise {
- const token = await this.acquireLock(lockKey, ttlMs);
+ if (!redis || !redis.status || redis.status !== "ready") {
+ return await fn();
+ }
- if (!token) {
+ const acquired = await this.acquireLock(lockKey, ttlMs);
+
+ if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
try {
return await fn();
} finally {
- await this.releaseLock(lockKey, token);
+ await this.releaseLock(lockKey);
}
}
@@ -392,21 +346,7 @@ export class LockManager {
locksOwnedByMe: number;
}> {
if (!redis || !redis.status || redis.status !== "ready") {
- const now = Date.now();
- for (const [key, value] of localLocks.entries()) {
- if (value.expiresAt <= now) {
- localLocks.delete(key);
- }
- }
-
- let locksOwnedByMe = 0;
- for (const value of localLocks.values()) {
- if (value.owner.startsWith(`${instanceId}:`)) {
- locksOwnedByMe++;
- }
- }
-
- return { activeLocksCount: localLocks.size, locksOwnedByMe };
+ return { activeLocksCount: 0, locksOwnedByMe: 0 };
}
try {
@@ -416,7 +356,11 @@ export class LockManager {
if (keys.length > 0) {
const values = await redis.mget(...keys);
locksOwnedByMe = values.filter(
- (value) => value && value.startsWith(`${instanceId}:`)
+ (value) =>
+ value &&
+ value.startsWith(
+ `${instanceId}:`
+ )
).length;
}
diff --git a/server/private/lib/orgRebuildCounter.ts b/server/private/lib/orgRebuildCounter.ts
deleted file mode 100644
index 7107b9b8d..000000000
--- a/server/private/lib/orgRebuildCounter.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { redis } from "#private/lib/redis";
-import logger from "@server/logger";
-
-export const ORG_REBUILD_CONCURRENCY_LIMIT = 10;
-
-// Safety-net TTL: slightly longer than the rebuild lock TTL (120 s). If a
-// server process dies while holding a rebuild, this ensures the counter key
-// eventually expires rather than staying inflated forever.
-const ORG_REBUILD_COUNT_TTL_MS = 180000;
-const KEY_PREFIX = "rebuild-org-count:";
-
-// In-memory fallback used when Redis is unavailable.
-const localFallback = new Map();
-
-function isRedisReady(): boolean {
- return !!(redis && redis.status === "ready");
-}
-
-export async function incrementOrgRebuildCount(orgId: string): Promise {
- if (!isRedisReady()) {
- localFallback.set(orgId, (localFallback.get(orgId) ?? 0) + 1);
- return;
- }
- try {
- const key = `${KEY_PREFIX}${orgId}`;
- await redis!.incr(key);
- // Always refresh the TTL so the key doesn't expire while rebuilds are
- // still in progress. The TTL is purely a crash-recovery safety net.
- await redis!.pexpire(key, ORG_REBUILD_COUNT_TTL_MS);
- } catch (err) {
- logger.warn(
- `orgRebuildCounter: Redis increment failed for org ${orgId}, falling back to local:`,
- err
- );
- localFallback.set(orgId, (localFallback.get(orgId) ?? 0) + 1);
- }
-}
-
-export async function decrementOrgRebuildCount(orgId: string): Promise {
- if (!isRedisReady()) {
- const current = localFallback.get(orgId) ?? 0;
- if (current <= 1) {
- localFallback.delete(orgId);
- } else {
- localFallback.set(orgId, current - 1);
- }
- return;
- }
- try {
- const key = `${KEY_PREFIX}${orgId}`;
- const count = await redis!.decr(key);
- if (count <= 0) {
- await redis!.del(key);
- }
- } catch (err) {
- logger.warn(
- `orgRebuildCounter: Redis decrement failed for org ${orgId}, falling back to local:`,
- err
- );
- const current = localFallback.get(orgId) ?? 0;
- if (current <= 1) {
- localFallback.delete(orgId);
- } else {
- localFallback.set(orgId, current - 1);
- }
- }
-}
-
-export async function getOrgActiveRebuildCount(orgId: string): Promise {
- if (!isRedisReady()) {
- return localFallback.get(orgId) ?? 0;
- }
- try {
- const key = `${KEY_PREFIX}${orgId}`;
- const val = await redis!.get(key);
- return val ? parseInt(val, 10) : 0;
- } catch (err) {
- logger.warn(
- `orgRebuildCounter: Redis get failed for org ${orgId}, falling back to local:`,
- err
- );
- return localFallback.get(orgId) ?? 0;
- }
-}
-
-export async function checkOrgRebuildRateLimit(
- orgId: string
-): Promise {
- return (
- (await getOrgActiveRebuildCount(orgId)) >= ORG_REBUILD_CONCURRENCY_LIMIT
- );
-}
diff --git a/server/private/lib/rateLimit.ts b/server/private/lib/rateLimit.ts
index 32aa6419c..a8cf3c01c 100644
--- a/server/private/lib/rateLimit.ts
+++ b/server/private/lib/rateLimit.ts
@@ -12,7 +12,7 @@
*/
import logger from "@server/logger";
-import { regionalRedisManager as redisManager } from "#private/lib/redis";
+import redisManager from "#private/lib/redis";
import { build } from "@server/build";
// Rate limiting configuration
@@ -152,9 +152,10 @@ export class RateLimitService {
);
// Set TTL using the client directly - this prevents the key from persisting forever
- const writeClient = redisManager.getClient();
- if (writeClient) {
- await writeClient.expire(globalKey, RATE_LIMIT_WINDOW + 10);
+ if (redisManager.getClient()) {
+ await redisManager
+ .getClient()
+ .expire(globalKey, RATE_LIMIT_WINDOW + 10);
}
// Update tracking
@@ -203,12 +204,10 @@ export class RateLimitService {
);
// Set TTL using the client directly - this prevents the key from persisting forever
- const writeClient = redisManager.getClient();
- if (writeClient) {
- await writeClient.expire(
- messageTypeKey,
- RATE_LIMIT_WINDOW + 10
- );
+ if (redisManager.getClient()) {
+ await redisManager
+ .getClient()
+ .expire(messageTypeKey, RATE_LIMIT_WINDOW + 10);
}
// Update tracking
@@ -488,13 +487,16 @@ export class RateLimitService {
await redisManager.del(globalKey);
// Get all message type keys for this client and delete them
- const messageTypeKeys = await redisManager.keys(
- `ratelimit:${clientId}:*`
- );
- if (messageTypeKeys.length > 0) {
- await Promise.all(
- messageTypeKeys.map((key) => redisManager.del(key))
+ const client = redisManager.getClient();
+ if (client) {
+ const messageTypeKeys = await client.keys(
+ `ratelimit:${clientId}:*`
);
+ if (messageTypeKeys.length > 0) {
+ await Promise.all(
+ messageTypeKeys.map((key) => redisManager.del(key))
+ );
+ }
}
}
}
diff --git a/server/private/lib/rebuildQueue.ts b/server/private/lib/rebuildQueue.ts
deleted file mode 100644
index 87da95f60..000000000
--- a/server/private/lib/rebuildQueue.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { redis } from "#private/lib/redis";
-import { lockManager } from "#private/lib/lock";
-import logger from "@server/logger";
-import { isTransientError } from "@server/lib/dbRetry";
-
-export type RebuildJobType = "site-resource" | "client";
-
-export interface RebuildJob {
- type: RebuildJobType;
- id: number;
- // Number of times this job has already been re-queued after a transient
- // failure. Absent/0 means it has not failed yet.
- attempt?: number;
-}
-
-export interface RebuildJobHandlers {
- onSiteResource(siteResourceId: number): Promise;
- onClient(clientId: number): Promise;
-}
-
-// Redis list holding pending rebuild jobs (RPUSH to enqueue, LPOP to dequeue — FIFO order).
-const QUEUE_KEY = "rebuild-client-associations:queue";
-const QUEUED_SET_KEY = "rebuild-client-associations:queued";
-
-// Distributed lock that serialises queue consumption to a single server instance
-// at a time. TTL is generous enough to cover a full batch of expensive rebuilds.
-const PROCESSOR_LOCK_KEY = "rebuild-client-associations:processor";
-
-// Each rebuild can take up to REBUILD_ASSOCIATIONS_LOCK_TTL_MS (120 s) per
-// resource. Allow BATCH_SIZE resources per processor-lock acquisition, plus a
-// small buffer.
-const BATCH_SIZE = 5;
-const PROCESSOR_LOCK_TTL_MS = 120000 * BATCH_SIZE + 30000; // ~630 s
-
-const POLL_INTERVAL_MS = 500;
-
-// A job that fails with a transient DB error gets re-queued with backoff
-// instead of being dropped, up to this many times.
-const MAX_JOB_ATTEMPTS = 5;
-const JOB_RETRY_BASE_DELAY_MS = 1000;
-
-class RedisRebuildQueue {
- private processingStarted = false;
-
- async isQueued(job: RebuildJob): Promise {
- if (!redis || redis.status !== "ready") return false;
- const dedupeKey = `${job.type}:${job.id}`;
- try {
- const member = await redis.sismember(QUEUED_SET_KEY, dedupeKey);
- return member === 1;
- } catch {
- return false;
- }
- }
-
- async enqueue(job: RebuildJob): Promise {
- if (!redis || redis.status !== "ready") {
- logger.warn(
- `Rebuild queue: Redis not available — rebuild for ${job.type}:${job.id} will not be retried`
- );
- return;
- }
-
- try {
- const dedupeKey = `${job.type}:${job.id}`;
- const added = await redis.sadd(QUEUED_SET_KEY, dedupeKey);
- if (added === 0) {
- logger.debug(
- `Rebuild queue: skipped duplicate queued job ${job.type}:${job.id}`
- );
- return;
- }
-
- await redis.rpush(QUEUE_KEY, JSON.stringify(job));
- logger.debug(
- `Rebuild queue: enqueued ${job.type}:${job.id} (queue position: tail)`
- );
- } catch (err) {
- await redis
- .srem(QUEUED_SET_KEY, `${job.type}:${job.id}`)
- .catch((cleanupErr) =>
- logger.warn(
- `Rebuild queue: failed to cleanup dedupe key for ${job.type}:${job.id} after enqueue failure:`,
- cleanupErr
- )
- );
- logger.error(
- `Rebuild queue: failed to enqueue ${job.type}:${job.id}:`,
- err
- );
- }
- }
-
- startProcessing(handlers: RebuildJobHandlers): void {
- if (this.processingStarted) return;
- this.processingStarted = true;
-
- this.processLoop(handlers).catch((err) => {
- logger.error("Rebuild queue processor loop crashed:", err);
- });
-
- logger.info("Rebuild queue processor started");
- }
-
- private async processLoop(handlers: RebuildJobHandlers): Promise {
- while (true) {
- try {
- await this.tryProcessBatch(handlers);
- } catch (err) {
- logger.error(
- "Rebuild queue: unhandled error in process loop:",
- err
- );
- }
- await new Promise((resolve) =>
- setTimeout(resolve, POLL_INTERVAL_MS)
- );
- }
- }
-
- private async tryProcessBatch(handlers: RebuildJobHandlers): Promise {
- if (!redis || redis.status !== "ready") return;
-
- // Peek before acquiring the processor lock to avoid unnecessary Redis
- // round-trips and lock contention when the queue is idle.
- const queueLength = await redis.llen(QUEUE_KEY).catch(() => 0);
- if (queueLength === 0) return;
-
- try {
- await lockManager.withLock(
- PROCESSOR_LOCK_KEY,
- async () => {
- for (let i = 0; i < BATCH_SIZE; i++) {
- if (!redis || redis.status !== "ready") break;
-
- const payload = await redis.lpop(QUEUE_KEY);
- if (payload === null) break; // queue drained
-
- let job: RebuildJob;
- try {
- job = JSON.parse(payload) as RebuildJob;
- } catch {
- logger.error(
- `Rebuild queue: could not parse job payload, discarding: ${payload}`
- );
- continue;
- }
-
- // Remove from dedupe set once dequeued so the same job
- // can be re-queued while this one is in progress.
- await redis
- .srem(QUEUED_SET_KEY, `${job.type}:${job.id}`)
- .catch((cleanupErr) =>
- logger.warn(
- `Rebuild queue: failed to remove dedupe key for ${job.type}:${job.id} on dequeue:`,
- cleanupErr
- )
- );
-
- logger.debug(
- `Rebuild queue: processing ${job.type}:${job.id}`
- );
-
- try {
- if (job.type === "site-resource") {
- await handlers.onSiteResource(job.id);
- } else if (job.type === "client") {
- await handlers.onClient(job.id);
- } else {
- logger.warn(
- `Rebuild queue: unknown job type "${(job as any).type}", discarding`
- );
- }
-
- logger.debug(
- `Rebuild queue: completed ${job.type}:${job.id}`
- );
- } catch (err) {
- const attempt = (job.attempt ?? 0) + 1;
- if (
- isTransientError(err) &&
- attempt <= MAX_JOB_ATTEMPTS
- ) {
- const delay =
- JOB_RETRY_BASE_DELAY_MS *
- Math.pow(2, attempt - 1);
- logger.warn(
- `Rebuild queue: job ${job.type}:${job.id} hit a transient error (attempt ${attempt}/${MAX_JOB_ATTEMPTS}), re-queuing in ${delay}ms:`,
- err
- );
- setTimeout(() => {
- this.enqueue({ ...job, attempt }).catch(
- (enqueueErr) =>
- logger.error(
- `Rebuild queue: failed to re-queue ${job.type}:${job.id} after transient error:`,
- enqueueErr
- )
- );
- }, delay);
- } else {
- logger.error(
- `Rebuild queue: job ${job.type}:${job.id} threw an error:`,
- err
- );
- }
- }
- }
- },
- PROCESSOR_LOCK_TTL_MS
- );
- } catch (err: any) {
- if (
- typeof err?.message === "string" &&
- err.message.startsWith("Failed to acquire lock")
- ) {
- // Another server instance currently holds the processor lock and
- // is consuming the queue — nothing to do this cycle.
- logger.debug(
- "Rebuild queue: processor lock held by another instance, skipping this cycle"
- );
- } else {
- throw err;
- }
- }
- }
-}
-
-export const rebuildQueue: RedisRebuildQueue = new RedisRebuildQueue();
diff --git a/server/private/lib/redis.ts b/server/private/lib/redis.ts
index 7ff24ba93..7c3d67836 100644
--- a/server/private/lib/redis.ts
+++ b/server/private/lib/redis.ts
@@ -894,19 +894,6 @@ class RegionalRedisManager {
return opts;
}
- // The regional Redis StatefulSet's "redis" service pins to pod redis-0
- // (primary). The replica (redis-1) is only reachable through the
- // per-pod headless service: ..svc.cluster.local ->
- // redis-1.redis-headless..svc.cluster.local. Returns null
- // if the configured host doesn't match that pattern (e.g. local dev),
- // in which case callers should fall back to the primary for reads.
- private getReplicaHost(primaryHost: string): string | null {
- const match = primaryHost.match(/^redis\.([^.]+)\.svc\.cluster\.local$/);
- if (!match) return null;
- const namespace = match[1];
- return `redis-1.redis-headless.${namespace}.svc.cluster.local`;
- }
-
private initializeClients(): void {
const cfg = this.getConfig();
const baseOpts = {
@@ -920,42 +907,35 @@ class RegionalRedisManager {
try {
this.writeClient = new Redis(baseOpts);
+ // redis-1 (replica) handles reads; fall back to primary if not resolvable
+ this.readClient = new Redis({
+ ...baseOpts,
+ host: cfg.host!.replace(/^(.*?)(\.\S+)$/, (_, h, rest) => {
+ // Derive replica hostname from the headless service pattern:
+ // redis.redis.svc.cluster.local -> redis-1.redis-headless.redis.svc.cluster.local
+ // If it doesn't look like a k8s service, just use the same host
+ return h + rest;
+ })
+ });
- const replicaHost = this.getReplicaHost(cfg.host!);
- this.readClient = replicaHost
- ? new Redis({ ...baseOpts, host: replicaHost })
- : this.writeClient;
+ // For simplicity use same host for both; callers can always read from primary
+ // The real replica routing is handled by the StatefulSet headless service
+ this.readClient = this.writeClient;
this.writeClient.on("ready", () => {
- logger.info("Regional Redis write client ready");
+ logger.info("Regional Redis client ready");
this.isHealthy = true;
});
this.writeClient.on("error", (err) => {
- logger.error("Regional Redis write client error:", err);
+ logger.error("Regional Redis client error:", err);
this.isHealthy = false;
});
this.writeClient.on("reconnecting", () => {
- logger.info("Regional Redis write client reconnecting...");
+ logger.info("Regional Redis client reconnecting...");
this.isHealthy = false;
});
- if (this.readClient !== this.writeClient) {
- this.readClient.on("ready", () => {
- logger.info("Regional Redis read client ready");
- });
- this.readClient.on("error", (err) => {
- logger.error("Regional Redis read client error:", err);
- });
- this.readClient.on("reconnecting", () => {
- logger.info("Regional Redis read client reconnecting...");
- });
- }
-
- logger.info(
- replicaHost
- ? `Regional Redis client initialized (reads routed to replica ${replicaHost})`
- : "Regional Redis client initialized (no replica resolvable, reads routed to primary)"
- );
+ logger.info("Regional Redis client initialized");
} catch (error) {
logger.error("Failed to initialize regional Redis client:", error);
this.isEnabled = false;
@@ -1020,55 +1000,13 @@ class RegionalRedisManager {
}
}
- public getClient(): Redis | null {
- return this.writeClient;
- }
-
- public async hget(key: string, field: string): Promise {
- if (!this.isRedisEnabled() || !this.readClient) return null;
- try {
- return await this.readClient.hget(key, field);
- } catch (error) {
- logger.error("Regional Redis HGET error:", error);
- return null;
- }
- }
-
- public async hset(
- key: string,
- field: string,
- value: string
- ): Promise {
- if (!this.isRedisEnabled() || !this.writeClient) return false;
- try {
- await this.writeClient.hset(key, field, value);
- return true;
- } catch (error) {
- logger.error("Regional Redis HSET error:", error);
- return false;
- }
- }
-
- public async hgetall(key: string): Promise> {
- if (!this.isRedisEnabled() || !this.readClient) return {};
- try {
- return await this.readClient.hgetall(key);
- } catch (error) {
- logger.error("Regional Redis HGETALL error:", error);
- return {};
- }
- }
-
public async disconnect(): Promise {
try {
- if (this.readClient && this.readClient !== this.writeClient) {
- await this.readClient.quit();
- }
- this.readClient = null;
if (this.writeClient) {
await this.writeClient.quit();
this.writeClient = null;
}
+ this.readClient = null;
logger.info("Regional Redis client disconnected");
} catch (error) {
logger.error("Error disconnecting regional Redis client:", error);
diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts
index d47352e04..e81715d3b 100644
--- a/server/private/lib/traefik/getTraefikConfig.ts
+++ b/server/private/lib/traefik/getTraefikConfig.ts
@@ -84,8 +84,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false,
generateLoginPageRouters = false,
allowRawResources = true,
- maintenancePageUiUrl: string | null = null,
- browserGatewayUiUrl: string | null = null
+ allowMaintenancePage = true,
+ allowBrowserGatewayResources = true
): Promise {
// Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources
@@ -317,7 +317,7 @@ export async function getTraefikConfig(
BrowserGatewayResourceEntry
>();
- if (browserGatewayUiUrl) {
+ if (allowBrowserGatewayResources) {
for (const row of resourcesWithTargetsAndSites) {
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
continue;
@@ -630,11 +630,10 @@ export async function getTraefikConfig(
}
}
- if (showMaintenancePage && maintenancePageUiUrl) {
+ if (showMaintenancePage && allowMaintenancePage) {
const maintenanceServiceName = `${key}-maintenance-service`;
const maintenanceRouterName = `${key}-maintenance-router`;
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
- const maintenanceHeadersMiddlewareName = `${key}-maintenance-headers`;
const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint;
@@ -647,11 +646,15 @@ export async function getTraefikConfig(
? `*.${domainParts.slice(1).join(".")}`
: fullDomain;
+ const maintenancePort = config.getRawConfig().server.next_port;
+ const maintenanceHost =
+ config.getRawConfig().server.internal_hostname;
+
config_output.http.services[maintenanceServiceName] = {
loadBalancer: {
servers: [
{
- url: maintenancePageUiUrl
+ url: `http://${maintenanceHost}:${maintenancePort}`
}
],
passHostHeader: true
@@ -670,26 +673,12 @@ export async function getTraefikConfig(
}
};
- config_output.http.middlewares[
- maintenanceHeadersMiddlewareName
- ] = {
- headers: {
- customRequestHeaders: {
- Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
- "p-host": fullDomain
- }
- }
- };
-
config_output.http.routers[maintenanceRouterName] = {
entryPoints: [
resource.ssl ? entrypointHttps : entrypointHttp
],
service: maintenanceServiceName,
- middlewares: [
- rewriteMiddlewareName,
- maintenanceHeadersMiddlewareName
- ],
+ middlewares: [rewriteMiddlewareName],
rule: rule,
priority: 2000,
...(resource.ssl ? { tls } : {})
@@ -702,7 +691,6 @@ export async function getTraefikConfig(
resource.ssl ? entrypointHttps : entrypointHttp
],
service: maintenanceServiceName,
- middlewares: [maintenanceHeadersMiddlewareName],
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `,
priority: 2001,
...(resource.ssl ? { tls } : {})
@@ -1039,7 +1027,7 @@ export async function getTraefikConfig(
}
}
- if (browserGatewayUiUrl) {
+ if (allowBrowserGatewayResources) {
// Generate Traefik config for browser gateway resources
const browserGatewayPort = 39999;
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
@@ -1131,17 +1119,20 @@ export async function getTraefikConfig(
}
}
- if (showBgMaintenancePage && maintenancePageUiUrl) {
+ if (showBgMaintenancePage && allowMaintenancePage) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
- const bgMaintenanceHeadersMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-headers`;
const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint;
+ const maintenancePort = config.getRawConfig().server.next_port;
+ const maintenanceHost =
+ config.getRawConfig().server.internal_hostname;
+
if (!config_output.http.services)
config_output.http.services = {};
if (!config_output.http.middlewares)
@@ -1153,7 +1144,7 @@ export async function getTraefikConfig(
loadBalancer: {
servers: [
{
- url: maintenancePageUiUrl
+ url: `http://${maintenanceHost}:${maintenancePort}`
}
],
passHostHeader: true
@@ -1167,26 +1158,12 @@ export async function getTraefikConfig(
}
};
- config_output.http.middlewares![
- bgMaintenanceHeadersMiddlewareName
- ] = {
- headers: {
- customRequestHeaders: {
- Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
- "p-host": fullDomain
- }
- }
- };
-
config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
- middlewares: [
- bgRewriteMiddlewareName,
- bgMaintenanceHeadersMiddlewareName
- ],
+ middlewares: [bgRewriteMiddlewareName],
rule: hostRule,
priority: 2000,
...(bgResource.ssl ? { tls } : {})
@@ -1199,7 +1176,6 @@ export async function getTraefikConfig(
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
- middlewares: [bgMaintenanceHeadersMiddlewareName],
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001,
...(bgResource.ssl ? { tls } : {})
@@ -1258,8 +1234,9 @@ export async function getTraefikConfig(
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
// how the maintenance page rewrites everything to /maintenance-screen.
const primaryType = typeMap.keys().next().value as string;
+ const internalHost = config.getRawConfig().server.internal_hostname;
+ const internalPort = config.getRawConfig().server.next_port;
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
- const uiHeadersMiddlewareName = `bg-r${bgResource.resourceId}-ui-headers`;
const entrypoint = bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint;
@@ -1275,33 +1252,22 @@ export async function getTraefikConfig(
}
};
- config_output.http.middlewares![uiHeadersMiddlewareName] = {
- headers: {
- customRequestHeaders: {
- Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
- "p-host": fullDomain
- }
- }
- };
-
config_output.http.services![bgUiServiceName] = {
loadBalancer: {
servers: [
{
- url: browserGatewayUiUrl
+ url: `http://${internalHost}:${internalPort}`
}
]
}
};
- // Assets router at higher priority so /_next files load without rewrite.
- // Do NOT apply the path-rewrite middleware here — static assets must
- // keep their original path; only the host headers are needed.
+ // Assets router at higher priority so /_next files load without rewrite
config_output.http.routers![
`bg-r${bgResource.resourceId}-assets-router`
] = {
entryPoints: [entrypoint],
- middlewares: [...routerMiddlewares, uiHeadersMiddlewareName],
+ middlewares: routerMiddlewares,
service: bgUiServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101,
@@ -1313,11 +1279,7 @@ export async function getTraefikConfig(
`bg-r${bgResource.resourceId}-ui-router`
] = {
entryPoints: [entrypoint],
- middlewares: [
- ...routerMiddlewares,
- uiRewriteMiddlewareName,
- uiHeadersMiddlewareName
- ],
+ middlewares: [...routerMiddlewares, uiRewriteMiddlewareName],
service: bgUiServiceName,
rule: hostRule,
priority: 100,
@@ -1350,6 +1312,10 @@ export async function getTraefikConfig(
const siteResourceRouterName = `${srKey}-router`;
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
+ const maintenancePort = config.getRawConfig().server.next_port;
+ const maintenanceHost =
+ config.getRawConfig().server.internal_hostname;
+
if (!config_output.http.routers) {
config_output.http.routers = {};
}
@@ -1365,7 +1331,7 @@ export async function getTraefikConfig(
loadBalancer: {
servers: [
{
- url: maintenancePageUiUrl
+ url: `http://${maintenanceHost}:${maintenancePort}`
}
],
passHostHeader: true
diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts
index 3ae01b338..92d5d9cfe 100644
--- a/server/private/middlewares/verifySubscription.ts
+++ b/server/private/middlewares/verifySubscription.ts
@@ -17,7 +17,6 @@ import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { Tier } from "@server/types/Tiers";
-import logger from "@server/logger";
export function verifyValidSubscription(tiers: Tier[]) {
return async function (
@@ -31,9 +30,9 @@ export function verifyValidSubscription(tiers: Tier[]) {
}
const orgId =
- req.params?.orgId ||
- req.body?.orgId ||
- req.query?.orgId ||
+ req.params.orgId ||
+ req.body.orgId ||
+ req.query.orgId ||
req.userOrgId;
if (!orgId) {
@@ -66,7 +65,6 @@ export function verifyValidSubscription(tiers: Tier[]) {
return next();
} catch (e) {
- logger.error(e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
diff --git a/server/private/routers/auditLogs/index.ts b/server/private/routers/auditLogs/index.ts
index 5fffa2b86..aacd37635 100644
--- a/server/private/routers/auditLogs/index.ts
+++ b/server/private/routers/auditLogs/index.ts
@@ -17,4 +17,3 @@ export * from "./queryAccessAuditLog";
export * from "./exportAccessAuditLog";
export * from "./queryConnectionAuditLog";
export * from "./exportConnectionAuditLog";
-export * from "./logAccessAuditAttempt";
diff --git a/server/private/routers/auditLogs/logAccessAuditAttempt.ts b/server/private/routers/auditLogs/logAccessAuditAttempt.ts
deleted file mode 100644
index 5b0fb9c92..000000000
--- a/server/private/routers/auditLogs/logAccessAuditAttempt.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { NextFunction } from "express";
-import { Request, Response } from "express";
-import { z } from "zod";
-import createHttpError from "http-errors";
-import HttpCode from "@server/types/HttpCode";
-import { fromError } from "zod-validation-error";
-import response from "@server/lib/response";
-import logger from "@server/logger";
-import { logAccessAudit } from "#private/lib/logAccessAudit";
-
-export const logAccessAuditAttemptSchema = z.object({
- resourceId: z.number().int().positive(),
- action: z.boolean(),
- type: z.enum(["login", "ssh", "vnc", "rdp"])
-});
-
-export const logAccessAuditAttemptParams = z.object({
- orgId: z.string()
-});
-
-export async function logAccessAuditAttempt(
- req: Request,
- res: Response,
- next: NextFunction
-): Promise {
- try {
- const parsedBody = logAccessAuditAttemptSchema.safeParse(req.body);
- if (!parsedBody.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromError(parsedBody.error)
- )
- );
- }
- const parsedParams = logAccessAuditAttemptParams.safeParse(req.params);
- if (!parsedParams.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromError(parsedParams.error)
- )
- );
- }
-
- const { orgId } = parsedParams.data;
- const { resourceId, action, type } = parsedBody.data;
-
- const username = req.user?.username;
- const userId = req.user?.userId;
-
- await logAccessAudit({
- orgId: orgId,
- resourceId: resourceId,
- action: action,
- ...(username && userId
- ? {
- user: {
- username,
- userId
- }
- }
- : {}),
- type: type,
- userAgent: req.headers["user-agent"],
- requestIp: req.ip
- });
-
- return response(res, {
- data: null,
- success: true,
- error: false,
- message: "Access audit attempt logged 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/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts
index 9e819db64..0feca4154 100644
--- a/server/private/routers/auditLogs/queryAccessAuditLog.ts
+++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts
@@ -22,7 +22,7 @@ import {
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
-import { eq, gt, lt, and, count, desc, inArray, isNull, or } 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";
@@ -120,10 +120,7 @@ function getWhere(data: Q) {
lt(accessAuditLog.timestamp, data.timeEnd),
eq(accessAuditLog.orgId, data.orgId),
data.resourceId
- ? or(
- eq(accessAuditLog.resourceId, data.resourceId),
- eq(accessAuditLog.siteResourceId, data.resourceId)
- )
+ ? eq(accessAuditLog.resourceId, data.resourceId)
: undefined,
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
data.actorType
@@ -236,6 +233,7 @@ async function enrichWithResourceDetails(
const details = siteResourceMap.get(log.siteResourceId);
return {
...log,
+ resourceId: log.siteResourceId,
resourceName: details?.name ?? null,
resourceNiceId: details?.niceId ?? null
};
diff --git a/server/private/routers/billing/changeTier.ts b/server/private/routers/billing/changeTier.ts
index e532e88fd..d82cbfeea 100644
--- a/server/private/routers/billing/changeTier.ts
+++ b/server/private/routers/billing/changeTier.ts
@@ -25,7 +25,7 @@ import {
getTier1FeaturePriceSet,
getTier3FeaturePriceSet,
getTier2FeaturePriceSet,
- LimitId,
+ FeatureId,
type FeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
@@ -214,7 +214,7 @@ export async function changeTier(
}
// Map to the corresponding feature in the new tier
- const newPriceId = targetPriceSet[LimitId.USERS];
+ const newPriceId = targetPriceSet[FeatureId.USERS];
if (newPriceId) {
return {
diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts
index 1910a6a33..718559654 100644
--- a/server/private/routers/billing/getOrgUsage.ts
+++ b/server/private/routers/billing/getOrgUsage.ts
@@ -24,7 +24,7 @@ import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { Limit, limits, Usage, usage } from "@server/db";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
import { GetOrgUsageResponse } from "@server/routers/billing/types";
const getOrgSchema = z.strictObject({
@@ -93,28 +93,16 @@ export async function getOrgUsage(
// Get usage for org
const usageData = [];
- const sites = await usageService.getUsage(orgId, LimitId.SITES);
- const users = await usageService.getUsage(orgId, LimitId.USERS);
- const domains = await usageService.getUsage(orgId, LimitId.DOMAINS);
+ const sites = await usageService.getUsage(orgId, FeatureId.SITES);
+ const users = await usageService.getUsage(orgId, FeatureId.USERS);
+ const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS);
const remoteExitNodes = await usageService.getUsage(
orgId,
- LimitId.REMOTE_EXIT_NODES
+ FeatureId.REMOTE_EXIT_NODES
);
const organizations = await usageService.getUsage(
orgId,
- LimitId.ORGANIZATIONS
- );
- const publicResources = await usageService.getUsage(
- orgId,
- LimitId.PUBLIC_RESOURCES
- );
- const privateResources = await usageService.getUsage(
- orgId,
- LimitId.PRIVATE_RESOURCES
- );
- const machineClients = await usageService.getUsage(
- orgId,
- LimitId.MACHINE_CLIENTS
+ FeatureId.ORGINIZATIONS
);
// const egressData = await usageService.getUsage(
// orgId,
@@ -139,15 +127,6 @@ export async function getOrgUsage(
if (organizations) {
usageData.push(organizations);
}
- if (publicResources) {
- usageData.push(publicResources);
- }
- if (privateResources) {
- usageData.push(privateResources);
- }
- if (machineClients) {
- usageData.push(machineClients);
- }
const orgLimits = await db
.select()
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index 75a5b8d48..3cecd2ebb 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -31,6 +31,7 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
+import * as labels from "#private/routers/labels";
import * as client from "@server/routers/client";
import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy";
@@ -328,44 +329,6 @@ authenticated.delete(
remoteExitNode.deleteRemoteExitNode
);
-authenticated.get(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId/resources",
- verifyValidLicense,
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.getRemoteExitNode),
- remoteExitNode.listRemoteExitNodeResources
-);
-
-authenticated.post(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId/resources",
- verifyValidLicense,
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.updateRemoteExitNode),
- logActionAudit(ActionsEnum.updateRemoteExitNode),
- remoteExitNode.setRemoteExitNodeResources
-);
-
-authenticated.get(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId/preference-labels",
- verifyValidLicense,
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.getRemoteExitNode),
- remoteExitNode.listRemoteExitNodePreferenceLabels
-);
-
-authenticated.post(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId/preference-labels",
- verifyValidLicense,
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.updateRemoteExitNode),
- logActionAudit(ActionsEnum.updateRemoteExitNode),
- remoteExitNode.setRemoteExitNodePreferenceLabels
-);
-
authenticated.put(
"/org/:orgId/login-page",
verifyValidLicense,
@@ -532,31 +495,29 @@ authRouter.post(
auth.transferSession
);
-if (build !== "saas") {
- authenticated.post(
- "/license/activate",
- verifyUserIsServerAdmin,
- license.activateLicense
- );
+authenticated.post(
+ "/license/activate",
+ verifyUserIsServerAdmin,
+ license.activateLicense
+);
- authenticated.get(
- "/license/keys",
- verifyUserIsServerAdmin,
- license.listLicenseKeys
- );
+authenticated.get(
+ "/license/keys",
+ verifyUserIsServerAdmin,
+ license.listLicenseKeys
+);
- authenticated.delete(
- "/license/:licenseKey",
- verifyUserIsServerAdmin,
- license.deleteLicenseKey
- );
+authenticated.delete(
+ "/license/:licenseKey",
+ verifyUserIsServerAdmin,
+ license.deleteLicenseKey
+);
- authenticated.post(
- "/license/recheck",
- verifyUserIsServerAdmin,
- license.recheckStatus
- );
-}
+authenticated.post(
+ "/license/recheck",
+ verifyUserIsServerAdmin,
+ license.recheckStatus
+);
authenticated.get(
"/org/:orgId/logs/action",
@@ -809,6 +770,59 @@ authenticated.get(
alertRule.getAlertRule
);
+authenticated.get(
+ "/org/:orgId/labels",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyValidSubscription(tierMatrix.labels),
+ verifyUserHasAction(ActionsEnum.listOrgLabels),
+ labels.listOrgLabels
+);
+
+authenticated.post(
+ "/org/:orgId/labels",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyValidSubscription(tierMatrix.labels),
+ verifyUserHasAction(ActionsEnum.createOrgLabel),
+ labels.createOrgLabel
+);
+
+authenticated.patch(
+ "/org/:orgId/label/:labelId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyValidSubscription(tierMatrix.labels),
+ verifyUserHasAction(ActionsEnum.updateOrgLabel),
+ labels.updateOrgLabel
+);
+
+authenticated.delete(
+ "/org/:orgId/label/:labelId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.deleteOrgLabel),
+ labels.deleteOrgLabel
+);
+
+authenticated.put(
+ "/org/:orgId/label/:labelId/attach",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyValidSubscription(tierMatrix.labels),
+ verifyUserHasAction(ActionsEnum.attachLabelToItem),
+ labels.attachLabelToItem
+);
+
+authenticated.put(
+ "/org/:orgId/label/:labelId/detach",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyValidSubscription(tierMatrix.labels),
+ verifyUserHasAction(ActionsEnum.detachLabelFromItem),
+ labels.detachLabelFromItem
+);
+
authenticated.get(
"/org/:orgId/health-checks",
verifyValidLicense,
@@ -864,9 +878,3 @@ authenticated.post(
verifyClientAccess,
client.rebuildClientAssociationsCacheRoute
);
-
-authenticated.post(
- "/org/:orgId/logs/access/attempt",
- verifyOrgAccess,
- logs.logAccessAuditAttempt
-);
diff --git a/server/private/routers/gerbil/createExitNode.ts b/server/private/routers/gerbil/createExitNode.ts
index cfa7c42eb..818c5f0e1 100644
--- a/server/private/routers/gerbil/createExitNode.ts
+++ b/server/private/routers/gerbil/createExitNode.ts
@@ -29,41 +29,37 @@ export async function createExitNode(
.where(eq(exitNodes.publicKey, publicKey));
let exitNode: ExitNode;
if (!exitNodeQuery) {
- const { value: address, release } = await getNextAvailableSubnet();
- try {
- // TODO: eventually we will want to get the next available port so that we can multiple exit nodes
- // const listenPort = await getNextAvailablePort();
- const listenPort = config.getRawConfig().gerbil.start_port;
- let subEndpoint = "";
- if (config.getRawConfig().gerbil.use_subdomain) {
- subEndpoint = await getUniqueExitNodeEndpointName();
- }
-
- const exitNodeName =
- config.getRawConfig().gerbil.exit_node_name ||
- `Exit Node ${publicKey.slice(0, 8)}`;
-
- // create a new exit node
- [exitNode] = await db
- .insert(exitNodes)
- .values({
- publicKey,
- endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
- address,
- listenPort,
- online: true,
- reachableAt,
- name: exitNodeName
- })
- .returning()
- .execute();
-
- logger.info(
- `Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
- );
- } finally {
- await release();
+ const address = await getNextAvailableSubnet();
+ // TODO: eventually we will want to get the next available port so that we can multiple exit nodes
+ // const listenPort = await getNextAvailablePort();
+ const listenPort = config.getRawConfig().gerbil.start_port;
+ let subEndpoint = "";
+ if (config.getRawConfig().gerbil.use_subdomain) {
+ subEndpoint = await getUniqueExitNodeEndpointName();
}
+
+ const exitNodeName =
+ config.getRawConfig().gerbil.exit_node_name ||
+ `Exit Node ${publicKey.slice(0, 8)}`;
+
+ // create a new exit node
+ [exitNode] = await db
+ .insert(exitNodes)
+ .values({
+ publicKey,
+ endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
+ address,
+ listenPort,
+ online: true,
+ reachableAt,
+ name: exitNodeName
+ })
+ .returning()
+ .execute();
+
+ logger.info(
+ `Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
+ );
} else {
// update the reachable at
[exitNode] = await db
diff --git a/server/private/routers/healthChecks/createHealthCheck.ts b/server/private/routers/healthChecks/createHealthCheck.ts
index 6f49f0f18..aa3706833 100644
--- a/server/private/routers/healthChecks/createHealthCheck.ts
+++ b/server/private/routers/healthChecks/createHealthCheck.ts
@@ -29,40 +29,26 @@ const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
-const bodySchema = z
- .strictObject({
- name: z.string().nonempty(),
- siteId: z.number().int().positive(),
- hcEnabled: z.boolean().default(false),
- hcMode: z.string().default("http"),
- hcHostname: z.string().optional(),
- hcPort: z.number().int().min(1).max(65535).optional(),
- hcPath: z.string().optional(),
- hcScheme: z.string().optional(),
- hcMethod: z.string().default("GET"),
- hcInterval: z.number().int().positive().default(30),
- hcUnhealthyInterval: z.number().int().positive().default(30),
- hcTimeout: z.number().int().positive().default(1),
- hcHeaders: z.string().optional().nullable(),
- hcFollowRedirects: z.boolean().default(true),
- hcStatus: z.number().int().optional().nullable(),
- hcTlsServerName: z.string().optional(),
- hcHealthyThreshold: z.number().int().positive().default(1),
- hcUnhealthyThreshold: z.number().int().positive().default(1)
- })
- .superRefine((data, ctx) => {
- const hcHostnameMissing =
- data.hcHostname === undefined ||
- data.hcHostname.trim().length === 0;
-
- if (data.hcEnabled === true && hcHostnameMissing) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- path: ["hcHostname"],
- message: "hcHostname is required when hcEnabled is true"
- });
- }
- });
+const bodySchema = z.strictObject({
+ name: z.string().nonempty(),
+ siteId: z.number().int().positive(),
+ hcEnabled: z.boolean().default(false),
+ hcMode: z.string().default("http"),
+ hcHostname: z.string().optional(),
+ hcPort: z.number().int().min(1).max(65535).optional(),
+ hcPath: z.string().optional(),
+ hcScheme: z.string().optional(),
+ hcMethod: z.string().default("GET"),
+ hcInterval: z.number().int().positive().default(30),
+ hcUnhealthyInterval: z.number().int().positive().default(30),
+ hcTimeout: z.number().int().positive().default(1),
+ hcHeaders: z.string().optional().nullable(),
+ hcFollowRedirects: z.boolean().default(true),
+ hcStatus: z.number().int().optional().nullable(),
+ hcTlsServerName: z.string().optional(),
+ hcHealthyThreshold: z.number().int().positive().default(1),
+ hcUnhealthyThreshold: z.number().int().positive().default(1)
+});
export type CreateHealthCheckResponse = {
targetHealthCheckId: number;
@@ -71,6 +57,7 @@ const CreateHealthCheckResponseDataSchema = z.object({
targetHealthCheckId: z.number()
});
+
registry.registerPath({
method: "put",
path: "/org/{orgId}/health-check",
@@ -91,9 +78,7 @@ registry.registerPath({
description: "Successful response",
content: {
"application/json": {
- schema: createApiResponseSchema(
- CreateHealthCheckResponseDataSchema
- )
+ schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema)
}
}
}
diff --git a/server/private/routers/healthChecks/updateHealthCheck.ts b/server/private/routers/healthChecks/updateHealthCheck.ts
index 4fb7a624b..f08324f9b 100644
--- a/server/private/routers/healthChecks/updateHealthCheck.ts
+++ b/server/private/routers/healthChecks/updateHealthCheck.ts
@@ -105,6 +105,7 @@ const UpdateHealthCheckResponseDataSchema = z.object({
hcUnhealthyThreshold: z.number().nullable()
});
+
registry.registerPath({
method: "post",
path: "/org/{orgId}/health-check/{healthCheckId}",
@@ -125,9 +126,7 @@ registry.registerPath({
description: "Successful response",
content: {
"application/json": {
- schema: createApiResponseSchema(
- UpdateHealthCheckResponseDataSchema
- )
+ schema: createApiResponseSchema(UpdateHealthCheckResponseDataSchema)
}
}
}
@@ -216,32 +215,6 @@ export async function updateHealthCheck(
)
.limit(1);
- if (!existingHealthCheck) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- "Standalone health check not found"
- )
- );
- }
-
- const nextHcEnabled = hcEnabled ?? existingHealthCheck.hcEnabled;
- const nextHcHostname =
- hcHostname !== undefined
- ? hcHostname
- : existingHealthCheck.hcHostname;
- const hcHostnameMissing =
- !nextHcHostname || nextHcHostname.trim().length === 0;
-
- if (nextHcEnabled && hcHostnameMissing) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "hcHostname is required when hcEnabled is true"
- )
- );
- }
-
if (name !== undefined) updateData.name = name;
if (siteId !== undefined) updateData.siteId = siteId;
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts
index 124db587a..c6be3e7d1 100644
--- a/server/private/routers/hybrid.ts
+++ b/server/private/routers/hybrid.ts
@@ -277,8 +277,6 @@ hybridRouter.get(
);
}
- const pangolinUIUrl = config.getRawConfig().app.dashboard_url; // points to the dashboard to serve from there
-
try {
const traefikConfig = await getTraefikConfig(
remoteExitNode.exitNodeId,
@@ -286,8 +284,8 @@ hybridRouter.get(
true, // But don't allow domain namespace resources
false, // Dont include login pages,
true, // allow raw resources
- pangolinUIUrl, // dont generate maintenance page
- pangolinUIUrl // generate browser gateway targets
+ false, // dont generate maintenance page
+ false // dont generate browser gateway targets
);
return response(res, {
@@ -1695,7 +1693,6 @@ hybridRouter.get(
) {
for (const rule of rules) {
if (rule.match == "COUNTRY") {
- // @ts-expect-error this is for backward compatibility
rule.match = "GEOIP";
}
}
diff --git a/server/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts
similarity index 94%
rename from server/routers/labels/attachLabelToItem.ts
rename to server/private/routers/labels/attachLabelToItem.ts
index 0d9e49b35..d011a606d 100644
--- a/server/routers/labels/attachLabelToItem.ts
+++ b/server/private/routers/labels/attachLabelToItem.ts
@@ -1,3 +1,16 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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 {
clients,
clientLabels,
diff --git a/server/routers/labels/createOrgLabel.ts b/server/private/routers/labels/createOrgLabel.ts
similarity index 91%
rename from server/routers/labels/createOrgLabel.ts
rename to server/private/routers/labels/createOrgLabel.ts
index 6fa20816e..c856eecf4 100644
--- a/server/routers/labels/createOrgLabel.ts
+++ b/server/private/routers/labels/createOrgLabel.ts
@@ -1,3 +1,15 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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,
labels,
diff --git a/server/routers/labels/deleteOrgLabel.ts b/server/private/routers/labels/deleteOrgLabel.ts
similarity index 82%
rename from server/routers/labels/deleteOrgLabel.ts
rename to server/private/routers/labels/deleteOrgLabel.ts
index c46e67437..f091c910a 100644
--- a/server/routers/labels/deleteOrgLabel.ts
+++ b/server/private/routers/labels/deleteOrgLabel.ts
@@ -1,3 +1,15 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/routers/labels/detachLabelFromItem.ts b/server/private/routers/labels/detachLabelFromItem.ts
similarity index 94%
rename from server/routers/labels/detachLabelFromItem.ts
rename to server/private/routers/labels/detachLabelFromItem.ts
index 52c752ca8..9a5545312 100644
--- a/server/routers/labels/detachLabelFromItem.ts
+++ b/server/private/routers/labels/detachLabelFromItem.ts
@@ -1,3 +1,16 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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 {
clients,
clientLabels,
diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts
new file mode 100644
index 000000000..d988d8e38
--- /dev/null
+++ b/server/private/routers/labels/index.ts
@@ -0,0 +1,19 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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 "./listOrgLabels";
+export * from "./createOrgLabel";
+export * from "./updateOrgLabel";
+export * from "./attachLabelToItem";
+export * from "./detachLabelFromItem";
+export * from "./deleteOrgLabel";
diff --git a/server/routers/labels/listOrgLabels.ts b/server/private/routers/labels/listOrgLabels.ts
similarity index 91%
rename from server/routers/labels/listOrgLabels.ts
rename to server/private/routers/labels/listOrgLabels.ts
index 6d6b853cf..dc2b50017 100644
--- a/server/routers/labels/listOrgLabels.ts
+++ b/server/private/routers/labels/listOrgLabels.ts
@@ -1,3 +1,16 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/routers/labels/updateOrgLabel.ts b/server/private/routers/labels/updateOrgLabel.ts
similarity index 89%
rename from server/routers/labels/updateOrgLabel.ts
rename to server/private/routers/labels/updateOrgLabel.ts
index fe8a6f573..134a01f02 100644
--- a/server/routers/labels/updateOrgLabel.ts
+++ b/server/private/routers/labels/updateOrgLabel.ts
@@ -1,3 +1,16 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 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, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/private/routers/org/sendTrialNotification.ts b/server/private/routers/org/sendTrialNotification.ts
index d3210530a..233010064 100644
--- a/server/private/routers/org/sendTrialNotification.ts
+++ b/server/private/routers/org/sendTrialNotification.ts
@@ -215,7 +215,7 @@ export async function sendTrialNotification(
if (resetLimits) {
// this will only fire if they have not upgraded yet because when upgrading we delete the trial
- await handleSubscriptionLifesycle(orgId, "canceled");
+ await handleSubscriptionLifesycle(orgId, "cancled");
logger.debug(
`Trial ended for org ${orgId}, limits reset to free tier`
);
diff --git a/server/private/routers/orgIdp/unassociateOrgIdp.ts b/server/private/routers/orgIdp/unassociateOrgIdp.ts
index d9c0bfc8f..41b2e6c89 100644
--- a/server/private/routers/orgIdp/unassociateOrgIdp.ts
+++ b/server/private/routers/orgIdp/unassociateOrgIdp.ts
@@ -23,7 +23,6 @@ import { and, eq, sql } from "drizzle-orm";
import { removeUserFromOrg } from "@server/lib/userOrg";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { OpenAPITags, registry } from "@server/openApi";
-import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
const paramsSchema = z
.object({
@@ -91,15 +90,6 @@ export async function unassociateOrgIdp(
);
}
- if (await isOrgRebuildRateLimited(org.orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
const orgUsersFromIdp = await db
.select({
userId: userOrgs.userId,
@@ -131,7 +121,7 @@ export async function unassociateOrgIdp(
});
for (const userId of userIdsToRemove) {
- calculateUserClientsForOrgs(userId).catch((e) => {
+ calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
logger.error(
`Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}`
);
diff --git a/server/private/routers/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts
index bf86ed107..d7e889222 100644
--- a/server/private/routers/remoteExitNode/createRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts
@@ -35,7 +35,7 @@ import logger from "@server/logger";
import { and, eq, inArray, ne } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export const paramsSchema = z.object({
@@ -79,10 +79,7 @@ export async function createRemoteExitNode(
const { remoteExitNodeId, secret } = parsedBody.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")
);
@@ -90,13 +87,13 @@ export async function createRemoteExitNode(
const usage = await usageService.getUsage(
orgId,
- LimitId.REMOTE_EXIT_NODES
+ FeatureId.REMOTE_EXIT_NODES
);
if (usage) {
const rejectRemoteExitNodes = await usageService.checkLimitSet(
orgId,
- LimitId.REMOTE_EXIT_NODES,
+ FeatureId.REMOTE_EXIT_NODES,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -114,6 +111,8 @@ export async function createRemoteExitNode(
}
const secretHash = await hashPassword(secret);
+ // const address = await getNextAvailableSubnet();
+ const address = "100.89.140.1/24"; // FOR NOW LETS HARDCODE THESE ADDRESSES
const [existingRemoteExitNode] = await db
.select()
@@ -189,106 +188,89 @@ export async function createRemoteExitNode(
);
}
- // If this remote exit node isn't already backing an exit node in
- // another org, we're about to create a brand new one. Reserve a
- // subnet for it up front so the allocation lock is held across the
- // whole insert - this guarantees exit node subnets never overlap,
- // even under concurrent creation, which matters for HA setups.
- let releaseSubnetLock: (() => Promise) | null = null;
- let newExitNodeAddress: string | null = null;
- if (!existingExitNode) {
- const { value, release } = await getNextAvailableSubnet();
- newExitNodeAddress = value;
- releaseSubnetLock = release;
- }
+ await db.transaction(async (trx) => {
+ if (!existingExitNode) {
+ const [res] = await trx
+ .insert(exitNodes)
+ .values({
+ name: remoteExitNodeId,
+ address,
+ endpoint: "",
+ publicKey: "",
+ listenPort: 0,
+ online: false,
+ type: "remoteExitNode"
+ })
+ .returning();
+ existingExitNode = res;
+ }
- try {
- await db.transaction(async (trx) => {
- if (!existingExitNode) {
- const [res] = await trx
- .insert(exitNodes)
- .values({
- name: remoteExitNodeId,
- address: newExitNodeAddress!,
- endpoint: "",
- publicKey: "",
- listenPort: 0,
- online: false,
- type: "remoteExitNode"
- })
- .returning();
- existingExitNode = res;
- }
-
- if (!existingRemoteExitNode) {
- await trx.insert(remoteExitNodes).values({
- remoteExitNodeId: remoteExitNodeId,
- secretHash,
- dateCreated: moment().toISOString(),
+ if (!existingRemoteExitNode) {
+ await trx.insert(remoteExitNodes).values({
+ remoteExitNodeId: remoteExitNodeId,
+ secretHash,
+ dateCreated: moment().toISOString(),
+ exitNodeId: existingExitNode.exitNodeId
+ });
+ } else {
+ // update the existing remote exit node
+ await trx
+ .update(remoteExitNodes)
+ .set({
exitNodeId: existingExitNode.exitNodeId
- });
- } else {
- // update the existing remote exit node
- await trx
- .update(remoteExitNodes)
- .set({
- exitNodeId: existingExitNode.exitNodeId
- })
- .where(
+ })
+ .where(
+ eq(
+ remoteExitNodes.remoteExitNodeId,
+ existingRemoteExitNode.remoteExitNodeId
+ )
+ );
+ }
+
+ if (!existingExitNodeOrg) {
+ await trx.insert(exitNodeOrgs).values({
+ exitNodeId: existingExitNode.exitNodeId,
+ orgId: orgId
+ });
+ }
+
+ // calculate if the node is in any other of the orgs before we count it as an add to the billing org
+ if (org.billingOrgId) {
+ const otherBillingOrgs = await trx
+ .select()
+ .from(orgs)
+ .where(
+ and(
+ eq(orgs.billingOrgId, org.billingOrgId),
+ ne(orgs.orgId, orgId)
+ )
+ );
+
+ const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
+
+ const orgsInBillingDomainThatTheNodeIsStillIn = await trx
+ .select()
+ .from(exitNodeOrgs)
+ .where(
+ and(
eq(
- remoteExitNodes.remoteExitNodeId,
- existingRemoteExitNode.remoteExitNodeId
- )
- );
+ exitNodeOrgs.exitNodeId,
+ existingExitNode.exitNodeId
+ ),
+ inArray(exitNodeOrgs.orgId, billingOrgIds)
+ )
+ );
+
+ if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
+ await usageService.add(
+ orgId,
+ FeatureId.REMOTE_EXIT_NODES,
+ 1,
+ trx
+ );
}
-
- if (!existingExitNodeOrg) {
- await trx.insert(exitNodeOrgs).values({
- exitNodeId: existingExitNode.exitNodeId,
- orgId: orgId
- });
- }
-
- // calculate if the node is in any other of the orgs before we count it as an add to the billing org
- if (org.billingOrgId) {
- const otherBillingOrgs = await trx
- .select()
- .from(orgs)
- .where(
- and(
- eq(orgs.billingOrgId, org.billingOrgId),
- ne(orgs.orgId, orgId)
- )
- );
-
- const billingOrgIds = otherBillingOrgs.map((o) => o.orgId);
-
- const orgsInBillingDomainThatTheNodeIsStillIn = await trx
- .select()
- .from(exitNodeOrgs)
- .where(
- and(
- eq(
- exitNodeOrgs.exitNodeId,
- existingExitNode.exitNodeId
- ),
- inArray(exitNodeOrgs.orgId, billingOrgIds)
- )
- );
-
- if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
- await usageService.add(
- orgId,
- LimitId.REMOTE_EXIT_NODES,
- 1,
- trx
- );
- }
- }
- });
- } finally {
- await releaseSubnetLock?.();
- }
+ }
+ });
const token = generateSessionToken();
await createRemoteExitNodeSession(token, remoteExitNodeId);
diff --git a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
index 6fd871896..e86476a5a 100644
--- a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
@@ -117,7 +117,7 @@ export async function deleteRemoteExitNode(
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add(
orgId,
- LimitId.REMOTE_EXIT_NODES,
+ FeatureId.REMOTE_EXIT_NODES,
-1,
trx
);
diff --git a/server/private/routers/remoteExitNode/index.ts b/server/private/routers/remoteExitNode/index.ts
index 244ef17de..953ccba88 100644
--- a/server/private/routers/remoteExitNode/index.ts
+++ b/server/private/routers/remoteExitNode/index.ts
@@ -23,7 +23,3 @@ export * from "./pickRemoteExitNodeDefaults";
export * from "./quickStartRemoteExitNode";
export * from "./offlineChecker";
export * from "./exitNodeReconnectScheduler";
-export * from "./listRemoteExitNodeResources";
-export * from "./setRemoteExitNodeResources";
-export * from "./listRemoteExitNodePreferenceLabels";
-export * from "./setRemoteExitNodePreferenceLabels";
diff --git a/server/private/routers/remoteExitNode/listRemoteExitNodePreferenceLabels.ts b/server/private/routers/remoteExitNode/listRemoteExitNodePreferenceLabels.ts
deleted file mode 100644
index a03e02257..000000000
--- a/server/private/routers/remoteExitNode/listRemoteExitNodePreferenceLabels.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { NextFunction, Request, Response } from "express";
-import { z } from "zod";
-import {
- db,
- labels,
- remoteExitNodePreferenceLabels,
- remoteExitNodes
-} 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";
-import { ListRemoteExitNodePreferenceLabelsResponse } from "@server/routers/remoteExitNode";
-
-const paramsSchema = z.strictObject({
- orgId: z.string().min(1),
- remoteExitNodeId: z.string().min(1)
-});
-
-export async function listRemoteExitNodePreferenceLabels(
- 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 { remoteExitNodeId } = parsedParams.data;
-
- const [remoteExitNode] = await db
- .select()
- .from(remoteExitNodes)
- .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
- .limit(1);
-
- if (!remoteExitNode) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- `Remote exit node with ID ${remoteExitNodeId} not found`
- )
- );
- }
-
- const rows = await db
- .select({
- remoteExitNodePreferenceLabelId:
- remoteExitNodePreferenceLabels.remoteExitNodePreferenceLabelId,
- labelId: remoteExitNodePreferenceLabels.labelId,
- name: labels.name,
- color: labels.color
- })
- .from(remoteExitNodePreferenceLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, remoteExitNodePreferenceLabels.labelId)
- )
- .where(
- eq(
- remoteExitNodePreferenceLabels.remoteExitNodeId,
- remoteExitNodeId
- )
- );
-
- return response(res, {
- data: { labels: rows },
- success: true,
- error: false,
- message:
- "Remote exit node preference labels 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/remoteExitNode/listRemoteExitNodeResources.ts b/server/private/routers/remoteExitNode/listRemoteExitNodeResources.ts
deleted file mode 100644
index a51b4bed6..000000000
--- a/server/private/routers/remoteExitNode/listRemoteExitNodeResources.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { NextFunction, Request, Response } from "express";
-import { z } from "zod";
-import { db, remoteExitNodeResources, remoteExitNodes } 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";
-import { ListRemoteExitNodeResourcesResponse } from "@server/routers/remoteExitNode/types";
-
-const paramsSchema = z.strictObject({
- orgId: z.string().min(1),
- remoteExitNodeId: z.string().min(1)
-});
-
-export async function listRemoteExitNodeResources(
- 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 { remoteExitNodeId } = parsedParams.data;
-
- const [remoteExitNode] = await db
- .select()
- .from(remoteExitNodes)
- .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
- .limit(1);
-
- if (!remoteExitNode) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- `Remote exit node with ID ${remoteExitNodeId} not found`
- )
- );
- }
-
- const resources = await db
- .select()
- .from(remoteExitNodeResources)
- .where(
- eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
- );
-
- return response(res, {
- data: { resources },
- success: true,
- error: false,
- message: "Remote exit node resources 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/remoteExitNode/listRemoteExitNodes.ts b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts
index 872e62b2d..061be1792 100644
--- a/server/private/routers/remoteExitNode/listRemoteExitNodes.ts
+++ b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
-import { regionalCache as cache } from "#private/lib/cache";
+import cache from "#private/lib/cache";
import semver from "semver";
let stalePangolinNodeVersion: string | null = null;
diff --git a/server/private/routers/remoteExitNode/setRemoteExitNodePreferenceLabels.ts b/server/private/routers/remoteExitNode/setRemoteExitNodePreferenceLabels.ts
deleted file mode 100644
index 4c300d589..000000000
--- a/server/private/routers/remoteExitNode/setRemoteExitNodePreferenceLabels.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { NextFunction, Request, Response } from "express";
-import { z } from "zod";
-import {
- db,
- labels,
- remoteExitNodePreferenceLabels,
- remoteExitNodes
-} from "@server/db";
-import { and, eq, inArray } 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";
-import { SetRemoteExitNodePreferenceLabelsResponse } from "@server/routers/remoteExitNode";
-
-const paramsSchema = z.strictObject({
- orgId: z.string().min(1),
- remoteExitNodeId: z.string().min(1)
-});
-
-const bodySchema = z.strictObject({
- labelIds: z.array(z.number().int().positive())
-});
-
-export type SetRemoteExitNodePreferenceLabelsBody = z.infer;
-
-export async function setRemoteExitNodePreferenceLabels(
- 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, remoteExitNodeId } = parsedParams.data;
-
- const parsedBody = bodySchema.safeParse(req.body);
- if (!parsedBody.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromError(parsedBody.error).toString()
- )
- );
- }
-
- const { labelIds } = parsedBody.data;
-
- const [remoteExitNode] = await db
- .select()
- .from(remoteExitNodes)
- .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
- .limit(1);
-
- if (!remoteExitNode) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- `Remote exit node with ID ${remoteExitNodeId} not found`
- )
- );
- }
-
- // Validate all provided labelIds belong to this org
- if (labelIds.length > 0) {
- const existingLabels = await db
- .select({ labelId: labels.labelId })
- .from(labels)
- .where(
- and(
- eq(labels.orgId, orgId),
- inArray(labels.labelId, labelIds)
- )
- );
-
- if (existingLabels.length !== labelIds.length) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "One or more label IDs are invalid or do not belong to this organization"
- )
- );
- }
- }
-
- // Replace all preference labels atomically
- await db
- .delete(remoteExitNodePreferenceLabels)
- .where(
- eq(
- remoteExitNodePreferenceLabels.remoteExitNodeId,
- remoteExitNodeId
- )
- );
-
- if (labelIds.length > 0) {
- await db.insert(remoteExitNodePreferenceLabels).values(
- labelIds.map((labelId) => ({
- remoteExitNodeId,
- labelId
- }))
- );
- }
-
- const rows = await db
- .select({
- remoteExitNodePreferenceLabelId:
- remoteExitNodePreferenceLabels.remoteExitNodePreferenceLabelId,
- labelId: remoteExitNodePreferenceLabels.labelId,
- name: labels.name,
- color: labels.color
- })
- .from(remoteExitNodePreferenceLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, remoteExitNodePreferenceLabels.labelId)
- )
- .where(
- eq(
- remoteExitNodePreferenceLabels.remoteExitNodeId,
- remoteExitNodeId
- )
- );
-
- return response(res, {
- data: { labels: rows },
- success: true,
- error: false,
- message: "Remote exit node preference labels 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/remoteExitNode/setRemoteExitNodeResources.ts b/server/private/routers/remoteExitNode/setRemoteExitNodeResources.ts
deleted file mode 100644
index a5f9c1478..000000000
--- a/server/private/routers/remoteExitNode/setRemoteExitNodeResources.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 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 { NextFunction, Request, Response } from "express";
-import { z } from "zod";
-import {
- db,
- newts,
- remoteExitNodeResources,
- remoteExitNodes,
- sites
-} 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";
-import { sendToClientsBatch } from "#private/routers/ws";
-import { canCompress } from "@server/lib/clientVersionChecks";
-import { SetRemoteExitNodeResourcesResponse } from "@server/routers/remoteExitNode";
-
-const paramsSchema = z.strictObject({
- orgId: z.string().min(1),
- remoteExitNodeId: z.string().min(1)
-});
-
-const cidrRegex =
- /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$|^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/;
-
-const bodySchema = z.strictObject({
- destinations: z.array(
- z.string().regex(cidrRegex, "Must be a valid CIDR range")
- )
-});
-
-export type SetRemoteExitNodeResourcesBody = z.infer;
-
-export async function setRemoteExitNodeResources(
- 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 { remoteExitNodeId } = parsedParams.data;
-
- const parsedBody = bodySchema.safeParse(req.body);
- if (!parsedBody.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromError(parsedBody.error).toString()
- )
- );
- }
-
- const { destinations } = parsedBody.data;
-
- const [remoteExitNode] = await db
- .select()
- .from(remoteExitNodes)
- .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
- .limit(1);
-
- if (!remoteExitNode) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- `Remote exit node with ID ${remoteExitNodeId} not found`
- )
- );
- }
-
- // Replace all resources atomically
- await db
- .delete(remoteExitNodeResources)
- .where(
- eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
- );
-
- if (destinations.length > 0) {
- await db.insert(remoteExitNodeResources).values(
- destinations.map((destination) => ({
- remoteExitNodeId,
- destination
- }))
- );
- }
-
- const resources = await db
- .select()
- .from(remoteExitNodeResources)
- .where(
- eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
- );
-
- // Notify all newts connected to this remote exit node's exit node
- if (remoteExitNode.exitNodeId) {
- const connectedNewts = await db
- .select({ newtId: newts.newtId, version: newts.version })
- .from(newts)
- .innerJoin(sites, eq(newts.siteId, sites.siteId))
- .where(eq(sites.exitNodeId, remoteExitNode.exitNodeId));
-
- await sendToClientsBatch(
- connectedNewts.map(({ newtId, version }) => ({
- clientId: newtId,
- message: {
- type: "newt/wg/subnets/update",
- data: { subnets: destinations }
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(version, "newt")
- }
- }))
- );
- }
-
- return response(res, {
- data: { resources },
- success: true,
- error: false,
- message: "Remote exit node resources 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/user/addUserRole.ts b/server/private/routers/user/addUserRole.ts
index 2065984a3..c59a3d0f7 100644
--- a/server/private/routers/user/addUserRole.ts
+++ b/server/private/routers/user/addUserRole.ts
@@ -23,10 +23,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
-import {
- rebuildClientAssociationsFromClient,
- isOrgRebuildRateLimited
-} from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const addUserRoleParamsSchema = z.strictObject({
userId: z.string(),
@@ -131,15 +128,6 @@ export async function addUserRole(
);
}
- if (await isOrgRebuildRateLimited(role.orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
let newUserRole: {
userId: string;
orgId: string;
@@ -175,11 +163,13 @@ export async function addUserRole(
});
for (const orgClient of orgClientsToRebuild) {
- rebuildClientAssociationsFromClient(orgClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
+ );
+ }
+ );
}
return response(res, {
diff --git a/server/private/routers/user/removeUserRole.ts b/server/private/routers/user/removeUserRole.ts
index 79a5a522a..b96670815 100644
--- a/server/private/routers/user/removeUserRole.ts
+++ b/server/private/routers/user/removeUserRole.ts
@@ -170,11 +170,13 @@ export async function removeUserRole(
});
for (const orgClient of orgClientsToRebuild) {
- rebuildClientAssociationsFromClient(orgClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
+ );
+ }
+ );
}
return response(res, {
diff --git a/server/private/routers/user/setUserOrgRoles.ts b/server/private/routers/user/setUserOrgRoles.ts
index b583233d1..7790eacfb 100644
--- a/server/private/routers/user/setUserOrgRoles.ts
+++ b/server/private/routers/user/setUserOrgRoles.ts
@@ -21,10 +21,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
-import {
- rebuildClientAssociationsFromClient,
- isOrgRebuildRateLimited
-} from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const setUserOrgRolesParamsSchema = z.strictObject({
orgId: z.string(),
@@ -90,15 +87,6 @@ export async function setUserOrgRoles(
);
}
- if (await isOrgRebuildRateLimited(orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
const orgRoles = await db
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
.from(roles)
@@ -162,11 +150,13 @@ export async function setUserOrgRoles(
});
for (const orgClient of orgClientsToRebuild) {
- rebuildClientAssociationsFromClient(orgClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
+ );
+ }
+ );
}
return response(res, {
diff --git a/server/private/routers/ws/ws.ts b/server/private/routers/ws/ws.ts
index f014b0e57..a592927cc 100644
--- a/server/private/routers/ws/ws.ts
+++ b/server/private/routers/ws/ws.ts
@@ -26,7 +26,7 @@ import {
} from "@server/db";
import { eq } from "drizzle-orm";
import { db } from "@server/db";
-import { recordSitePing } from "@server/routers/newt/pingAccumulator";
+import { recordPing } from "@server/routers/newt/pingAccumulator";
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
import logger from "@server/logger";
@@ -38,7 +38,6 @@ import { messageHandlers } from "@server/routers/ws/messageHandlers";
import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers";
import {
AuthenticatedWebSocket,
- BatchSendMessage,
ClientType,
WSMessage,
TokenPayload,
@@ -188,8 +187,6 @@ const wss: WebSocketServer = new WebSocketServer({ noServer: true });
// Generate unique node ID for this instance
const NODE_ID = uuidv4();
const REDIS_CHANNEL = "websocket_messages";
-const REDIS_DIRECT_BATCH_SIZE = 250;
-const REDIS_DIRECT_FLUSH_INTERVAL_MS = 10;
// Client tracking map (local to this node)
const connectedClients: Map = new Map();
@@ -200,15 +197,6 @@ const clientConfigVersions: Map = new Map();
// Recovery tracking
let isRedisRecoveryInProgress = false;
-interface RedisDirectBatchEntry {
- targetClientId: string;
- message: WSMessage;
- resolve: () => void;
-}
-
-let pendingRedisDirectMessages: RedisDirectBatchEntry[] = [];
-let redisDirectFlushTimer: NodeJS.Timeout | null = null;
-
// Helper to get map key
const getClientMapKey = (clientId: string) => clientId;
@@ -219,78 +207,6 @@ const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
const getConfigVersionKey = (clientId: string) =>
`ws:configVersion:${clientId}`;
-const clearRedisDirectFlushTimer = (): void => {
- if (redisDirectFlushTimer) {
- clearTimeout(redisDirectFlushTimer);
- redisDirectFlushTimer = null;
- }
-};
-
-const publishDirectBatch = async (
- entries: RedisDirectBatchEntry[]
-): Promise => {
- const redisMessage: RedisMessage = {
- type: "direct-batch",
- messages: entries.map((entry) => ({
- targetClientId: entry.targetClientId,
- message: entry.message
- })),
- fromNodeId: NODE_ID
- };
-
- await redisManager.publish(REDIS_CHANNEL, JSON.stringify(redisMessage));
-};
-
-const flushPendingRedisDirectMessages = async (): Promise => {
- clearRedisDirectFlushTimer();
-
- if (pendingRedisDirectMessages.length === 0) {
- return;
- }
-
- const entries = pendingRedisDirectMessages;
- pendingRedisDirectMessages = [];
-
- if (!redisManager.isRedisEnabled()) {
- entries.forEach((entry) => entry.resolve());
- return;
- }
-
- for (let i = 0; i < entries.length; i += REDIS_DIRECT_BATCH_SIZE) {
- const batch = entries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
- try {
- await publishDirectBatch(batch);
- } catch (error) {
- logger.error(
- "Failed to send batched direct messages via Redis, messages may be lost:",
- error
- );
- } finally {
- batch.forEach((entry) => entry.resolve());
- }
- }
-};
-
-const enqueueRedisDirectMessage = async (
- targetClientId: string,
- message: WSMessage
-): Promise => {
- await new Promise((resolve) => {
- pendingRedisDirectMessages.push({ targetClientId, message, resolve });
-
- if (pendingRedisDirectMessages.length >= REDIS_DIRECT_BATCH_SIZE) {
- void flushPendingRedisDirectMessages();
- return;
- }
-
- if (!redisDirectFlushTimer) {
- redisDirectFlushTimer = setTimeout(() => {
- void flushPendingRedisDirectMessages();
- }, REDIS_DIRECT_FLUSH_INTERVAL_MS);
- }
- });
-};
-
// Initialize Redis subscription for cross-node messaging
const initializeRedisSubscription = async (): Promise => {
if (!redisManager.isRedisEnabled()) return;
@@ -311,16 +227,7 @@ const initializeRedisSubscription = async (): Promise => {
// Send to specific client on this node
await sendToClientLocal(
redisMessage.targetClientId,
- redisMessage.message,
- {},
- redisMessage.message.configVersion
- );
- } else if (
- redisMessage.type === "direct-batch" &&
- redisMessage.messages
- ) {
- await sendRedisDirectBatchToLocalClients(
- redisMessage.messages
+ redisMessage.message
);
} else if (redisMessage.type === "broadcast") {
// Broadcast to all clients on this node except excluded
@@ -596,8 +503,7 @@ const incrementClientConfigVersion = async (
const sendToClientLocal = async (
clientId: string,
message: WSMessage,
- options: SendMessageOptions = {},
- preResolvedConfigVersion?: number
+ options: SendMessageOptions = {}
): Promise => {
const mapKey = getClientMapKey(clientId);
const clients = connectedClients.get(mapKey);
@@ -606,8 +512,7 @@ const sendToClientLocal = async (
}
// Handle config version
- const configVersion =
- preResolvedConfigVersion ?? (await getClientConfigVersion(clientId));
+ const configVersion = await getClientConfigVersion(clientId);
// Add config version to message
const messageWithVersion = {
@@ -640,71 +545,43 @@ const sendToClientLocal = async (
return true;
};
-const sendRedisDirectBatchToLocalClients = async (
- entries: { targetClientId: string; message: WSMessage }[]
-): Promise => {
- const jobs = entries.map((entry) =>
- sendToClientLocal(
- entry.targetClientId,
- entry.message,
- {},
- entry.message.configVersion
- )
- );
- await Promise.all(jobs);
-};
-
const broadcastToAllExceptLocal = async (
message: WSMessage,
excludeClientId?: string,
options: SendMessageOptions = {}
): Promise => {
- const sendPlans = await Promise.all(
- Array.from(connectedClients.entries()).map(
- async ([mapKey, clients]) => {
- const clientId = mapKey; // mapKey is the clientId
- if (excludeClientId && clientId === excludeClientId) {
- return null;
- }
-
- let configVersion = await getClientConfigVersion(clientId);
- if (options.incrementConfigVersion) {
- configVersion =
- await incrementClientConfigVersion(clientId);
- }
-
- return {
- clients,
- messageWithVersion: {
- ...message,
- configVersion
- }
- };
+ for (const [mapKey, clients] of connectedClients.entries()) {
+ const [type, id] = mapKey.split(":");
+ const clientId = mapKey; // mapKey is the clientId
+ if (!(excludeClientId && clientId === excludeClientId)) {
+ // Handle config version per client
+ let configVersion = await getClientConfigVersion(clientId);
+ if (options.incrementConfigVersion) {
+ configVersion = await incrementClientConfigVersion(clientId);
}
- )
- );
- for (const plan of sendPlans) {
- if (!plan) {
- continue;
- }
+ // Add config version to message
+ const messageWithVersion = {
+ ...message,
+ configVersion
+ };
- if (options.compress) {
- const compressed = zlib.gzipSync(
- Buffer.from(JSON.stringify(plan.messageWithVersion), "utf8")
- );
- plan.clients.forEach((client) => {
- if (client.readyState === WebSocket.OPEN) {
- client.send(compressed);
- }
- });
- } else {
- const messageString = JSON.stringify(plan.messageWithVersion);
- plan.clients.forEach((client) => {
- if (client.readyState === WebSocket.OPEN) {
- client.send(messageString);
- }
- });
+ if (options.compress) {
+ const compressed = zlib.gzipSync(
+ Buffer.from(JSON.stringify(messageWithVersion), "utf8")
+ );
+ clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(compressed);
+ }
+ });
+ } else {
+ clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(JSON.stringify(messageWithVersion));
+ }
+ });
+ }
}
}
};
@@ -725,23 +602,28 @@ const sendToClient = async (
);
// Try to send locally first
- const localSent = await sendToClientLocal(
- clientId,
- message,
- options,
- configVersion
- );
+ const localSent = await sendToClientLocal(clientId, message, options);
// Only send via Redis if the client is not connected locally and Redis is enabled
if (!localSent && redisManager.isRedisEnabled()) {
try {
- await enqueueRedisDirectMessage(clientId, {
- ...message,
- configVersion
- });
+ const redisMessage: RedisMessage = {
+ type: "direct",
+ targetClientId: clientId,
+ message: {
+ ...message,
+ configVersion
+ },
+ fromNodeId: NODE_ID
+ };
+
+ await redisManager.publish(
+ REDIS_CHANNEL,
+ JSON.stringify(redisMessage)
+ );
} catch (error) {
logger.error(
- "Failed to queue batched direct message for Redis delivery, message may be lost:",
+ "Failed to send message via Redis, message may be lost:",
error
);
// Continue execution - local delivery already attempted
@@ -756,95 +638,6 @@ const sendToClient = async (
return localSent;
};
-const sendToClientsBatch = async (
- entries: BatchSendMessage[]
-): Promise => {
- if (entries.length === 0) {
- return;
- }
-
- const remoteEntries: { targetClientId: string; message: WSMessage }[] = [];
- const clientsWithIncrement = new Set(
- entries
- .filter((entry) => !!entry.options?.incrementConfigVersion)
- .map((entry) => entry.clientId)
- );
- const nonIncrementOnlyClientIds = Array.from(
- new Set(
- entries
- .map((entry) => entry.clientId)
- .filter((clientId) => !clientsWithIncrement.has(clientId))
- )
- );
- const stableConfigVersionByClient = new Map(
- await Promise.all(
- nonIncrementOnlyClientIds.map(
- async (clientId) =>
- [clientId, await getClientConfigVersion(clientId)] as const
- )
- )
- );
-
- for (const entry of entries) {
- const options = entry.options || {};
- const { clientId, message } = entry;
-
- const configVersion = options.incrementConfigVersion
- ? await incrementClientConfigVersion(clientId)
- : stableConfigVersionByClient.get(clientId);
-
- logger.debug(
- `sendToClientsBatch: Message type ${message.type} queued for clientId ${clientId} (new configVersion: ${configVersion})`
- );
-
- const localSent = await sendToClientLocal(
- clientId,
- message,
- options,
- configVersion
- );
-
- if (!localSent && redisManager.isRedisEnabled()) {
- remoteEntries.push({
- targetClientId: clientId,
- message: {
- ...message,
- configVersion
- }
- });
- } else if (!localSent && !redisManager.isRedisEnabled()) {
- logger.debug(
- `Could not deliver batch message to ${clientId} - not connected locally and Redis unavailable`
- );
- }
- }
-
- if (!redisManager.isRedisEnabled() || remoteEntries.length === 0) {
- return;
- }
-
- for (let i = 0; i < remoteEntries.length; i += REDIS_DIRECT_BATCH_SIZE) {
- const messages = remoteEntries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
- try {
- const redisMessage: RedisMessage = {
- type: "direct-batch",
- messages,
- fromNodeId: NODE_ID
- };
-
- await redisManager.publish(
- REDIS_CHANNEL,
- JSON.stringify(redisMessage)
- );
- } catch (error) {
- logger.error(
- "Failed to send explicit direct batch via Redis, messages may be lost:",
- error
- );
- }
- }
-};
-
const broadcastToAllExcept = async (
message: WSMessage,
excludeClientId?: string,
@@ -1063,7 +856,7 @@ const setupConnection = async (
// pending pings in a single batched UPDATE every ~10s, which
// prevents connection pool exhaustion under load (especially
// with cross-region latency to the database).
- recordSitePing(newtClient.siteId);
+ recordPing(newtClient.siteId);
});
}
@@ -1316,8 +1109,6 @@ const disconnectClient = async (clientId: string): Promise => {
// Cleanup function for graceful shutdown
const cleanup = async (): Promise => {
try {
- await flushPendingRedisDirectMessages();
-
// Close all WebSocket connections
connectedClients.forEach((clients) => {
clients.forEach((client) => {
@@ -1348,7 +1139,6 @@ export {
router,
handleWSUpgrade,
sendToClient,
- sendToClientsBatch,
broadcastToAllExcept,
connectedClients,
hasActiveConnections,
diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts
index 472d9da40..0339cc2c4 100644
--- a/server/routers/accessToken/listAccessTokens.ts
+++ b/server/routers/accessToken/listAccessTokens.ts
@@ -30,7 +30,7 @@ const listAccessTokensParamsSchema = z
error: "Either resourceId or orgId must be provided, but not both"
});
-const listAccessTokensSchema = z.strictObject({
+const listAccessTokensSchema = z.object({
limit: z
.string()
.optional()
diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts
index 3b5efa8b5..364f3aee2 100644
--- a/server/routers/apiKeys/listApiKeyActions.ts
+++ b/server/routers/apiKeys/listApiKeyActions.ts
@@ -15,7 +15,7 @@ const paramsSchema = z.object({
apiKeyId: z.string().nonempty()
});
-const querySchema = z.strictObject({
+const querySchema = z.object({
limit: z
.string()
.optional()
diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts
index 68a7f9a25..ba87a3033 100644
--- a/server/routers/apiKeys/listOrgApiKeys.ts
+++ b/server/routers/apiKeys/listOrgApiKeys.ts
@@ -11,7 +11,7 @@ import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
-const querySchema = z.strictObject({
+const querySchema = z.object({
limit: z
.string()
.optional()
diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts
index 434ff5a8b..654b830a6 100644
--- a/server/routers/apiKeys/listRootApiKeys.ts
+++ b/server/routers/apiKeys/listRootApiKeys.ts
@@ -9,7 +9,7 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
-const querySchema = z.strictObject({
+const querySchema = z.object({
limit: z
.string()
.optional()
diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts
index 7f4a0ec16..f14c28cf1 100644
--- a/server/routers/auditLogs/queryRequestAuditLog.ts
+++ b/server/routers/auditLogs/queryRequestAuditLog.ts
@@ -20,7 +20,7 @@ import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
-export const queryAccessAuditLogsQuery = z.strictObject({
+export const queryAccessAuditLogsQuery = z.object({
// iso string just validate its a parseable date
timeStart: z
.string()
diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts
index 15ca1e87e..b8168ef1e 100644
--- a/server/routers/auditLogs/types.ts
+++ b/server/routers/auditLogs/types.ts
@@ -68,7 +68,6 @@ export type QueryAccessAuditLogResponse = {
actorType: string | null;
actorId: string | null;
resourceId: number | null;
- siteResourceId: number | null;
resourceName: string | null;
resourceNiceId: string | null;
ip: string | null;
diff --git a/server/routers/auth/changePassword.ts b/server/routers/auth/changePassword.ts
index 256396763..1a26b9117 100644
--- a/server/routers/auth/changePassword.ts
+++ b/server/routers/auth/changePassword.ts
@@ -10,8 +10,9 @@ import { hashPassword, verifyPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/totp";
import logger from "@server/logger";
import { unauthorized } from "@server/auth/unauthorizedResponse";
-import { invalidateAllSessionsExceptCurrent } from "@server/auth/sessions/app";
-import { eq } from "drizzle-orm";
+import { invalidateAllSessions } from "@server/auth/sessions/app";
+import { sessions, resourceSessions } from "@server/db";
+import { and, eq, ne, inArray } from "drizzle-orm";
import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes";
import { sendEmail } from "@server/emails";
@@ -30,6 +31,48 @@ export type ChangePasswordResponse = {
codeRequested?: boolean;
};
+async function invalidateAllSessionsExceptCurrent(
+ userId: string,
+ currentSessionId: string
+): Promise {
+ try {
+ await db.transaction(async (trx) => {
+ // Get all user sessions except the current one
+ const userSessions = await trx
+ .select()
+ .from(sessions)
+ .where(
+ and(
+ eq(sessions.userId, userId),
+ ne(sessions.sessionId, currentSessionId)
+ )
+ );
+
+ // Delete resource sessions for the sessions we're invalidating
+ if (userSessions.length > 0) {
+ await trx.delete(resourceSessions).where(
+ inArray(
+ resourceSessions.userSessionId,
+ userSessions.map((s) => s.sessionId)
+ )
+ );
+ }
+
+ // Delete the user sessions (except current)
+ await trx
+ .delete(sessions)
+ .where(
+ and(
+ eq(sessions.userId, userId),
+ ne(sessions.sessionId, currentSessionId)
+ )
+ );
+ });
+ } catch (e) {
+ logger.error("Failed to invalidate user sessions except current", e);
+ }
+}
+
export async function changePassword(
req: Request,
res: Response,
diff --git a/server/routers/auth/deleteMyAccount.ts b/server/routers/auth/deleteMyAccount.ts
index 0a1f74f3e..d03af5631 100644
--- a/server/routers/auth/deleteMyAccount.ts
+++ b/server/routers/auth/deleteMyAccount.ts
@@ -20,7 +20,7 @@ import { getOrgTierData } from "#dynamic/lib/billing";
import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg";
import { UserType } from "@server/types/UserTypes";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
const deleteMyAccountBody = z.strictObject({
password: z.string().optional(),
@@ -220,11 +220,11 @@ export async function deleteMyAccount(
await trx.delete(users).where(eq(users.userId, userId));
// loop through the other orgs and decrement the count
for (const userOrg of otherOrgsTheUserWasIn) {
- await usageService.add(userOrg.orgId, LimitId.USERS, -1, trx);
+ await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
}
});
- calculateUserClientsForOrgs(userId).catch((e) => {
+ calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
logger.error(
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
);
diff --git a/server/routers/auth/verifyTotp.ts b/server/routers/auth/verifyTotp.ts
index 5fc6cf13c..207287ea0 100644
--- a/server/routers/auth/verifyTotp.ts
+++ b/server/routers/auth/verifyTotp.ts
@@ -15,10 +15,6 @@ import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNot
import config from "@server/lib/config";
import { UserType } from "@server/types/UserTypes";
import { generateBackupCodes } from "@server/lib/totp";
-import {
- invalidateAllSessions,
- invalidateAllSessionsExceptCurrent
-} from "@server/auth/sessions/app";
import { verifySession } from "@server/auth/sessions/verifySession";
import { unauthorized } from "@server/auth/unauthorizedResponse";
@@ -172,15 +168,6 @@ export async function verifyTotp(
);
}
- if (existingSession) {
- await invalidateAllSessionsExceptCurrent(
- user.userId,
- existingSession.sessionId
- );
- } else {
- await invalidateAllSessions(user.userId);
- }
-
sendEmail(
TwoFactorAuthNotification({
email: user.email!,
diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts
index 681717f9c..8333a4578 100644
--- a/server/routers/badger/verifySession.test.ts
+++ b/server/routers/badger/verifySession.test.ts
@@ -1,6 +1,5 @@
import { assertEquals } from "@test/assert";
import { REGIONS } from "@server/db/regions";
-import { isPathAllowed } from "@server/lib/pathMatch";
function isIpInRegion(
ipCountryCode: string | undefined,
@@ -34,6 +33,76 @@ function isIpInRegion(
return false;
}
+function isPathAllowed(pattern: string, path: string): boolean {
+ // Normalize and split paths into segments
+ const normalize = (p: string) => p.split("/").filter(Boolean);
+ const patternParts = normalize(pattern);
+ const pathParts = normalize(path);
+
+ // Recursive function to try different wildcard matches
+ function matchSegments(patternIndex: number, pathIndex: number): boolean {
+ const indent = " ".repeat(pathIndex); // Indent based on recursion depth
+ const currentPatternPart = patternParts[patternIndex];
+ const currentPathPart = pathParts[pathIndex];
+
+ // If we've consumed all pattern parts, we should have consumed all path parts
+ if (patternIndex >= patternParts.length) {
+ const result = pathIndex >= pathParts.length;
+ return result;
+ }
+
+ // If we've consumed all path parts but still have pattern parts
+ if (pathIndex >= pathParts.length) {
+ // The only way this can match is if all remaining pattern parts are wildcards
+ const remainingPattern = patternParts.slice(patternIndex);
+ const result = remainingPattern.every((p) => p === "*");
+ return result;
+ }
+
+ // For full segment wildcards, try consuming different numbers of path segments
+ if (currentPatternPart === "*") {
+ // Try consuming 0 segments (skip the wildcard)
+ if (matchSegments(patternIndex + 1, pathIndex)) {
+ return true;
+ }
+
+ // Try consuming current segment and recursively try rest
+ if (matchSegments(patternIndex, pathIndex + 1)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
+ if (currentPatternPart.includes("*")) {
+ // Convert the pattern segment to a regex pattern
+ const regexPattern = currentPatternPart
+ .replace(/\*/g, ".*") // Replace * with .* for regex wildcard
+ .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
+
+ const regex = new RegExp(`^${regexPattern}$`);
+
+ if (regex.test(currentPathPart)) {
+ return matchSegments(patternIndex + 1, pathIndex + 1);
+ }
+
+ return false;
+ }
+
+ // For regular segments, they must match exactly
+ if (currentPatternPart !== currentPathPart) {
+ return false;
+ }
+
+ // Move to next segments in both pattern and path
+ return matchSegments(patternIndex + 1, pathIndex + 1);
+ }
+
+ const result = matchSegments(0, 0);
+ return result;
+}
+
function runTests() {
console.log("Running path matching tests...");
@@ -239,121 +308,6 @@ function runTests() {
console.log("All path matching tests passed!");
}
-function runSpecialCharacterTests() {
- console.log("\nRunning special character tests...");
-
- let threw = false;
- try {
- isPathAllowed("(api*", "anything");
- isPathAllowed("a(b*", "a(bc");
- isPathAllowed("c[d*", "c[de");
- isPathAllowed("x{2}*", "x{2}y");
- isPathAllowed("a|b*", "a|bc");
- isPathAllowed("back\\slash*", "back\\slashed");
- } catch (e) {
- threw = true;
- console.error(
- "Patterns accepted by isValidUrlGlobPattern crashed the matcher:",
- e instanceof Error ? e.message : e
- );
- }
- assertEquals(
- threw,
- false,
- "Patterns with regex metacharacters must not throw"
- );
-
- assertEquals(
- isPathAllowed("(api*", "(api-v1"),
- true,
- "Parenthesis should be treated as a literal character"
- );
- assertEquals(
- isPathAllowed("(api*", "xapi-v1"),
- false,
- "Parenthesis should not match other characters"
- );
- assertEquals(
- isPathAllowed("a(b)*", "a(b)c"),
- true,
- "Parentheses pair should be treated as literal characters"
- );
-
- assertEquals(
- isPathAllowed("*.png", "image.png"),
- true,
- "Dot should match a literal dot"
- );
- assertEquals(
- isPathAllowed("*.png", "imageXpng"),
- false,
- "Dot should not act as a regex wildcard"
- );
- assertEquals(
- isPathAllowed("v1.0*", "v1.0.1"),
- true,
- "Version-like literal should match itself"
- );
- assertEquals(
- isPathAllowed("v1.0*", "v1x0-beta"),
- false,
- "Version-like literal should not match arbitrary characters"
- );
-
- assertEquals(
- isPathAllowed("a+b*", "a+bc"),
- true,
- "Plus should be treated as a literal character"
- );
- assertEquals(
- isPathAllowed("a+b*", "aaabc"),
- false,
- "Plus should not act as a regex quantifier"
- );
-
- assertEquals(
- isPathAllowed("$ref*", "$refs"),
- true,
- "Dollar sign should be treated as a literal character"
- );
- assertEquals(
- isPathAllowed("price$*", "price$100"),
- true,
- "Dollar sign mid-pattern should be treated as a literal character"
- );
-
- assertEquals(
- isPathAllowed("^start*", "^started"),
- true,
- "Caret should be treated as a literal character"
- );
-
- assertEquals(
- isPathAllowed("a|b*", "a|bc"),
- true,
- "Pipe should be treated as a literal character"
- );
- assertEquals(
- isPathAllowed("a|b*", "a"),
- false,
- "Pipe should not act as regex alternation"
- );
-
- assertEquals(
- isPathAllowed("file?*", "fileX"),
- true,
- "Question mark should still act as a single-character wildcard"
- );
-
- assertEquals(
- isPathAllowed("api/*", "api/" + "x/".repeat(50)),
- true,
- "Deeply nested paths should still match"
- );
-
- console.log("All special character tests passed!");
-}
-
function runRegionTests() {
console.log("\nRunning isIpInRegion tests...");
@@ -413,7 +367,6 @@ function runRegionTests() {
// Run all tests
try {
runTests();
- runSpecialCharacterTests();
runRegionTests();
console.log("\n✅ All tests passed!");
} catch (error) {
diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts
index cf36fef72..677fa281d 100644
--- a/server/routers/badger/verifySession.ts
+++ b/server/routers/badger/verifySession.ts
@@ -25,7 +25,6 @@ import {
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
-import { isPathAllowed } from "@server/lib/pathMatch";
import { response } from "@server/lib/response";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
@@ -736,14 +735,11 @@ export async function verifyResourceSession(
}
}
- // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge.
- // Skip the challenge when SSO is also enabled so browsers get the SSO redirect instead of a native Basic
- // Auth dialog; clients that proactively send Authorization: Basic are still accepted above.
+ // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
if (
headerAuthExtendedCompatibility &&
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
- !clientHeaderAuth &&
- !sso
+ !clientHeaderAuth
) {
return headerAuthChallenged(res, redirectPath, resource.orgId);
}
@@ -1057,10 +1053,7 @@ async function checkRules(
isPathAllowed(rule.value, path)
) {
return rule.action as any;
- } else if (
- clientIp &&
- (rule.match === "COUNTRY" || rule.match === "COUNTRY_IS_NOT")
- ) {
+ } else if (clientIp && rule.match == "COUNTRY") {
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
if (
rule.value.toUpperCase() === "ALL" &&
@@ -1069,10 +1062,7 @@ async function checkRules(
continue;
}
- const inCountry = await isIpInGeoIP(ipCC, rule.value);
- const matched = rule.match === "COUNTRY" ? inCountry : !inCountry;
-
- if (matched) {
+ if (await isIpInGeoIP(ipCC, rule.value)) {
return rule.action as any;
}
} else if (clientIp && rule.match == "ASN") {
@@ -1100,7 +1090,143 @@ async function checkRules(
return;
}
-export { isPathAllowed };
+export function isPathAllowed(pattern: string, path: string): boolean {
+ logger.debug(`\nMatching path "${path}" against pattern "${pattern}"`);
+
+ // Normalize and split paths into segments
+ const normalize = (p: string) => p.split("/").filter(Boolean);
+ const patternParts = normalize(pattern);
+ const pathParts = normalize(path);
+
+ logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
+ logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
+
+ // Maximum recursion depth to prevent stack overflow and memory issues
+ const MAX_RECURSION_DEPTH = 100;
+
+ // Recursive function to try different wildcard matches
+ function matchSegments(
+ patternIndex: number,
+ pathIndex: number,
+ depth: number = 0
+ ): boolean {
+ // Check recursion depth limit
+ if (depth > MAX_RECURSION_DEPTH) {
+ logger.warn(
+ `Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"`
+ );
+ return false;
+ }
+
+ const indent = " ".repeat(depth); // Indent based on recursion depth
+ const currentPatternPart = patternParts[patternIndex];
+ const currentPathPart = pathParts[pathIndex];
+
+ logger.debug(
+ `${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]`
+ );
+
+ // If we've consumed all pattern parts, we should have consumed all path parts
+ if (patternIndex >= patternParts.length) {
+ const result = pathIndex >= pathParts.length;
+ logger.debug(
+ `${indent}Reached end of pattern, remaining path: ${pathParts.slice(pathIndex).join("/")} -> ${result}`
+ );
+ return result;
+ }
+
+ // If we've consumed all path parts but still have pattern parts
+ if (pathIndex >= pathParts.length) {
+ // The only way this can match is if all remaining pattern parts are wildcards
+ const remainingPattern = patternParts.slice(patternIndex);
+ const result = remainingPattern.every((p) => p === "*");
+ logger.debug(
+ `${indent}Reached end of path, remaining pattern: ${remainingPattern.join("/")} -> ${result}`
+ );
+ return result;
+ }
+
+ // For full segment wildcards, try consuming different numbers of path segments
+ if (currentPatternPart === "*") {
+ logger.debug(
+ `${indent}Found wildcard at pattern index ${patternIndex}`
+ );
+
+ // Try consuming 0 segments (skip the wildcard)
+ logger.debug(
+ `${indent}Trying to skip wildcard (consume 0 segments)`
+ );
+ if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
+ logger.debug(
+ `${indent}Successfully matched by skipping wildcard`
+ );
+ return true;
+ }
+
+ // Try consuming current segment and recursively try rest
+ logger.debug(
+ `${indent}Trying to consume segment "${currentPathPart}" for wildcard`
+ );
+ if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
+ logger.debug(
+ `${indent}Successfully matched by consuming segment for wildcard`
+ );
+ return true;
+ }
+
+ logger.debug(`${indent}Failed to match wildcard`);
+ return false;
+ }
+
+ // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
+ if (currentPatternPart.includes("*")) {
+ logger.debug(
+ `${indent}Found in-segment wildcard in "${currentPatternPart}"`
+ );
+
+ // Convert the pattern segment to a regex pattern
+ const regexPattern = currentPatternPart
+ .replace(/\*/g, ".*") // Replace * with .* for regex wildcard
+ .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
+
+ const regex = new RegExp(`^${regexPattern}$`);
+
+ if (regex.test(currentPathPart)) {
+ logger.debug(
+ `${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
+ );
+ return matchSegments(
+ patternIndex + 1,
+ pathIndex + 1,
+ depth + 1
+ );
+ }
+
+ logger.debug(
+ `${indent}Segment with wildcard mismatch: "${currentPatternPart}" doesn't match "${currentPathPart}"`
+ );
+ return false;
+ }
+
+ // For regular segments, they must match exactly
+ if (currentPatternPart !== currentPathPart) {
+ logger.debug(
+ `${indent}Segment mismatch: "${currentPatternPart}" != "${currentPathPart}"`
+ );
+ return false;
+ }
+
+ logger.debug(
+ `${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
+ );
+ // Move to next segments in both pattern and path
+ return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
+ }
+
+ const result = matchSegments(0, 0, 0);
+ logger.debug(`Final result: ${result}`);
+ return result;
+}
async function isIpInGeoIP(
ipCountryCode: string | undefined,
diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts
index ae231f8c8..8ad41e9e4 100644
--- a/server/routers/blueprints/applyJSONBlueprint.ts
+++ b/server/routers/blueprints/applyJSONBlueprint.ts
@@ -99,7 +99,7 @@ export async function applyJSONBlueprint(
source: "API"
});
} catch (error) {
- logger.debug(`Failed to update database from config: ${error}`);
+ logger.error(`Failed to update database from config: ${error}`);
return next(
createHttpError(
HttpCode.BAD_REQUEST,
diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts
index 11c7c9ec2..ecda098c5 100644
--- a/server/routers/client/createClient.ts
+++ b/server/routers/client/createClient.ts
@@ -24,14 +24,9 @@ import { isIpInCidr } from "@server/lib/ip";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { generateId } from "@server/auth/sessions/app";
import { OpenAPITags, registry } from "@server/openApi";
-import {
- rebuildClientAssociationsFromClient,
- isOrgRebuildRateLimited
-} from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { getUniqueClientName } from "@server/db/names";
import { build } from "@server/build";
-import { LimitId } from "@server/lib/billing";
-import { usageService } from "@server/lib/billing/usageService";
const createClientParamsSchema = z.strictObject({
orgId: z.string()
@@ -130,38 +125,6 @@ export async function createClient(
);
}
- if (build == "saas") {
- const usage = await usageService.getUsage(
- orgId,
- LimitId.MACHINE_CLIENTS
- );
- if (!usage) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- "No usage data found for this organization"
- )
- );
- }
- const rejectClient = await usageService.checkLimitSet(
- orgId,
-
- LimitId.MACHINE_CLIENTS,
- {
- ...usage,
- instantaneousValue: (usage.instantaneousValue || 0) + 1
- } // We need to add one to know if we are violating the limit
- );
- if (rejectClient) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "Machine client limit exceeded. Please upgrade your plan."
- )
- );
- }
- }
-
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
@@ -191,15 +154,6 @@ export async function createClient(
);
}
- if (await isOrgRebuildRateLimited(orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
// make sure the subnet is unique
@@ -323,23 +277,23 @@ export async function createClient(
clientId: newClient.clientId,
dateCreated: moment().toISOString()
});
-
- await usageService.add(orgId, LimitId.MACHINE_CLIENTS, 1, trx);
});
if (newClient) {
- rebuildClientAssociationsFromClient(newClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations after creating client: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations after creating client: ${e}`
+ );
+ }
+ );
}
return response(res, {
data: newClient,
success: true,
error: false,
- message: "Client created successfully",
+ message: "Site created successfully",
status: HttpCode.CREATED
});
} catch (error) {
diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts
index 70027e4a5..09bec218a 100644
--- a/server/routers/client/createUserClient.ts
+++ b/server/routers/client/createUserClient.ts
@@ -21,10 +21,7 @@ import { isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import { OpenAPITags, registry } from "@server/openApi";
-import {
- rebuildClientAssociationsFromClient,
- isOrgRebuildRateLimited
-} from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { getUniqueClientName } from "@server/db/names";
const paramsSchema = z
@@ -149,15 +146,6 @@ export async function createUserClient(
);
}
- if (await isOrgRebuildRateLimited(orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
// make sure the subnet is unique
@@ -267,11 +255,13 @@ export async function createUserClient(
});
if (newClient) {
- rebuildClientAssociationsFromClient(newClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations after creating user client: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations after creating user client: ${e}`
+ );
+ }
+ );
}
return response(res, {
diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts
index f09d6432e..24ab9917a 100644
--- a/server/routers/client/deleteClient.ts
+++ b/server/routers/client/deleteClient.ts
@@ -9,14 +9,9 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
-import {
- rebuildClientAssociationsFromClient,
- isOrgRebuildRateLimited
-} from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { sendTerminateClient } from "./terminate";
import { OlmErrorCodes } from "../olm/error";
-import { LimitId } from "@server/lib/billing/features";
-import { usageService } from "@server/lib/billing/usageService";
const deleteClientSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
@@ -81,15 +76,6 @@ export async function deleteClient(
);
}
- if (await isOrgRebuildRateLimited(client.orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
// Only allow deletion of machine clients (clients without userId)
if (client.userId) {
return next(
@@ -120,21 +106,16 @@ export async function deleteClient(
if (!client.userId && client.olmId) {
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
}
-
- await usageService.add(
- deletedClient.orgId,
- LimitId.MACHINE_CLIENTS,
- -1,
- trx
- );
});
if (deletedClient) {
- rebuildClientAssociationsFromClient(deletedClient).catch((e) => {
- logger.error(
- `Failed to rebuild client associations after deleting client ${clientId}: ${e}`
- );
- });
+ rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
+ (e) => {
+ logger.error(
+ `Failed to rebuild client associations after deleting client ${clientId}: ${e}`
+ );
+ }
+ );
if (olm) {
sendTerminateClient(
deletedClient.clientId,
diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts
index 3a05c41c5..9178c27a5 100644
--- a/server/routers/client/listClients.ts
+++ b/server/routers/client/listClients.ts
@@ -41,7 +41,7 @@ const listClientsParamsSchema = z.strictObject({
orgId: z.string()
});
-const listClientsSchema = z.strictObject({
+const listClientsSchema = z.object({
pageSize: z.coerce
.number() // for prettier formatting
.int()
@@ -304,6 +304,11 @@ export async function listClients(
(client) => client.clientId
);
+ const isLabelFeatureEnabled = await isLicensedOrSubscribed(
+ orgId,
+ tierMatrix.labels
+ );
+
// Get client count with filter
const conditions = [
and(
@@ -336,7 +341,7 @@ export async function listClients(
conditions.push(or(...filterAggregates));
}
- if (labelFilter && labelFilter.length > 0) {
+ if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
conditions.push(
inArray(
clients.clientId,
@@ -356,20 +361,25 @@ export async function listClients(
const q = "%" + query.toLowerCase() + "%";
const queryList = [
like(sql`LOWER(${clients.name})`, q),
- like(sql`LOWER(${clients.niceId})`, q),
- inArray(
- clients.clientId,
- db
- .select({ id: clientLabels.clientId })
- .from(clientLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, clientLabels.labelId)
- )
- .where(like(sql`LOWER(${labels.name})`, q))
- )
+ like(sql`LOWER(${clients.niceId})`, q)
];
+ if (isLabelFeatureEnabled) {
+ queryList.push(
+ inArray(
+ clients.clientId,
+ db
+ .select({ id: clientLabels.clientId })
+ .from(clientLabels)
+ .innerJoin(
+ labels,
+ eq(labels.labelId, clientLabels.labelId)
+ )
+ .where(like(sql`LOWER(${labels.name})`, q))
+ )
+ );
+ }
+
conditions.push(or(...queryList));
}
@@ -404,7 +414,7 @@ export async function listClients(
clientId: number;
}> = [];
- if (clientIds.length > 0) {
+ if (isLabelFeatureEnabled && clientIds.length > 0) {
labelsForClients = await db
.select({
labelId: labels.labelId,
diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts
index fb3004921..5a864f93b 100644
--- a/server/routers/client/listUserDevices.ts
+++ b/server/routers/client/listUserDevices.ts
@@ -40,7 +40,7 @@ const listUserDevicesParamsSchema = z.strictObject({
orgId: z.string()
});
-const listUserDevicesSchema = z.strictObject({
+const listUserDevicesSchema = z.object({
pageSize: z.coerce
.number() // for prettier formatting
.int()
@@ -420,6 +420,31 @@ export async function listUserDevices(
}
);
+ // 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
+ // );
+ // }
+
return response(res, {
data: {
devices: olmsWithUpdates,
diff --git a/server/routers/client/rebuildClientAssociationsCacheRoute.ts b/server/routers/client/rebuildClientAssociationsCacheRoute.ts
index ca149fa97..32a6a407a 100644
--- a/server/routers/client/rebuildClientAssociationsCacheRoute.ts
+++ b/server/routers/client/rebuildClientAssociationsCacheRoute.ts
@@ -9,7 +9,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
-import { rebuildClientAssociationsFromClient, isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
+import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const paramsSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive())
@@ -60,26 +60,13 @@ export async function rebuildClientAssociationsCacheRoute(
);
}
- if (await isOrgRebuildRateLimited(client.orgId)) {
- return next(
- createHttpError(
- HttpCode.TOO_MANY_REQUESTS,
- "Too many concurrent rebuild operations for this organization. Please retry after a moment."
- )
- );
- }
-
- rebuildClientAssociationsFromClient(client).catch((e) => {
- logger.error(
- `Failed to rebuild client associations for client ${clientId}: ${e}`
- );
- });
+ await rebuildClientAssociationsFromClient(client);
return response(res, {
data: null,
success: true,
error: false,
- message: "Client association cache queued successfully",
+ message: "Client association cache rebuilt successfully",
status: HttpCode.OK
});
} catch (error) {
diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts
index 543293a10..c208acd88 100644
--- a/server/routers/client/targets.ts
+++ b/server/routers/client/targets.ts
@@ -1,4 +1,4 @@
-import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
+import { sendToClient } from "#dynamic/routers/ws";
import { db, newts, olms } from "@server/db";
import {
Alias,
@@ -8,7 +8,7 @@ import {
} from "@server/lib/ip";
import { canCompress } from "@server/lib/clientVersionChecks";
import logger from "@server/logger";
-import { eq, inArray } from "drizzle-orm";
+import { eq } from "drizzle-orm";
import semver from "semver";
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
@@ -59,42 +59,6 @@ export async function addTargets(
);
}
-export async function addTargetsBatch(
- entries: {
- newtId: string;
- targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
- version?: string | null;
- }[]
-) {
- if (entries.length === 0) {
- return;
- }
-
- const resolved = await Promise.all(
- entries.map(async (entry) => ({
- ...entry,
- targets: await convertTargetsIfNecessary(
- entry.newtId,
- entry.targets
- )
- }))
- );
-
- await sendToClientsBatch(
- resolved.map((entry) => ({
- clientId: entry.newtId,
- message: {
- type: `newt/wg/targets/add`,
- data: entry.targets
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(entry.version, "newt")
- }
- }))
- );
-}
-
export async function removeTargets(
newtId: string,
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
@@ -112,42 +76,6 @@ export async function removeTargets(
);
}
-export async function removeTargetsBatch(
- entries: {
- newtId: string;
- targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
- version?: string | null;
- }[]
-) {
- if (entries.length === 0) {
- return;
- }
-
- const resolved = await Promise.all(
- entries.map(async (entry) => ({
- ...entry,
- targets: await convertTargetsIfNecessary(
- entry.newtId,
- entry.targets
- )
- }))
- );
-
- await sendToClientsBatch(
- resolved.map((entry) => ({
- clientId: entry.newtId,
- message: {
- type: `newt/wg/targets/remove`,
- data: entry.targets
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(entry.version, "newt")
- }
- }))
- );
-}
-
export async function updateTargets(
newtId: string,
targets: {
@@ -273,235 +201,6 @@ export async function removePeerData(
});
}
-const resolveOlmTargets = async (
- entries: {
- clientId: number;
- olmId?: string;
- version?: string | null;
- }[]
-) => {
- const unresolvedClientIds = entries
- .filter((entry) => !entry.olmId)
- .map((entry) => entry.clientId);
-
- const olmMap = new Map();
-
- if (unresolvedClientIds.length > 0) {
- const olmRows = await db
- .select({
- clientId: olms.clientId,
- olmId: olms.olmId,
- version: olms.version
- })
- .from(olms)
- .where(inArray(olms.clientId, unresolvedClientIds));
-
- for (const row of olmRows) {
- if (row.clientId !== null) {
- olmMap.set(row.clientId, {
- olmId: row.olmId,
- version: row.version
- });
- }
- }
- }
-
- return entries
- .map((entry) => {
- if (entry.olmId) {
- return {
- clientId: entry.clientId,
- olmId: entry.olmId,
- version: entry.version
- };
- }
-
- const resolved = olmMap.get(entry.clientId);
- if (!resolved) {
- return null;
- }
-
- return {
- clientId: entry.clientId,
- olmId: resolved.olmId,
- version: entry.version ?? resolved.version
- };
- })
- .filter((entry) => entry !== null);
-};
-
-export async function addPeerDataBatch(
- entries: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: Alias[];
- olmId?: string;
- version?: string | null;
- }[]
-) {
- if (entries.length === 0) {
- return;
- }
-
- const resolvedTargets = await resolveOlmTargets(entries);
-
- if (resolvedTargets.length === 0) {
- return;
- }
-
- const payloads = entries
- .map((entry) => {
- const resolved = resolvedTargets.find(
- (target) => target.clientId === entry.clientId
- );
- if (!resolved) {
- return null;
- }
-
- return {
- clientId: resolved.olmId,
- message: {
- type: `olm/wg/peer/data/add`,
- data: {
- siteId: entry.siteId,
- remoteSubnets: entry.remoteSubnets,
- aliases: entry.aliases
- }
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(resolved.version, "olm")
- }
- };
- })
- .filter((entry) => entry !== null);
-
- if (payloads.length === 0) {
- return;
- }
-
- await sendToClientsBatch(payloads);
-}
-
-export async function removePeerDataBatch(
- entries: {
- clientId: number;
- siteId: number;
- remoteSubnets: string[];
- aliases: Alias[];
- olmId?: string;
- version?: string | null;
- }[]
-) {
- if (entries.length === 0) {
- return;
- }
-
- const resolvedTargets = await resolveOlmTargets(entries);
-
- if (resolvedTargets.length === 0) {
- return;
- }
-
- const payloads = entries
- .map((entry) => {
- const resolved = resolvedTargets.find(
- (target) => target.clientId === entry.clientId
- );
- if (!resolved) {
- return null;
- }
-
- return {
- clientId: resolved.olmId,
- message: {
- type: `olm/wg/peer/data/remove`,
- data: {
- siteId: entry.siteId,
- remoteSubnets: entry.remoteSubnets,
- aliases: entry.aliases
- }
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(resolved.version, "olm")
- }
- };
- })
- .filter((entry) => entry !== null);
-
- if (payloads.length === 0) {
- return;
- }
-
- await sendToClientsBatch(payloads);
-}
-
-export async function updatePeerDataBatch(
- entries: {
- clientId: number;
- siteId: number;
- remoteSubnets:
- | {
- oldRemoteSubnets: string[];
- newRemoteSubnets: string[];
- }
- | undefined;
- aliases:
- | {
- oldAliases: Alias[];
- newAliases: Alias[];
- }
- | undefined;
- olmId?: string;
- version?: string | null;
- }[]
-) {
- if (entries.length === 0) {
- return;
- }
-
- const resolvedTargets = await resolveOlmTargets(entries);
-
- if (resolvedTargets.length === 0) {
- return;
- }
-
- const payloads = entries
- .map((entry) => {
- const resolved = resolvedTargets.find(
- (target) => target.clientId === entry.clientId
- );
- if (!resolved) {
- return null;
- }
-
- return {
- clientId: resolved.olmId,
- message: {
- type: `olm/wg/peer/data/update`,
- data: {
- siteId: entry.siteId,
- ...entry.remoteSubnets,
- ...entry.aliases
- }
- },
- options: {
- incrementConfigVersion: true,
- compress: canCompress(resolved.version, "olm")
- }
- };
- })
- .filter((entry) => entry !== null);
-
- if (payloads.length === 0) {
- return;
- }
-
- await sendToClientsBatch(payloads);
-}
-
export async function updatePeerData(
clientId: number,
siteId: number,
diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts
index 8370b5465..ceb61b25f 100644
--- a/server/routers/domain/createOrgDomain.ts
+++ b/server/routers/domain/createOrgDomain.ts
@@ -17,7 +17,7 @@ import { subdomainSchema } from "@server/lib/schemas";
import { generateId } from "@server/auth/sessions/app";
import { eq, and } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators";
import { build } from "@server/build";
import config from "@server/lib/config";
@@ -120,7 +120,7 @@ export async function createOrgDomain(
}
if (build == "saas") {
- const usage = await usageService.getUsage(orgId, LimitId.DOMAINS);
+ const usage = await usageService.getUsage(orgId, FeatureId.DOMAINS);
if (!usage) {
return next(
createHttpError(
@@ -132,7 +132,7 @@ export async function createOrgDomain(
const rejectDomains = await usageService.checkLimitSet(
orgId,
- LimitId.DOMAINS,
+ FeatureId.DOMAINS,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -346,7 +346,7 @@ export async function createOrgDomain(
await trx.insert(dnsRecords).values(recordsToInsert);
}
- await usageService.add(orgId, LimitId.DOMAINS, 1, trx);
+ await usageService.add(orgId, FeatureId.DOMAINS, 1, trx);
});
if (!returned) {
diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts
index 3f443eac5..4c347668e 100644
--- a/server/routers/domain/deleteOrgDomain.ts
+++ b/server/routers/domain/deleteOrgDomain.ts
@@ -8,7 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq } from "drizzle-orm";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
const paramsSchema = z.strictObject({
domainId: z.string(),
@@ -77,7 +77,7 @@ export async function deleteAccountDomain(
await trx.delete(domains).where(eq(domains.domainId, domainId));
- await usageService.add(orgId, LimitId.DOMAINS, -1, trx);
+ await usageService.add(orgId, FeatureId.DOMAINS, -1, trx);
});
return response(res, {
diff --git a/server/routers/external.ts b/server/routers/external.ts
index 9e4c5b3f1..960c00249 100644
--- a/server/routers/external.ts
+++ b/server/routers/external.ts
@@ -17,7 +17,6 @@ import * as idp from "./idp";
import * as blueprints from "./blueprints";
import * as apiKeys from "./apiKeys";
import * as logs from "./auditLogs";
-import * as launcher from "./launcher";
import * as newt from "./newt";
import * as olm from "./olm";
import * as serverInfo from "./serverInfo";
@@ -54,7 +53,6 @@ import { build } from "@server/build";
import { createStore } from "#dynamic/lib/rateLimitStore";
import { logActionAudit } from "#dynamic/middlewares";
import { checkRoundTripMessage } from "./ws";
-import * as labels from "@server/routers/labels";
// Root routes
export const unauthenticated = Router();
@@ -256,14 +254,6 @@ authenticated.delete(
site.deleteSite
);
-authenticated.post(
- "/site/:siteId/restart",
- verifySiteAccess,
- verifyUserHasAction(ActionsEnum.restartSite),
- logActionAudit(ActionsEnum.restartSite),
- site.restartSite
-);
-
// TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite"
authenticated.get(
"/site/:siteId/docker/status",
@@ -328,14 +318,6 @@ authenticated.get(
siteResource.listAllSiteResourcesByOrg
);
-authenticated.get(
- "/org/:orgId/site-resource/:siteResourceId",
- verifyOrgAccess,
- verifySiteResourceAccess,
- verifyUserHasAction(ActionsEnum.getSiteResource),
- siteResource.getSiteResource
-);
-
authenticated.get(
"/site-resource/:siteResourceId",
verifySiteResourceAccess,
@@ -473,78 +455,6 @@ authenticated.get(
resource.getUserResources
);
-authenticated.get(
- "/org/:orgId/launcher/groups",
- verifyOrgAccess,
- launcher.listLauncherGroups
-);
-
-authenticated.get(
- "/org/:orgId/launcher/scale",
- verifyOrgAccess,
- launcher.listLauncherScale
-);
-
-authenticated.get(
- "/org/:orgId/launcher/resources",
- verifyOrgAccess,
- launcher.listLauncherResources
-);
-
-authenticated.get(
- "/org/:orgId/launcher/sites",
- verifyOrgAccess,
- launcher.listLauncherSites
-);
-
-authenticated.get(
- "/org/:orgId/launcher/labels",
- verifyOrgAccess,
- launcher.listLauncherLabels
-);
-
-authenticated.get(
- "/org/:orgId/launcher/views",
- verifyOrgAccess,
- launcher.listLauncherViews
-);
-
-authenticated.post(
- "/org/:orgId/launcher/invalidate-cache",
- verifyOrgAccess,
- launcher.invalidateLauncherCache
-);
-
-authenticated.post(
- "/org/:orgId/launcher/views",
- verifyOrgAccess,
- launcher.createLauncherView
-);
-
-authenticated.put(
- "/org/:orgId/launcher/views/:viewId",
- verifyOrgAccess,
- launcher.updateLauncherView
-);
-
-authenticated.put(
- "/org/:orgId/launcher/default-view",
- verifyOrgAccess,
- launcher.upsertLauncherDefaultView
-);
-
-authenticated.delete(
- "/org/:orgId/launcher/default-view",
- verifyOrgAccess,
- launcher.deleteLauncherDefaultView
-);
-
-authenticated.delete(
- "/org/:orgId/launcher/views/:viewId",
- verifyOrgAccess,
- launcher.deleteLauncherView
-);
-
authenticated.get(
"/org/:orgId/user-resource-aliases",
verifyOrgAccess,
@@ -1000,6 +910,19 @@ unauthenticated.post(
);
unauthenticated.get("/my-device", verifySessionMiddleware, user.myDevice);
+authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers);
+authenticated.get("/user/:userId", verifyUserIsServerAdmin, user.adminGetUser);
+authenticated.post(
+ "/user/:userId/generate-password-reset-code",
+ verifyUserIsServerAdmin,
+ user.adminGeneratePasswordResetCode
+);
+authenticated.delete(
+ "/user/:userId",
+ verifyUserIsServerAdmin,
+ user.adminRemoveUser
+);
+
authenticated.put(
"/org/:orgId/user",
verifyOrgAccess,
@@ -1022,6 +945,12 @@ authenticated.post(
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
+authenticated.post(
+ "/user/:userId/2fa",
+ verifyUserIsServerAdmin,
+ user.updateUser2FA
+);
+
authenticated.get(
"/org/:orgId/users",
verifyOrgAccess,
@@ -1104,112 +1033,85 @@ authenticated.post(
olm.recoverOlmWithFingerprint
);
-if (build !== "saas") {
- authenticated.put(
- "/idp/oidc",
- verifyUserIsServerAdmin,
- // verifyUserHasAction(ActionsEnum.createIdp),
- idp.createOidcIdp
- );
+authenticated.put(
+ "/idp/oidc",
+ verifyUserIsServerAdmin,
+ // verifyUserHasAction(ActionsEnum.createIdp),
+ idp.createOidcIdp
+);
- authenticated.post(
- "/idp/:idpId/oidc",
- verifyUserIsServerAdmin,
- idp.updateOidcIdp
- );
+authenticated.post(
+ "/idp/:idpId/oidc",
+ verifyUserIsServerAdmin,
+ idp.updateOidcIdp
+);
- authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
+authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
- authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
+authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
- authenticated.put(
- "/idp/:idpId/org/:orgId",
- verifyUserIsServerAdmin,
- idp.createIdpOrgPolicy
- );
+authenticated.put(
+ "/idp/:idpId/org/:orgId",
+ verifyUserIsServerAdmin,
+ idp.createIdpOrgPolicy
+);
- authenticated.post(
- "/idp/:idpId/org/:orgId",
- verifyUserIsServerAdmin,
- idp.updateIdpOrgPolicy
- );
+authenticated.post(
+ "/idp/:idpId/org/:orgId",
+ verifyUserIsServerAdmin,
+ idp.updateIdpOrgPolicy
+);
- authenticated.delete(
- "/idp/:idpId/org/:orgId",
- verifyUserIsServerAdmin,
- idp.deleteIdpOrgPolicy
- );
+authenticated.delete(
+ "/idp/:idpId/org/:orgId",
+ verifyUserIsServerAdmin,
+ idp.deleteIdpOrgPolicy
+);
- authenticated.get(
- "/idp/:idpId/org",
- verifyUserIsServerAdmin,
- idp.listIdpOrgPolicies
- );
-
- authenticated.get(
- `/api-key/:apiKeyId`,
- verifyUserIsServerAdmin,
- apiKeys.getApiKey
- );
-
- authenticated.put(
- `/api-key`,
- verifyUserIsServerAdmin,
- apiKeys.createRootApiKey
- );
-
- authenticated.delete(
- `/api-key/:apiKeyId`,
- verifyUserIsServerAdmin,
- apiKeys.deleteApiKey
- );
-
- authenticated.get(
- `/api-keys`,
- verifyUserIsServerAdmin,
- apiKeys.listRootApiKeys
- );
-
- authenticated.get(
- `/api-key/:apiKeyId/actions`,
- verifyUserIsServerAdmin,
- apiKeys.listApiKeyActions
- );
-
- authenticated.post(
- `/api-key/:apiKeyId/actions`,
- verifyUserIsServerAdmin,
- apiKeys.setApiKeyActions
- );
-
- authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers);
-
- authenticated.get(
- "/user/:userId",
- verifyUserIsServerAdmin,
- user.adminGetUser
- );
-
- authenticated.post(
- "/user/:userId/generate-password-reset-code",
- verifyUserIsServerAdmin,
- user.adminGeneratePasswordResetCode
- );
-
- authenticated.delete(
- "/user/:userId",
- verifyUserIsServerAdmin,
- user.adminRemoveUser
- );
-
- authenticated.post(
- "/user/:userId/2fa",
- verifyUserIsServerAdmin,
- user.updateUser2FA
- );
-}
+authenticated.get(
+ "/idp/:idpId/org",
+ verifyUserIsServerAdmin,
+ idp.listIdpOrgPolicies
+);
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
+authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
+
+authenticated.get(
+ `/api-key/:apiKeyId`,
+ verifyUserIsServerAdmin,
+ apiKeys.getApiKey
+);
+
+authenticated.put(
+ `/api-key`,
+ verifyUserIsServerAdmin,
+ apiKeys.createRootApiKey
+);
+
+authenticated.delete(
+ `/api-key/:apiKeyId`,
+ verifyUserIsServerAdmin,
+ apiKeys.deleteApiKey
+);
+
+authenticated.get(
+ `/api-keys`,
+ verifyUserIsServerAdmin,
+ apiKeys.listRootApiKeys
+);
+
+authenticated.get(
+ `/api-key/:apiKeyId/actions`,
+ verifyUserIsServerAdmin,
+ apiKeys.listApiKeyActions
+);
+
+authenticated.post(
+ `/api-key/:apiKeyId/actions`,
+ verifyUserIsServerAdmin,
+ apiKeys.setApiKeyActions
+);
authenticated.get(
`/org/:orgId/api-keys`,
@@ -1336,48 +1238,6 @@ authenticated.get(
authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage);
-authenticated.get(
- "/org/:orgId/labels",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.listOrgLabels),
- labels.listOrgLabels
-);
-
-authenticated.post(
- "/org/:orgId/labels",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.createOrgLabel),
- labels.createOrgLabel
-);
-
-authenticated.patch(
- "/org/:orgId/label/:labelId",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.updateOrgLabel),
- labels.updateOrgLabel
-);
-
-authenticated.delete(
- "/org/:orgId/label/:labelId",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.deleteOrgLabel),
- labels.deleteOrgLabel
-);
-
-authenticated.put(
- "/org/:orgId/label/:labelId/attach",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.attachLabelToItem),
- labels.attachLabelToItem
-);
-
-authenticated.put(
- "/org/:orgId/label/:labelId/detach",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.detachLabelFromItem),
- labels.detachLabelFromItem
-);
-
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
diff --git a/server/routers/gerbil/createExitNode.ts b/server/routers/gerbil/createExitNode.ts
index 9e93cf575..bc9650367 100644
--- a/server/routers/gerbil/createExitNode.ts
+++ b/server/routers/gerbil/createExitNode.ts
@@ -13,41 +13,37 @@ export async function createExitNode(
const [exitNodeQuery] = await db.select().from(exitNodes).limit(1);
let exitNode: ExitNode;
if (!exitNodeQuery) {
- const { value: address, release } = await getNextAvailableSubnet();
- try {
- // TODO: eventually we will want to get the next available port so that we can multiple exit nodes
- // const listenPort = await getNextAvailablePort();
- const listenPort = config.getRawConfig().gerbil.start_port;
- let subEndpoint = "";
- if (config.getRawConfig().gerbil.use_subdomain) {
- subEndpoint = await getUniqueExitNodeEndpointName();
- }
-
- const exitNodeName =
- config.getRawConfig().gerbil.exit_node_name ||
- `Exit Node ${publicKey.slice(0, 8)}`;
-
- // create a new exit node
- [exitNode] = await db
- .insert(exitNodes)
- .values({
- publicKey,
- endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
- address,
- online: true,
- listenPort,
- reachableAt,
- name: exitNodeName
- })
- .returning()
- .execute();
-
- logger.info(
- `Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
- );
- } finally {
- await release();
+ const address = await getNextAvailableSubnet();
+ // TODO: eventually we will want to get the next available port so that we can multiple exit nodes
+ // const listenPort = await getNextAvailablePort();
+ const listenPort = config.getRawConfig().gerbil.start_port;
+ let subEndpoint = "";
+ if (config.getRawConfig().gerbil.use_subdomain) {
+ subEndpoint = await getUniqueExitNodeEndpointName();
}
+
+ const exitNodeName =
+ config.getRawConfig().gerbil.exit_node_name ||
+ `Exit Node ${publicKey.slice(0, 8)}`;
+
+ // create a new exit node
+ [exitNode] = await db
+ .insert(exitNodes)
+ .values({
+ publicKey,
+ endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
+ address,
+ online: true,
+ listenPort,
+ reachableAt,
+ name: exitNodeName
+ })
+ .returning()
+ .execute();
+
+ logger.info(
+ `Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
+ );
} else {
// update the existing exit node
[exitNode] = await db
diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts
index aacf7f843..eacf3dad4 100644
--- a/server/routers/gerbil/receiveBandwidth.ts
+++ b/server/routers/gerbil/receiveBandwidth.ts
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { usageService } from "@server/lib/billing/usageService";
-import { LimitId } from "@server/lib/billing/features";
+import { FeatureId } from "@server/lib/billing/features";
import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
@@ -171,9 +171,8 @@ export async function flushSiteBandwidthToDb(): Promise {
}
// 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 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`
@@ -229,7 +228,7 @@ export async function flushSiteBandwidthToDb(): Promise {
const totalBandwidth = orgUsageMap.get(orgId)!;
const bandwidthUsage = await usageService.add(
orgId,
- LimitId.EGRESS_DATA_MB,
+ FeatureId.EGRESS_DATA_MB,
totalBandwidth
);
if (bandwidthUsage) {
@@ -237,7 +236,7 @@ export async function flushSiteBandwidthToDb(): Promise {
usageService
.checkLimitSet(
orgId,
- LimitId.EGRESS_DATA_MB,
+ FeatureId.EGRESS_DATA_MB,
bandwidthUsage
)
.catch((error: any) => {
@@ -248,7 +247,10 @@ export async function flushSiteBandwidthToDb(): Promise {
});
}
} catch (error) {
- logger.error(`Error processing usage for org ${orgId}:`, error);
+ logger.error(
+ `Error processing usage for org ${orgId}:`,
+ error
+ );
// Continue with other orgs.
}
}
diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts
index 57b16964d..6a82f2c12 100644
--- a/server/routers/idp/validateOidcCallback.ts
+++ b/server/routers/idp/validateOidcCallback.ts
@@ -31,7 +31,7 @@ import {
} from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto";
import { UserType } from "@server/types/UserTypes";
-import { LimitId } from "@server/lib/billing";
+import { FeatureId } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
@@ -635,7 +635,7 @@ export async function validateOidcCallback(
}
});
- calculateUserClientsForOrgs(userId!).catch((err) => {
+ calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => {
logger.error(
"Error calculating user clients after syncing orgs and roles for OIDC user",
{ error: err }
@@ -645,7 +645,7 @@ export async function validateOidcCallback(
for (const orgCount of orgUserCounts) {
await usageService.updateCount(
orgCount.orgId,
- LimitId.USERS,
+ FeatureId.USERS,
orgCount.userCount
);
}
diff --git a/server/routers/integration.ts b/server/routers/integration.ts
index 74ea20078..ceec501f9 100644
--- a/server/routers/integration.ts
+++ b/server/routers/integration.ts
@@ -17,6 +17,7 @@ import {
verifyApiKey,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction,
+ verifyApiKeyCanSetUserOrgRoles,
verifyApiKeySiteAccess,
verifyApiKeyResourceAccess,
verifyApiKeyTargetAccess,
@@ -159,7 +160,6 @@ authenticated.get(
verifyApiKeyOrgAccess,
resource.getUserResources
);
-
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site-resource",
@@ -808,16 +808,6 @@ authenticated.post(
accessToken.generateAccessToken
);
-authenticated.post(
- `/resource/:resourceId/session-token`,
- verifyApiKeyResourceAccess,
- verifyApiKeyUserAccess,
- verifyLimits,
- verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
- logActionAudit(ActionsEnum.createResourceSessionToken),
- resource.createResourceSessionToken
-);
-
authenticated.delete(
`/access-token/:accessTokenId`,
verifyApiKeyAccessTokenAccess,
@@ -905,6 +895,12 @@ authenticated.delete(
user.removeUserOrg
);
+// authenticated.put(
+// "/newt",
+// verifyApiKeyHasAction(ActionsEnum.createNewt),
+// newt.createNewt
+// );
+
authenticated.get(
`/org/:orgId/api-keys`,
verifyApiKeyIsRoot,
@@ -978,13 +974,6 @@ authenticated.get(
idp.getIdp
);
-authenticated.delete(
- "/idp/:idpId",
- verifyApiKeyIsRoot,
- verifyApiKeyHasAction(ActionsEnum.deleteIdp),
- idp.deleteIdp
-);
-
authenticated.put(
"/idp/:idpId/org/:orgId",
verifyApiKeyIsRoot,
diff --git a/server/routers/labels/index.ts b/server/routers/labels/index.ts
deleted file mode 100644
index aa8c34222..000000000
--- a/server/routers/labels/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from "./listOrgLabels";
-export * from "./createOrgLabel";
-export * from "./updateOrgLabel";
-export * from "./attachLabelToItem";
-export * from "./detachLabelFromItem";
-export * from "./deleteOrgLabel";
diff --git a/server/routers/launcher/createLauncherView.ts b/server/routers/launcher/createLauncherView.ts
deleted file mode 100644
index 195dfc555..000000000
--- a/server/routers/launcher/createLauncherView.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { db, launcherViews } from "@server/db";
-import { response } from "@server/lib/response";
-import HttpCode from "@server/types/HttpCode";
-import { NextFunction, Request, Response } from "express";
-import createHttpError from "http-errors";
-import moment from "moment";
-import { fromZodError } from "zod-validation-error";
-import { z } from "zod";
-import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
-import { launcherViewConfigSchema } from "./types";
-
-const createLauncherViewBodySchema = z.strictObject({
- name: z.string().min(1).max(128),
- config: launcherViewConfigSchema,
- orgWide: z.boolean().optional().default(false)
-});
-
-export async function createLauncherView(
- req: Request,
- res: Response,
- next: NextFunction
-): Promise {
- try {
- const orgId = req.userOrgId;
- const userId = req.user!.userId;
-
- if (!orgId) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
- );
- }
-
- const parsed = createLauncherViewBodySchema.safeParse(req.body);
- if (!parsed.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromZodError(parsed.error)
- )
- );
- }
-
- if (parsed.data.orgWide) {
- const canCreateOrgWide = await checkUserActionPermission(
- ActionsEnum.createOrgWideLauncherView,
- req
- );
- if (!canCreateOrgWide) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "User does not have permission perform this action"
- )
- );
- }
- }
-
- const now = moment().toISOString();
- const [created] = await db
- .insert(launcherViews)
- .values({
- orgId,
- userId: parsed.data.orgWide ? null : userId,
- name: parsed.data.name,
- config: JSON.stringify(parsed.data.config),
- createdAt: now,
- updatedAt: now
- })
- .returning();
-
- return response(res, {
- data: {
- viewId: created.viewId,
- orgId: created.orgId,
- userId: created.userId,
- name: created.name,
- config: launcherViewConfigSchema.parse(
- JSON.parse(created.config)
- ),
- createdAt: created.createdAt,
- updatedAt: created.updatedAt,
- isOrgWide: created.userId == null,
- isDefault: created.isDefault
- },
- success: true,
- error: false,
- message: "Launcher view created successfully",
- status: HttpCode.CREATED
- });
- } catch (error) {
- if (createHttpError.isHttpError(error)) {
- return next(error);
- }
- console.error("Error creating launcher view:", error);
- return next(
- createHttpError(
- HttpCode.INTERNAL_SERVER_ERROR,
- "Internal server error"
- )
- );
- }
-}
diff --git a/server/routers/launcher/deleteLauncherDefaultView.ts b/server/routers/launcher/deleteLauncherDefaultView.ts
deleted file mode 100644
index 50a1a7beb..000000000
--- a/server/routers/launcher/deleteLauncherDefaultView.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { response } from "@server/lib/response";
-import HttpCode from "@server/types/HttpCode";
-import { NextFunction, Request, Response } from "express";
-import createHttpError from "http-errors";
-import { fromZodError } from "zod-validation-error";
-import { z } from "zod";
-import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
-import {
- deleteAllDefaultViewOverrides,
- deleteDefaultViewOverride
-} from "./launcherDefaultView";
-
-const deleteLauncherDefaultViewBodySchema = z.strictObject({
- orgWide: z.boolean().optional().default(false),
- all: z.boolean().optional().default(false)
-});
-
-export async function deleteLauncherDefaultView(
- req: Request,
- res: Response,
- next: NextFunction
-): Promise {
- try {
- const orgId = req.userOrgId;
- const userId = req.user!.userId;
-
- if (!orgId) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
- );
- }
-
- const parsed = deleteLauncherDefaultViewBodySchema.safeParse(req.body);
- if (!parsed.success) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- fromZodError(parsed.error)
- )
- );
- }
-
- if (parsed.data.all) {
- const canManageOrgWide = await checkUserActionPermission(
- ActionsEnum.createOrgWideLauncherView,
- req
- );
- if (!canManageOrgWide) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "User does not have permission perform this action"
- )
- );
- }
-
- await deleteAllDefaultViewOverrides(orgId, userId);
- } else if (parsed.data.orgWide) {
- const canManageOrgWide = await checkUserActionPermission(
- ActionsEnum.createOrgWideLauncherView,
- req
- );
- if (!canManageOrgWide) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "User does not have permission perform this action"
- )
- );
- }
-
- await deleteDefaultViewOverride({
- orgId,
- userId,
- orgWide: true
- });
- } else {
- await deleteDefaultViewOverride({
- orgId,
- userId,
- orgWide: false
- });
- }
-
- return response(res, {
- data: null,
- success: true,
- error: false,
- message: "Launcher default view reset successfully",
- status: HttpCode.OK
- });
- } catch (error) {
- if (createHttpError.isHttpError(error)) {
- return next(error);
- }
- console.error("Error resetting launcher default view:", error);
- return next(
- createHttpError(
- HttpCode.INTERNAL_SERVER_ERROR,
- "Internal server error"
- )
- );
- }
-}
diff --git a/server/routers/launcher/deleteLauncherView.ts b/server/routers/launcher/deleteLauncherView.ts
deleted file mode 100644
index 8377872fb..000000000
--- a/server/routers/launcher/deleteLauncherView.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { db, launcherViews } from "@server/db";
-import { response } from "@server/lib/response";
-import { getFirstString } from "@server/lib/requestParams";
-import HttpCode from "@server/types/HttpCode";
-import { and, eq } from "drizzle-orm";
-import { NextFunction, Request, Response } from "express";
-import createHttpError from "http-errors";
-import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
-
-export async function deleteLauncherView(
- req: Request,
- res: Response,
- next: NextFunction
-): Promise {
- try {
- const orgId = req.userOrgId;
- const userId = req.user!.userId;
- const viewId = Number.parseInt(
- getFirstString(req.params.viewId) ?? "",
- 10
- );
-
- if (!orgId || !Number.isFinite(viewId)) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "Invalid request parameters"
- )
- );
- }
-
- const [existing] = await db
- .select()
- .from(launcherViews)
- .where(
- and(
- eq(launcherViews.viewId, viewId),
- eq(launcherViews.orgId, orgId)
- )
- )
- .limit(1);
-
- if (!existing) {
- return next(
- createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
- );
- }
-
- if (existing.isDefault) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "The default view cannot be deleted from here"
- )
- );
- }
-
- const isPersonalView = existing.userId === userId;
- const isOrgWideView = existing.userId == null;
- const canManageOrgWide = await checkUserActionPermission(
- ActionsEnum.createOrgWideLauncherView,
- req
- );
-
- if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "You do not have permission to delete this view"
- )
- );
- }
-
- await db.delete(launcherViews).where(eq(launcherViews.viewId, viewId));
-
- return response(res, {
- data: null,
- success: true,
- error: false,
- message: "Launcher view deleted successfully",
- status: HttpCode.OK
- });
- } catch (error) {
- if (createHttpError.isHttpError(error)) {
- return next(error);
- }
- console.error("Error deleting launcher view:", error);
- return next(
- createHttpError(
- HttpCode.INTERNAL_SERVER_ERROR,
- "Internal server error"
- )
- );
- }
-}
diff --git a/server/routers/launcher/formatLauncherAccess.ts b/server/routers/launcher/formatLauncherAccess.ts
deleted file mode 100644
index c12767ce1..000000000
--- a/server/routers/launcher/formatLauncherAccess.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { formatEndpoint, parseEndpoint } from "@server/lib/ip";
-
-export type SiteResourceDestinationInput = {
- mode: "host" | "cidr" | "http" | "ssh";
- destination: string | null;
- destinationPort: number | null;
- scheme: "http" | "https" | null;
-};
-
-export function resolveHttpHttpsDisplayPort(
- mode: "http",
- destinationPort: number | null
-): number {
- if (destinationPort != null) {
- return destinationPort;
- }
- return 80;
-}
-
-export function formatSiteResourceDestinationDisplay(
- row: SiteResourceDestinationInput
-): string {
- if (!row.destination) {
- return "";
- }
- const { mode, destination, destinationPort, scheme } = row;
- if (mode !== "http") {
- return destination;
- }
- const port = resolveHttpHttpsDisplayPort(mode, destinationPort);
- const downstreamScheme = scheme ?? "http";
- const hostPart =
- destination.includes(":") && !destination.startsWith("[")
- ? `[${destination}]`
- : destination;
- return `${downstreamScheme}://${hostPart}:${port}`;
-}
-
-export type PublicResourceAccessInput = {
- mode: string;
- fullDomain: string | null;
- ssl: boolean;
- proxyPort: number | null;
- wildcard: boolean;
- exitNodeEndpoint?: string | null;
-};
-
-export type SiteResourceAccessInput = {
- mode: string;
- destination: string | null;
- destinationPort: number | null;
- scheme: "http" | "https" | null;
- ssl: boolean;
- fullDomain: string | null;
- alias: string | null;
- aliasAddress: string | null;
-};
-
-export type LauncherAccessFields = {
- accessDisplay: string;
- accessCopyValue: string;
- accessUrl: string | null;
-};
-
-function formatTcpUdpResourceAccess(
- exitNodeEndpoint: string | null | undefined,
- proxyPort: number | null
-): LauncherAccessFields {
- if (proxyPort == null) {
- return {
- accessDisplay: "",
- accessCopyValue: "",
- accessUrl: null
- };
- }
-
- if (!exitNodeEndpoint?.trim()) {
- const port = proxyPort.toString();
- return {
- accessDisplay: port,
- accessCopyValue: port,
- accessUrl: null
- };
- }
-
- const parsed = parseEndpoint(exitNodeEndpoint);
- const host = parsed?.ip ?? exitNodeEndpoint.trim();
- const access = formatEndpoint(host, proxyPort);
-
- return {
- accessDisplay: access,
- accessCopyValue: access,
- accessUrl: null
- };
-}
-
-export function formatPublicResourceAccess(
- resource: PublicResourceAccessInput
-): LauncherAccessFields {
- const browserModes = ["http", "ssh", "rdp", "vnc"];
- if (!browserModes.includes(resource.mode)) {
- return formatTcpUdpResourceAccess(
- resource.exitNodeEndpoint,
- resource.proxyPort
- );
- }
-
- if (!resource.fullDomain) {
- return {
- accessDisplay: "",
- accessCopyValue: "",
- accessUrl: null
- };
- }
-
- const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
- return {
- accessDisplay: url,
- accessCopyValue: url,
- accessUrl: resource.wildcard ? null : url
- };
-}
-
-export function formatSiteResourceAccess(
- resource: SiteResourceAccessInput
-): LauncherAccessFields {
- if (resource.alias) {
- return {
- accessDisplay: resource.alias,
- accessCopyValue: resource.alias,
- accessUrl: null
- };
- }
-
- if (resource.mode === "http" && resource.fullDomain) {
- const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
- return {
- accessDisplay: url,
- accessCopyValue: url,
- accessUrl: url
- };
- }
-
- const destination = formatSiteResourceDestinationDisplay({
- mode: resource.mode as SiteResourceDestinationInput["mode"],
- destination: resource.destination,
- destinationPort: resource.destinationPort,
- scheme: resource.scheme
- });
-
- if (destination) {
- return {
- accessDisplay: destination,
- accessCopyValue: destination,
- accessUrl: resource.mode === "http" ? destination : null
- };
- }
-
- if (resource.aliasAddress) {
- return {
- accessDisplay: resource.aliasAddress,
- accessCopyValue: resource.aliasAddress,
- accessUrl: null
- };
- }
-
- return {
- accessDisplay: "",
- accessCopyValue: "",
- accessUrl: null
- };
-}
diff --git a/server/routers/launcher/index.ts b/server/routers/launcher/index.ts
deleted file mode 100644
index 1c3fed44c..000000000
--- a/server/routers/launcher/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export * from "./types";
-export { listLauncherGroups } from "./listLauncherGroups";
-export { listLauncherScale } from "./listLauncherScale";
-export { listLauncherResources } from "./listLauncherResources";
-export { listLauncherSites } from "./listLauncherSites";
-export { listLauncherLabels } from "./listLauncherLabels";
-export { listLauncherViews } from "./listLauncherViews";
-export { createLauncherView } from "./createLauncherView";
-export { updateLauncherView } from "./updateLauncherView";
-export { deleteLauncherView } from "./deleteLauncherView";
-export { upsertLauncherDefaultView } from "./upsertLauncherDefaultView";
-export { deleteLauncherDefaultView } from "./deleteLauncherDefaultView";
-export { invalidateLauncherCache } from "./invalidateLauncherCache";
diff --git a/server/routers/launcher/invalidateLauncherCache.ts b/server/routers/launcher/invalidateLauncherCache.ts
deleted file mode 100644
index 5e4ff1618..000000000
--- a/server/routers/launcher/invalidateLauncherCache.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { regionalCache as cache } from "#dynamic/lib/cache";
-import { response } from "@server/lib/response";
-import HttpCode from "@server/types/HttpCode";
-import { NextFunction, Request, Response } from "express";
-import createHttpError from "http-errors";
-
-async function invalidateLauncherCacheForUser(
- orgId: string,
- userId: string
-): Promise {
- const prefixes = [
- `launcherAccessibleIds:${orgId}:${userId}:`,
- `launcher:groups:${orgId}:${userId}:`,
- `launcher:results:${orgId}:${userId}:`,
- `launcher:scale:counts:${orgId}:${userId}:`
- ];
-
- const keys = (
- await Promise.all(
- prefixes.map((prefix) => cache.keysWithPrefix(prefix))
- )
- ).flat();
-
- if (keys.length > 0) {
- await cache.del(keys);
- }
-}
-
-export async function invalidateLauncherCache(
- req: Request,
- res: Response,
- next: NextFunction
-): Promise {
- try {
- const orgId = req.userOrgId;
- const userId = req.user!.userId;
-
- if (!orgId) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
- );
- }
-
- await invalidateLauncherCacheForUser(orgId, userId);
-
- return response(res, {
- data: null,
- success: true,
- error: false,
- message: "Launcher cache invalidated successfully",
- status: HttpCode.OK
- });
- } catch (error) {
- if (createHttpError.isHttpError(error)) {
- return next(error);
- }
- console.error("Error invalidating launcher cache:", error);
- return next(
- createHttpError(
- HttpCode.INTERNAL_SERVER_ERROR,
- "Internal server error"
- )
- );
- }
-}
diff --git a/server/routers/launcher/launcherDefaultView.ts b/server/routers/launcher/launcherDefaultView.ts
deleted file mode 100644
index a0c2fd066..000000000
--- a/server/routers/launcher/launcherDefaultView.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { db, launcherViews } from "@server/db";
-import { and, eq, isNull } from "drizzle-orm";
-import moment from "moment";
-import {
- launcherViewConfigSchema,
- type LauncherDefaultViewOverrides,
- type LauncherViewConfig,
- type LauncherViewRecord
-} from "./types";
-
-export function mapViewRow(
- row: typeof launcherViews.$inferSelect
-): LauncherViewRecord {
- return {
- viewId: row.viewId,
- orgId: row.orgId,
- userId: row.userId,
- name: row.name,
- config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- isOrgWide: row.userId == null,
- isDefault: row.isDefault
- };
-}
-
-export function extractDefaultViewOverrides(
- rows: Array
-): LauncherDefaultViewOverrides {
- const overrideRows = rows.filter((row) => row.isDefault);
-
- const personalRow = overrideRows.find((row) => row.userId !== null);
- const orgWideRow = overrideRows.find((row) => row.userId === null);
-
- return {
- personal: personalRow ? mapViewRow(personalRow) : null,
- orgWide: orgWideRow ? mapViewRow(orgWideRow) : null
- };
-}
-
-export function listVisibleLauncherViews(
- rows: Array
-): LauncherViewRecord[] {
- return rows.filter((row) => !row.isDefault).map(mapViewRow);
-}
-
-export async function findDefaultViewOverride(
- orgId: string,
- orgWide: boolean,
- userId: string
-) {
- const [existing] = await db
- .select()
- .from(launcherViews)
- .where(
- and(
- eq(launcherViews.orgId, orgId),
- eq(launcherViews.isDefault, true),
- orgWide
- ? isNull(launcherViews.userId)
- : eq(launcherViews.userId, userId)
- )
- )
- .limit(1);
-
- return existing ?? null;
-}
-
-export async function upsertDefaultViewOverride({
- orgId,
- userId,
- orgWide,
- config
-}: {
- orgId: string;
- userId: string;
- orgWide: boolean;
- config: LauncherViewConfig;
-}) {
- const now = moment().toISOString();
- const existing = await findDefaultViewOverride(orgId, orgWide, userId);
-
- if (existing) {
- const [updated] = await db
- .update(launcherViews)
- .set({
- config: JSON.stringify(config),
- updatedAt: now
- })
- .where(eq(launcherViews.viewId, existing.viewId))
- .returning();
-
- return mapViewRow(updated);
- }
-
- const [created] = await db
- .insert(launcherViews)
- .values({
- orgId,
- userId: orgWide ? null : userId,
- name: "",
- isDefault: true,
- config: JSON.stringify(config),
- createdAt: now,
- updatedAt: now
- })
- .returning();
-
- return mapViewRow(created);
-}
-
-export async function deleteDefaultViewOverride({
- orgId,
- userId,
- orgWide
-}: {
- orgId: string;
- userId: string;
- orgWide: boolean;
-}) {
- const existing = await findDefaultViewOverride(orgId, orgWide, userId);
- if (!existing) {
- return;
- }
-
- await db
- .delete(launcherViews)
- .where(eq(launcherViews.viewId, existing.viewId));
-}
-
-export async function deleteAllDefaultViewOverrides(
- orgId: string,
- userId: string
-) {
- await deleteDefaultViewOverride({ orgId, userId, orgWide: false });
- await deleteDefaultViewOverride({ orgId, userId, orgWide: true });
-}
diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts
deleted file mode 100644
index ec4500262..000000000
--- a/server/routers/launcher/launcherResourceAccess.ts
+++ /dev/null
@@ -1,1607 +0,0 @@
-import { createHash } from "node:crypto";
-import { db } from "@server/db";
-import {
- exitNodes,
- labels,
- launcherViews,
- resourceLabels,
- resources,
- rolePolicies,
- roleResources,
- roles,
- roleSiteResources,
- siteNetworks,
- siteResourceLabels,
- siteResources,
- sites,
- targets,
- userOrgRoles,
- userPolicies,
- userResources,
- userSiteResources
-} from "@server/db";
-import { regionalCache as cache } from "#dynamic/lib/cache";
-import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
-import { tierMatrix } from "@server/lib/billing/tierMatrix";
-import {
- and,
- asc,
- countDistinct,
- eq,
- inArray,
- isNull,
- like,
- or,
- sql,
- type SQL
-} from "drizzle-orm";
-import {
- formatPublicResourceAccess,
- formatSiteResourceAccess
-} from "./formatLauncherAccess";
-import {
- LAUNCHER_FLAT_GROUP_KEY,
- LAUNCHER_NO_SITE_GROUP_KEY,
- LAUNCHER_UNLABELED_GROUP_KEY,
- type LauncherFilterListQuery,
- type LauncherGroup,
- type LauncherLabel,
- type LauncherListQuery,
- type LauncherResource,
- type LauncherSiteInfo,
- parseIdListParam
-} from "./types";
-
-const effectiveResourcePolicyId = sql<
- number | null
->`coalesce(${resources.resourcePolicyId}, ${resources.defaultResourcePolicyId})`;
-
-export type AccessibleIds = {
- resourceIds: number[];
- siteResourceIds: number[];
-};
-
-const LAUNCHER_ACCESSIBLE_IDS_TTL_SEC = 60;
-const LAUNCHER_RESOURCES_RESULT_TTL_SEC = 60;
-const LAUNCHER_GROUPS_RESULT_TTL_SEC = 60;
-
-type LauncherResourcesCacheEntry = {
- items: LauncherResource[];
- total: number;
-};
-
-type LauncherGroupsCacheEntry = {
- groups: LauncherGroup[];
- total: number;
-};
-
-function launcherListQueryHash(
- userRoleIds: number[],
- query: LauncherListQuery,
- extra?: Record
-) {
- const payload = JSON.stringify({
- roles: [...userRoleIds].sort((a, b) => a - b),
- query: query.query,
- groupBy: query.groupBy,
- siteIds: query.siteIds ?? "",
- labelIds: query.labelIds ?? "",
- sort_by: query.sort_by,
- order: query.order,
- ...extra
- });
- return createHash("sha256").update(payload).digest("hex").slice(0, 16);
-}
-
-function launcherResourcesQueryHash(
- userRoleIds: number[],
- query: LauncherListQuery & { groupKey: string }
-) {
- return launcherListQueryHash(userRoleIds, query, {
- groupKey: query.groupKey
- });
-}
-
-function launcherGroupsQueryHash(
- userRoleIds: number[],
- query: LauncherListQuery
-) {
- return launcherListQueryHash(userRoleIds, query);
-}
-
-function launcherResourcesCacheKey(
- orgId: string,
- userId: string,
- queryHash: string
-) {
- return `launcher:results:${orgId}:${userId}:${queryHash}`;
-}
-
-function launcherGroupsCacheKey(
- orgId: string,
- userId: string,
- queryHash: string
-) {
- return `launcher:groups:${orgId}:${userId}:${queryHash}`;
-}
-
-function launcherAccessibleIdsCacheKey(
- orgId: string,
- userId: string,
- roleIds: number[]
-) {
- const rolesKey = [...roleIds].sort((a, b) => a - b).join(",");
- return `launcherAccessibleIds:${orgId}:${userId}:${rolesKey}`;
-}
-
-async function resolveAccessibleIdsUncached(
- orgId: string,
- userId: string,
- userRoleIds: number[]
-): Promise {
- const [
- directResources,
- roleResourceResults,
- directPolicyResourceResults,
- rolePolicyResourceResults,
- directSiteResourceResults,
- roleSiteResourceResults
- ] = await Promise.all([
- db
- .select({ resourceId: userResources.resourceId })
- .from(userResources)
- .innerJoin(
- resources,
- eq(userResources.resourceId, resources.resourceId)
- )
- .where(
- and(
- eq(userResources.userId, userId),
- eq(resources.orgId, orgId)
- )
- ),
- userRoleIds.length > 0
- ? db
- .select({ resourceId: roleResources.resourceId })
- .from(roleResources)
- .innerJoin(
- resources,
- eq(roleResources.resourceId, resources.resourceId)
- )
- .where(
- and(
- inArray(roleResources.roleId, userRoleIds),
- eq(resources.orgId, orgId)
- )
- )
- : Promise.resolve([]),
- db
- .select({ resourceId: resources.resourceId })
- .from(resources)
- .innerJoin(
- userPolicies,
- eq(effectiveResourcePolicyId, userPolicies.resourcePolicyId)
- )
- .where(
- and(eq(userPolicies.userId, userId), eq(resources.orgId, orgId))
- ),
- userRoleIds.length > 0
- ? db
- .select({ resourceId: resources.resourceId })
- .from(resources)
- .innerJoin(
- rolePolicies,
- eq(
- effectiveResourcePolicyId,
- rolePolicies.resourcePolicyId
- )
- )
- .where(
- and(
- inArray(rolePolicies.roleId, userRoleIds),
- eq(resources.orgId, orgId)
- )
- )
- : Promise.resolve([]),
- db
- .select({ siteResourceId: userSiteResources.siteResourceId })
- .from(userSiteResources)
- .where(eq(userSiteResources.userId, userId)),
- userRoleIds.length > 0
- ? db
- .select({
- siteResourceId: roleSiteResources.siteResourceId
- })
- .from(roleSiteResources)
- .where(inArray(roleSiteResources.roleId, userRoleIds))
- : Promise.resolve([])
- ]);
-
- return {
- resourceIds: Array.from(
- new Set([
- ...directResources.map((r) => r.resourceId),
- ...roleResourceResults.map((r) => r.resourceId),
- ...directPolicyResourceResults.map((r) => r.resourceId),
- ...rolePolicyResourceResults.map((r) => r.resourceId)
- ])
- ),
- siteResourceIds: Array.from(
- new Set([
- ...directSiteResourceResults.map((r) => r.siteResourceId),
- ...roleSiteResourceResults.map((r) => r.siteResourceId)
- ])
- )
- };
-}
-
-export async function resolveAccessibleIds(
- orgId: string,
- userId: string,
- userRoleIds: number[]
-): Promise {
- const cacheKey = launcherAccessibleIdsCacheKey(orgId, userId, userRoleIds);
- const cached = await cache.get(cacheKey);
- if (cached) {
- return cached;
- }
-
- const result = await resolveAccessibleIdsUncached(
- orgId,
- userId,
- userRoleIds
- );
- await cache.set(cacheKey, result, LAUNCHER_ACCESSIBLE_IDS_TTL_SEC);
- return result;
-}
-
-function searchPattern(query: string) {
- return `%${query.trim()}%`;
-}
-
-function combineOrConditions(
- ...conditions: (SQL | undefined)[]
-): SQL | undefined {
- const parts = conditions.filter(
- (condition): condition is SQL => !!condition
- );
- if (parts.length === 0) {
- return undefined;
- }
- if (parts.length === 1) {
- return parts[0];
- }
- return or(...parts);
-}
-
-function buildSearchConditionForPublic(query: string) {
- if (!query.trim()) {
- return undefined;
- }
- const pattern = searchPattern(query.toLowerCase());
- const queryList = [
- like(sql`LOWER(${resources.name})`, pattern),
- like(sql`LOWER(${resources.fullDomain})`, pattern),
- like(sql`LOWER(cast(${resources.proxyPort} as text))`, pattern),
- inArray(
- resources.resourceId,
- db
- .select({ id: resources.resourceId })
- .from(resources)
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
- .leftJoin(sites, eq(targets.siteId, sites.siteId))
- .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
- .where(like(sql`LOWER(${exitNodes.endpoint})`, pattern))
- ),
- inArray(
- resources.resourceId,
- db
- .select({ id: resourceLabels.resourceId })
- .from(resourceLabels)
- .innerJoin(labels, eq(labels.labelId, resourceLabels.labelId))
- .where(like(sql`LOWER(${labels.name})`, pattern))
- )
- ];
-
- return or(...queryList);
-}
-
-function buildSearchConditionForSiteResource(query: string) {
- if (!query.trim()) {
- return undefined;
- }
- const pattern = searchPattern(query.toLowerCase());
- const queryList = [
- like(sql`LOWER(${siteResources.name})`, pattern),
- like(sql`LOWER(${siteResources.destination})`, pattern),
- like(
- sql`LOWER(cast(${siteResources.destinationPort} as text))`,
- pattern
- ),
- like(sql`LOWER(${siteResources.scheme})`, pattern),
- like(sql`LOWER(${siteResources.alias})`, pattern),
- like(sql`LOWER(${siteResources.fullDomain})`, pattern),
- like(sql`LOWER(${siteResources.aliasAddress})`, pattern),
- inArray(
- siteResources.siteResourceId,
- db
- .select({ id: siteResourceLabels.siteResourceId })
- .from(siteResourceLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, siteResourceLabels.labelId)
- )
- .where(like(sql`LOWER(${labels.name})`, pattern))
- )
- ];
-
- return or(...queryList);
-}
-
-async function filterPublicResourceIdsByTextSearch(
- orgId: string,
- resourceIds: number[],
- query: string
-): Promise {
- if (!query.trim() || resourceIds.length === 0) {
- return resourceIds;
- }
-
- const textMatch = combineOrConditions(
- buildSearchConditionForPublic(query),
- buildSiteNameSearchCondition(query)
- );
- if (!textMatch) {
- return resourceIds;
- }
-
- const rows = await db
- .selectDistinct({ resourceId: resources.resourceId })
- .from(resources)
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
- .leftJoin(sites, eq(targets.siteId, sites.siteId))
- .where(
- and(
- inArray(resources.resourceId, resourceIds),
- eq(resources.orgId, orgId),
- eq(resources.enabled, true),
- textMatch
- )
- );
-
- return rows.map((row) => row.resourceId);
-}
-
-async function filterSiteResourceIdsByTextSearch(
- orgId: string,
- siteResourceIds: number[],
- query: string
-): Promise {
- if (!query.trim() || siteResourceIds.length === 0) {
- return siteResourceIds;
- }
-
- const textMatch = combineOrConditions(
- buildSearchConditionForSiteResource(query),
- buildSiteNameSearchCondition(query)
- );
- if (!textMatch) {
- return siteResourceIds;
- }
-
- const rows = await db
- .selectDistinct({ siteResourceId: siteResources.siteResourceId })
- .from(siteResources)
- .leftJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
- .where(
- and(
- inArray(siteResources.siteResourceId, siteResourceIds),
- eq(siteResources.orgId, orgId),
- eq(siteResources.enabled, true),
- textMatch
- )
- );
-
- return rows.map((row) => row.siteResourceId);
-}
-
-async function fetchLabelsForResources(
- orgId: string,
- resourceIds: number[],
- siteResourceIds: number[]
-): Promise<{
- byResourceId: Map;
- bySiteResourceId: Map;
-}> {
- const byResourceId = new Map();
- const bySiteResourceId = new Map();
-
- const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([
- resourceIds.length === 0
- ? Promise.resolve([])
- : db
- .select({
- resourceId: resourceLabels.resourceId,
- labelId: labels.labelId,
- name: labels.name,
- color: labels.color
- })
- .from(resourceLabels)
- .innerJoin(labels, eq(resourceLabels.labelId, labels.labelId))
- .where(inArray(resourceLabels.resourceId, resourceIds))
- .orderBy(asc(resourceLabels.resourceLabelId)),
- siteResourceIds.length === 0
- ? Promise.resolve([])
- : db
- .select({
- siteResourceId: siteResourceLabels.siteResourceId,
- labelId: labels.labelId,
- name: labels.name,
- color: labels.color
- })
- .from(siteResourceLabels)
- .innerJoin(
- labels,
- eq(siteResourceLabels.labelId, labels.labelId)
- )
- .where(
- inArray(
- siteResourceLabels.siteResourceId,
- siteResourceIds
- )
- )
- .orderBy(asc(siteResourceLabels.siteResourceLabelId))
- ]);
-
- for (const row of resourceLabelRows) {
- const list = byResourceId.get(row.resourceId) ?? [];
- list.push({
- labelId: row.labelId,
- name: row.name,
- color: row.color
- });
- byResourceId.set(row.resourceId, list);
- }
-
- for (const row of siteResourceLabelRows) {
- const list = bySiteResourceId.get(row.siteResourceId) ?? [];
- list.push({
- labelId: row.labelId,
- name: row.name,
- color: row.color
- });
- bySiteResourceId.set(row.siteResourceId, list);
- }
-
- return { byResourceId, bySiteResourceId };
-}
-
-type SiteGroupRow = {
- siteId: number;
- name: string;
- type: string;
- online: boolean;
- itemCount: number;
-};
-
-async function listSiteGroups(
- orgId: string,
- accessible: AccessibleIds,
- query: LauncherListQuery
-): Promise<{ groups: LauncherGroup[]; total: number }> {
- const siteFilterIds = parseIdListParam(query.siteIds);
- const labelFilterIds = parseIdListParam(query.labelIds);
- const searchPublic = buildSearchConditionForPublic(query.query);
- const searchSite = buildSearchConditionForSiteResource(query.query);
- const siteCountMap = new Map();
-
- if (accessible.resourceIds.length > 0) {
- const publicConditions = [
- inArray(resources.resourceId, accessible.resourceIds),
- eq(resources.orgId, orgId),
- eq(resources.enabled, true)
- ];
- if (searchPublic) {
- publicConditions.push(searchPublic);
- }
- if (siteFilterIds.length > 0) {
- publicConditions.push(inArray(targets.siteId, siteFilterIds));
- }
-
- let publicQuery = db
- .select({
- siteId: sites.siteId,
- name: sites.name,
- type: sites.type,
- online: sites.online,
- itemCount: countDistinct(resources.resourceId)
- })
- .from(targets)
- .innerJoin(resources, eq(targets.resourceId, resources.resourceId))
- .innerJoin(sites, eq(targets.siteId, sites.siteId));
-
- if (labelFilterIds.length > 0) {
- publicQuery = publicQuery.innerJoin(
- resourceLabels,
- eq(resourceLabels.resourceId, resources.resourceId)
- );
- publicConditions.push(
- inArray(resourceLabels.labelId, labelFilterIds)
- );
- }
-
- const publicRows = await publicQuery
- .where(and(...publicConditions))
- .groupBy(sites.siteId, sites.name, sites.type, sites.online);
-
- for (const row of publicRows) {
- const existing = siteCountMap.get(row.siteId);
- if (existing) {
- existing.itemCount += Number(row.itemCount);
- } else {
- siteCountMap.set(row.siteId, {
- siteId: row.siteId,
- name: row.name,
- type: row.type,
- online: row.online,
- itemCount: Number(row.itemCount)
- });
- }
- }
- }
-
- if (accessible.siteResourceIds.length > 0) {
- const siteConditions = [
- inArray(siteResources.siteResourceId, accessible.siteResourceIds),
- eq(siteResources.orgId, orgId),
- eq(siteResources.enabled, true)
- ];
- if (searchSite) {
- siteConditions.push(searchSite);
- }
- if (siteFilterIds.length > 0) {
- siteConditions.push(inArray(sites.siteId, siteFilterIds));
- }
-
- let siteResourceQuery = db
- .select({
- siteId: sites.siteId,
- name: sites.name,
- type: sites.type,
- online: sites.online,
- itemCount: countDistinct(siteResources.siteResourceId)
- })
- .from(siteResources)
- .innerJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .innerJoin(sites, eq(siteNetworks.siteId, sites.siteId));
-
- if (labelFilterIds.length > 0) {
- siteResourceQuery = siteResourceQuery.innerJoin(
- siteResourceLabels,
- eq(
- siteResourceLabels.siteResourceId,
- siteResources.siteResourceId
- )
- );
- siteConditions.push(
- inArray(siteResourceLabels.labelId, labelFilterIds)
- );
- }
-
- const siteRows = await siteResourceQuery
- .where(and(...siteConditions))
- .groupBy(sites.siteId, sites.name, sites.type, sites.online);
-
- for (const row of siteRows) {
- const existing = siteCountMap.get(row.siteId);
- if (existing) {
- existing.itemCount += Number(row.itemCount);
- } else {
- siteCountMap.set(row.siteId, {
- siteId: row.siteId,
- name: row.name,
- type: row.type,
- online: row.online,
- itemCount: Number(row.itemCount)
- });
- }
- }
- }
-
- let noSiteCount = 0;
-
- if (accessible.resourceIds.length > 0 && siteFilterIds.length === 0) {
- const noSitePublicConditions = [
- inArray(resources.resourceId, accessible.resourceIds),
- eq(resources.orgId, orgId),
- eq(resources.enabled, true)
- ];
- if (searchPublic) {
- noSitePublicConditions.push(searchPublic);
- }
-
- let noSitePublicQuery = db
- .select({
- itemCount: countDistinct(resources.resourceId)
- })
- .from(resources)
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId));
-
- if (labelFilterIds.length > 0) {
- noSitePublicQuery = noSitePublicQuery.innerJoin(
- resourceLabels,
- eq(resourceLabels.resourceId, resources.resourceId)
- );
- noSitePublicConditions.push(
- inArray(resourceLabels.labelId, labelFilterIds)
- );
- }
-
- const [noSitePublicRow] = await noSitePublicQuery.where(
- and(...noSitePublicConditions, isNull(targets.targetId))
- );
-
- noSiteCount += Number(noSitePublicRow?.itemCount ?? 0);
- }
-
- if (accessible.siteResourceIds.length > 0 && siteFilterIds.length === 0) {
- const noSiteSiteConditions = [
- inArray(siteResources.siteResourceId, accessible.siteResourceIds),
- eq(siteResources.orgId, orgId),
- eq(siteResources.enabled, true)
- ];
- if (searchSite) {
- noSiteSiteConditions.push(searchSite);
- }
-
- let noSiteSiteQuery = db
- .select({
- itemCount: countDistinct(siteResources.siteResourceId)
- })
- .from(siteResources)
- .leftJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId));
-
- if (labelFilterIds.length > 0) {
- noSiteSiteQuery = noSiteSiteQuery.innerJoin(
- siteResourceLabels,
- eq(
- siteResourceLabels.siteResourceId,
- siteResources.siteResourceId
- )
- );
- noSiteSiteConditions.push(
- inArray(siteResourceLabels.labelId, labelFilterIds)
- );
- }
-
- const [noSiteSiteRow] = await noSiteSiteQuery.where(
- and(...noSiteSiteConditions, isNull(sites.siteId))
- );
-
- noSiteCount += Number(noSiteSiteRow?.itemCount ?? 0);
- }
-
- let groups: LauncherGroup[] = Array.from(siteCountMap.values()).map(
- (row) => ({
- groupKey: String(row.siteId),
- name: row.name,
- groupType: "site" as const,
- itemCount: row.itemCount,
- siteType: row.type,
- siteOnline: row.online
- })
- );
-
- if (noSiteCount > 0 && siteFilterIds.length === 0) {
- groups.push({
- groupKey: LAUNCHER_NO_SITE_GROUP_KEY,
- name: "No Site",
- groupType: "site",
- itemCount: noSiteCount
- });
- }
-
- groups.sort((a, b) => {
- const cmp = a.name.localeCompare(b.name, undefined, {
- sensitivity: "base"
- });
- return query.order === "desc" ? -cmp : cmp;
- });
-
- const total = groups.length;
- return {
- groups,
- total
- };
-}
-
-async function listLabelGroups(
- orgId: string,
- accessible: AccessibleIds,
- query: LauncherListQuery
-): Promise<{ groups: LauncherGroup[]; total: number }> {
- const siteFilterIds = parseIdListParam(query.siteIds);
- const labelFilterIds = parseIdListParam(query.labelIds);
- const labelCountMap = new Map<
- number,
- { labelId: number; name: string; color: string; itemCount: number }
- >();
- let unlabeledCount = 0;
-
- const matchesLabelFilters = (labelId: number) =>
- labelFilterIds.length === 0 || labelFilterIds.includes(labelId);
-
- if (accessible.resourceIds.length > 0) {
- const publicConditions = [
- inArray(resources.resourceId, accessible.resourceIds),
- eq(resources.orgId, orgId),
- eq(resources.enabled, true)
- ];
- const searchPublic = buildSearchConditionForPublic(query.query);
- if (searchPublic) {
- publicConditions.push(searchPublic);
- }
- if (siteFilterIds.length > 0) {
- publicConditions.push(inArray(targets.siteId, siteFilterIds));
- }
-
- const labeledPublic = await db
- .select({
- labelId: labels.labelId,
- name: labels.name,
- color: labels.color,
- itemCount: countDistinct(resources.resourceId)
- })
- .from(resourceLabels)
- .innerJoin(labels, eq(resourceLabels.labelId, labels.labelId))
- .innerJoin(
- resources,
- eq(resourceLabels.resourceId, resources.resourceId)
- )
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
- .where(and(...publicConditions, eq(labels.orgId, orgId)))
- .groupBy(labels.labelId, labels.name, labels.color);
-
- for (const row of labeledPublic) {
- if (!matchesLabelFilters(row.labelId)) {
- continue;
- }
- const existing = labelCountMap.get(row.labelId);
- if (existing) {
- existing.itemCount += Number(row.itemCount);
- } else {
- labelCountMap.set(row.labelId, {
- labelId: row.labelId,
- name: row.name,
- color: row.color,
- itemCount: Number(row.itemCount)
- });
- }
- }
-
- const labeledPublicIds = await db
- .select({ resourceId: resourceLabels.resourceId })
- .from(resourceLabels)
- .innerJoin(
- resources,
- eq(resourceLabels.resourceId, resources.resourceId)
- )
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
- .where(and(...publicConditions));
-
- const labeledSet = new Set(labeledPublicIds.map((r) => r.resourceId));
- unlabeledCount += accessible.resourceIds.filter(
- (id) => !labeledSet.has(id)
- ).length;
- }
-
- if (accessible.siteResourceIds.length > 0) {
- const siteConditions = [
- inArray(siteResources.siteResourceId, accessible.siteResourceIds),
- eq(siteResources.orgId, orgId),
- eq(siteResources.enabled, true)
- ];
- const searchSite = buildSearchConditionForSiteResource(query.query);
- if (searchSite) {
- siteConditions.push(searchSite);
- }
- if (siteFilterIds.length > 0) {
- siteConditions.push(inArray(sites.siteId, siteFilterIds));
- }
-
- const labeledSite = await db
- .select({
- labelId: labels.labelId,
- name: labels.name,
- color: labels.color,
- itemCount: countDistinct(siteResources.siteResourceId)
- })
- .from(siteResourceLabels)
- .innerJoin(labels, eq(siteResourceLabels.labelId, labels.labelId))
- .innerJoin(
- siteResources,
- eq(
- siteResourceLabels.siteResourceId,
- siteResources.siteResourceId
- )
- )
- .leftJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
- .where(and(...siteConditions, eq(labels.orgId, orgId)))
- .groupBy(labels.labelId, labels.name, labels.color);
-
- for (const row of labeledSite) {
- if (!matchesLabelFilters(row.labelId)) {
- continue;
- }
- const existing = labelCountMap.get(row.labelId);
- if (existing) {
- existing.itemCount += Number(row.itemCount);
- } else {
- labelCountMap.set(row.labelId, {
- labelId: row.labelId,
- name: row.name,
- color: row.color,
- itemCount: Number(row.itemCount)
- });
- }
- }
-
- const labeledSiteIds = await db
- .select({ siteResourceId: siteResourceLabels.siteResourceId })
- .from(siteResourceLabels)
- .innerJoin(
- siteResources,
- eq(
- siteResourceLabels.siteResourceId,
- siteResources.siteResourceId
- )
- )
- .leftJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
- .where(and(...siteConditions));
-
- const labeledSet = new Set(labeledSiteIds.map((r) => r.siteResourceId));
- unlabeledCount += accessible.siteResourceIds.filter(
- (id) => !labeledSet.has(id)
- ).length;
- }
-
- let groups: LauncherGroup[] = Array.from(labelCountMap.values()).map(
- (row) => ({
- groupKey: String(row.labelId),
- name: row.name,
- groupType: "label" as const,
- itemCount: row.itemCount,
- labelColor: row.color
- })
- );
-
- if (unlabeledCount > 0 && labelFilterIds.length === 0) {
- groups.push({
- groupKey: LAUNCHER_UNLABELED_GROUP_KEY,
- name: "Unlabeled",
- groupType: "label",
- itemCount: unlabeledCount,
- labelColor: "#a1a1aa"
- });
- }
-
- groups.sort((a, b) => {
- const cmp = a.name.localeCompare(b.name, undefined, {
- sensitivity: "base"
- });
- return query.order === "desc" ? -cmp : cmp;
- });
-
- const total = groups.length;
- return {
- groups,
- total
- };
-}
-
-async function listLauncherGroupsForUserUncached(
- orgId: string,
- userId: string,
- userRoleIds: number[],
- query: LauncherListQuery
-): Promise {
- const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
-
- if (query.groupBy === "label") {
- return listLabelGroups(orgId, accessible, query);
- }
-
- return listSiteGroups(orgId, accessible, query);
-}
-
-export async function listLauncherGroupsForUser(
- orgId: string,
- userId: string,
- userRoleIds: number[],
- query: LauncherListQuery
-): Promise<{ groups: LauncherGroup[]; total: number }> {
- const queryHash = launcherGroupsQueryHash(userRoleIds, query);
- const cacheKey = launcherGroupsCacheKey(orgId, userId, queryHash);
- const cached = await cache.get(cacheKey);
-
- let result = cached;
- if (!result) {
- result = await listLauncherGroupsForUserUncached(
- orgId,
- userId,
- userRoleIds,
- query
- );
- await cache.set(cacheKey, result, LAUNCHER_GROUPS_RESULT_TTL_SEC);
- }
-
- const offset = (query.page - 1) * query.pageSize;
- return {
- groups: result.groups.slice(offset, offset + query.pageSize),
- total: result.total
- };
-}
-
-async function mapPublicResources(
- orgId: string,
- resourceIds: number[],
- labelMaps: Awaited>,
- siteIdFilter?: number
-): Promise {
- if (resourceIds.length === 0) {
- return [];
- }
-
- const rows = await db
- .select({
- resourceId: resources.resourceId,
- niceId: resources.niceId,
- name: resources.name,
- mode: resources.mode,
- fullDomain: resources.fullDomain,
- ssl: resources.ssl,
- proxyPort: resources.proxyPort,
- wildcard: resources.wildcard,
- enabled: resources.enabled,
- siteId: sites.siteId,
- siteName: sites.name,
- siteType: sites.type,
- siteOnline: sites.online,
- exitNodeEndpoint: exitNodes.endpoint
- })
- .from(resources)
- .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
- .leftJoin(sites, eq(targets.siteId, sites.siteId))
- .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
- .where(
- and(
- inArray(resources.resourceId, resourceIds),
- eq(resources.orgId, orgId),
- eq(resources.enabled, true),
- siteIdFilter != null
- ? eq(sites.siteId, siteIdFilter)
- : undefined
- )
- );
-
- const seen = new Set();
- const result: LauncherResource[] = [];
-
- for (const row of rows) {
- const key = `public:${row.resourceId}`;
- if (seen.has(key)) {
- continue;
- }
- seen.add(key);
-
- const access = formatPublicResourceAccess({
- mode: row.mode,
- fullDomain: row.fullDomain,
- ssl: row.ssl,
- proxyPort: row.proxyPort,
- wildcard: row.wildcard,
- exitNodeEndpoint: row.exitNodeEndpoint
- });
-
- result.push({
- launcherResourceKey: key,
- resourceType: "public",
- resourceId: row.resourceId,
- niceId: row.niceId,
- name: row.name,
- ...access,
- iconUrl: null,
- enabled: row.enabled,
- mode: row.mode,
- labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
- site:
- row.siteId != null
- ? {
- siteId: row.siteId,
- name: row.siteName!,
- type: row.siteType!,
- online: row.siteOnline ?? undefined
- }
- : undefined
- });
- }
-
- return result;
-}
-
-async function mapSiteResources(
- orgId: string,
- siteResourceIds: number[],
- labelMaps: Awaited>,
- siteIdFilter?: number
-): Promise {
- if (siteResourceIds.length === 0) {
- return [];
- }
-
- const rows = await db
- .select({
- siteResourceId: siteResources.siteResourceId,
- niceId: siteResources.niceId,
- name: siteResources.name,
- mode: siteResources.mode,
- destination: siteResources.destination,
- destinationPort: siteResources.destinationPort,
- scheme: siteResources.scheme,
- ssl: siteResources.ssl,
- fullDomain: siteResources.fullDomain,
- alias: siteResources.alias,
- aliasAddress: siteResources.aliasAddress,
- enabled: siteResources.enabled,
- siteId: sites.siteId,
- siteName: sites.name,
- siteType: sites.type,
- siteOnline: sites.online
- })
- .from(siteResources)
- .leftJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
- .where(
- and(
- inArray(siteResources.siteResourceId, siteResourceIds),
- eq(siteResources.orgId, orgId),
- eq(siteResources.enabled, true),
- siteIdFilter != null
- ? eq(sites.siteId, siteIdFilter)
- : undefined
- )
- );
-
- const seen = new Set();
- const result: LauncherResource[] = [];
-
- for (const row of rows) {
- const key = `site:${row.siteResourceId}`;
- if (seen.has(key)) {
- continue;
- }
- seen.add(key);
-
- const access = formatSiteResourceAccess({
- mode: row.mode,
- destination: row.destination,
- destinationPort: row.destinationPort,
- scheme: row.scheme,
- ssl: row.ssl,
- fullDomain: row.fullDomain,
- alias: row.alias,
- aliasAddress: row.aliasAddress
- });
-
- result.push({
- launcherResourceKey: key,
- resourceType: "site",
- resourceId: row.siteResourceId,
- siteResourceId: row.siteResourceId,
- niceId: row.niceId,
- name: row.name,
- ...access,
- iconUrl: null,
- enabled: row.enabled,
- mode: row.mode,
- labels: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
- site:
- row.siteId != null
- ? {
- siteId: row.siteId,
- name: row.siteName!,
- type: row.siteType!,
- online: row.siteOnline ?? undefined
- }
- : undefined
- });
- }
-
- return result;
-}
-
-function filterResourcesBySite(
- items: LauncherResource[],
- groupKey: string
-): LauncherResource[] {
- if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
- return items.filter((item) => !item.site);
- }
- const siteId = Number.parseInt(groupKey, 10);
- if (!Number.isFinite(siteId)) {
- return items;
- }
- return items.filter((item) => item.site?.siteId === siteId);
-}
-
-function filterResourcesByLabel(
- items: LauncherResource[],
- groupKey: string
-): LauncherResource[] {
- if (groupKey === LAUNCHER_UNLABELED_GROUP_KEY) {
- return items.filter((item) => item.labels.length === 0);
- }
- const labelId = Number.parseInt(groupKey, 10);
- return items.filter((item) =>
- item.labels.some((label) => label.labelId === labelId)
- );
-}
-
-function sortLauncherResources(
- items: LauncherResource[],
- order: "asc" | "desc"
-): LauncherResource[] {
- return [...items].sort((a, b) => {
- const cmp = a.name.localeCompare(b.name, undefined, {
- sensitivity: "base"
- });
- return order === "desc" ? -cmp : cmp;
- });
-}
-
-async function listLauncherResourcesForUserUncached(
- orgId: string,
- userId: string,
- userRoleIds: number[],
- query: LauncherListQuery & { groupKey: string }
-): Promise {
- const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
-
- const siteFilterIds = parseIdListParam(query.siteIds);
- const labelFilterIds = parseIdListParam(query.labelIds);
-
- let filteredResourceIds = accessible.resourceIds;
- let filteredSiteResourceIds = accessible.siteResourceIds;
-
- if (siteFilterIds.length > 0 && accessible.resourceIds.length > 0) {
- const publicOnSites = await db
- .select({ resourceId: resources.resourceId })
- .from(targets)
- .innerJoin(resources, eq(targets.resourceId, resources.resourceId))
- .where(
- and(
- inArray(resources.resourceId, accessible.resourceIds),
- inArray(targets.siteId, siteFilterIds)
- )
- );
- filteredResourceIds = publicOnSites.map((r) => r.resourceId);
- }
-
- if (siteFilterIds.length > 0 && accessible.siteResourceIds.length > 0) {
- const privateOnSites = await db
- .select({ siteResourceId: siteResources.siteResourceId })
- .from(siteResources)
- .innerJoin(
- siteNetworks,
- eq(siteResources.networkId, siteNetworks.networkId)
- )
- .where(
- and(
- inArray(
- siteResources.siteResourceId,
- accessible.siteResourceIds
- ),
- inArray(siteNetworks.siteId, siteFilterIds)
- )
- );
- filteredSiteResourceIds = privateOnSites.map((r) => r.siteResourceId);
- }
-
- if (labelFilterIds.length > 0) {
- if (filteredResourceIds.length > 0) {
- const withLabels = await db
- .select({ resourceId: resourceLabels.resourceId })
- .from(resourceLabels)
- .where(
- and(
- inArray(resourceLabels.resourceId, filteredResourceIds),
- inArray(resourceLabels.labelId, labelFilterIds)
- )
- );
- filteredResourceIds = withLabels.map((r) => r.resourceId);
- }
- if (filteredSiteResourceIds.length > 0) {
- const withLabels = await db
- .select({ siteResourceId: siteResourceLabels.siteResourceId })
- .from(siteResourceLabels)
- .where(
- and(
- inArray(
- siteResourceLabels.siteResourceId,
- filteredSiteResourceIds
- ),
- inArray(siteResourceLabels.labelId, labelFilterIds)
- )
- );
- filteredSiteResourceIds = withLabels.map((r) => r.siteResourceId);
- }
- }
-
- if (query.query.trim()) {
- if (filteredResourceIds.length > 0) {
- filteredResourceIds = await filterPublicResourceIdsByTextSearch(
- orgId,
- filteredResourceIds,
- query.query
- );
- }
- if (filteredSiteResourceIds.length > 0) {
- filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch(
- orgId,
- filteredSiteResourceIds,
- query.query
- );
- }
- }
-
- const labelMaps = await fetchLabelsForResources(
- orgId,
- filteredResourceIds,
- filteredSiteResourceIds
- );
-
- const parsedSiteId =
- query.groupBy === "site" &&
- query.groupKey !== LAUNCHER_NO_SITE_GROUP_KEY
- ? Number.parseInt(query.groupKey, 10)
- : Number.NaN;
- const siteIdFilter = Number.isFinite(parsedSiteId)
- ? parsedSiteId
- : undefined;
-
- const [publicItems, siteItems] = await Promise.all([
- mapPublicResources(
- orgId,
- filteredResourceIds,
- labelMaps,
- Number.isFinite(siteIdFilter) ? siteIdFilter : undefined
- ),
- mapSiteResources(
- orgId,
- filteredSiteResourceIds,
- labelMaps,
- Number.isFinite(siteIdFilter) ? siteIdFilter : undefined
- )
- ]);
-
- let items = [...publicItems, ...siteItems];
-
- if (query.groupKey !== LAUNCHER_FLAT_GROUP_KEY) {
- if (query.groupBy === "label") {
- items = filterResourcesByLabel(items, query.groupKey);
- } else if (query.groupBy === "site") {
- items = filterResourcesBySite(items, query.groupKey);
- }
- }
-
- items = sortLauncherResources(items, query.order);
-
- return {
- items,
- total: items.length
- };
-}
-
-export async function listLauncherResourcesForUser(
- orgId: string,
- userId: string,
- userRoleIds: number[],
- query: LauncherListQuery & { groupKey: string }
-): Promise<{ resources: LauncherResource[]; total: number }> {
- const queryHash = launcherResourcesQueryHash(userRoleIds, query);
- const cacheKey = launcherResourcesCacheKey(orgId, userId, queryHash);
- const cached = await cache.get(cacheKey);
-
- let result = cached;
- if (!result) {
- result = await listLauncherResourcesForUserUncached(
- orgId,
- userId,
- userRoleIds,
- query
- );
- await cache.set(cacheKey, result, LAUNCHER_RESOURCES_RESULT_TTL_SEC);
- }
-
- const offset = (query.page - 1) * query.pageSize;
- return {
- resources: result.items.slice(offset, offset + query.pageSize),
- total: result.total
- };
-}
-
-function buildSiteNameSearchCondition(query: string) {
- if (!query.trim()) {
- return undefined;
- }
- const pattern = searchPattern(query.toLowerCase());
- return or(
- like(sql`LOWER(${sites.name})`, pattern),
- like(sql`LOWER(${sites.niceId})`, pattern)
- );
-}
-
-function buildLabelNameSearchCondition(query: string) {
- if (!query.trim()) {
- return undefined;
- }
- const pattern = searchPattern(query.toLowerCase());
- return like(sql`LOWER(${labels.name})`, pattern);
-}
-
-async function collectAccessibleSites(
- orgId: string,
- accessible: AccessibleIds,
- siteNameSearch?: ReturnType
-): Promise