mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 06:39:52 +02:00
Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d13e9105c | |||
| 289be30e6b | |||
| a74c0c227c | |||
| 05bf77da29 | |||
| bb4deb1ae9 | |||
| 1bf3d2cdd6 | |||
| 5fc5a3ebca | |||
| e40f325703 | |||
| 8f377a4fb2 | |||
| 3699f8f9cb | |||
| 5a2388a1e6 | |||
| 86e6ebc8af | |||
| 005f050a81 | |||
| c82678852e | |||
| 2e3ab10f5e | |||
| 4985ed02d3 | |||
| a2fea7f714 | |||
| ddba2aff21 | |||
| da9e668fb8 | |||
| 9c2d14d8c6 | |||
| c383e74df4 | |||
| 40101f8bb0 | |||
| b20ed9efa9 | |||
| 56266e3b62 | |||
| 47eff8c948 | |||
| 15f36096ef | |||
| 064252586d | |||
| 6181d46a1e | |||
| 9747731668 | |||
| 2a478eef6f | |||
| 4ab101f8a9 | |||
| e35878ee55 | |||
| 807613f28c | |||
| 663244fa3a | |||
| bcc128aeb6 | |||
| ba33cb9895 | |||
| 69e7fedcfc | |||
| 023110b341 | |||
| 7fb95e1726 | |||
| db0a7cc1ce | |||
| 61fc2e5ea7 | |||
| 0871a211ec | |||
| 5a1d5cb66e | |||
| 5a7ca5b542 | |||
| 87e1a509ce | |||
| 75f481bc3d | |||
| 97cdb2eb5a | |||
| 297fd2caf3 | |||
| 22dd4220fe | |||
| 108cb6216c | |||
| e3ef592778 | |||
| 3c37e10638 | |||
| 561f75b6b1 | |||
| e98bcb83ac | |||
| 80284863bb | |||
| bc759c5c9e | |||
| f4854a3a74 | |||
| 376dd465b3 | |||
| 296439fd67 | |||
| 31f675f38c | |||
| 9f68be2a9b | |||
| fed4ec42c4 | |||
| f0efa4203b | |||
| cfbbdedaf5 | |||
| 686789ee4c | |||
| 3fda190ff6 | |||
| 0033f40f4d | |||
| af95052706 | |||
| 9bb2d6cdc8 | |||
| 29563a13a4 | |||
| b41c1f5b27 | |||
| e5652cdb8a | |||
| 7c2ea153c5 | |||
| ccabddc225 | |||
| 42d98fa83b | |||
| 2f2b7f43c1 | |||
| 528bbeca26 | |||
| d60c15b0ae | |||
| ff89a64453 | |||
| 4718c489d3 | |||
| d5d99a4804 | |||
| 9c18936be7 | |||
| cf07cceb5d | |||
| faee9e6330 | |||
| 31725eb3cc | |||
| 04d4e298e8 | |||
| c9cc9581b1 | |||
| eac7c67dcc | |||
| 633d9031af | |||
| 05dc558c4a | |||
| 7506c0420d | |||
| 5572822c4a | |||
| ea3f1c341b | |||
| 35dffe71cb | |||
| 5428bf4ed0 | |||
| 9a89579e08 | |||
| 784588cebc | |||
| 2e628fe0e4 | |||
| 7590e8d8a1 | |||
| 053ff1e799 | |||
| c5ffca499e | |||
| 9ae54f445d | |||
| 8183d19400 | |||
| 7425daad3f | |||
| 39d61a35eb | |||
| f3fe11c136 | |||
| 66b1b385a3 | |||
| 822a07d48e | |||
| 3c13b1ea15 | |||
| fee635b861 | |||
| 7e4dea918a | |||
| 56187d61d5 | |||
| 36460d4cc0 | |||
| 88a9b92dc3 | |||
| dd26518d6f | |||
| 60339706bb | |||
| 4b1b3d3d5b | |||
| cf21bacd9c | |||
| 80d257b94b | |||
| e0d0c5dcbf | |||
| 0f02d1bc02 | |||
| f8591f27c5 | |||
| 877985deb3 | |||
| be3877a3ce | |||
| 79de64dc07 | |||
| d0defa380a | |||
| 4eba51de72 | |||
| a48032adb3 | |||
| 6fe4eee336 | |||
| 242123b875 | |||
| 2b38658ea6 | |||
| b18a41e4aa | |||
| d303fa05cb | |||
| 75b87ffba7 | |||
| 62fc2edae9 | |||
| 80b66cf9b9 | |||
| 034bcbd271 | |||
| bc63747efe | |||
| bb7729df00 | |||
| 2a8ceeec1b | |||
| 91ef0d0153 | |||
| e104489257 | |||
| b8101402cd | |||
| 7731849a2f | |||
| c11d24e10a | |||
| a9b7cce49b | |||
| d78223b94f | |||
| 963e9da7dd | |||
| 2cbc88fa05 | |||
| 65bad456cb | |||
| f48a4f7bc0 | |||
| ce3c2f7583 | |||
| 51c357e6c7 | |||
| 7ae29612d4 | |||
| da794adb7d | |||
| 8004ae6870 | |||
| 1bff7bbc2f | |||
| 50db5695fc | |||
| babd90ae71 | |||
| f7050ef989 | |||
| b2778a2c49 | |||
| 096940a152 | |||
| 73eb07de71 | |||
| 74ef844e27 | |||
| c58968536d | |||
| 228efacfe0 | |||
| 1262030abb | |||
| b07fe6d18b | |||
| 63c3ee623b | |||
| 18ec6c8d92 | |||
| d8acccbde4 | |||
| 37eaf34e4d | |||
| cfb63f9742 | |||
| c76b4555e1 | |||
| c25bfbad27 | |||
| 44782f8963 | |||
| e6f7cd6da9 | |||
| 19faa3a29c | |||
| c284dc2e83 | |||
| 1b634955d8 | |||
| be888c3fc1 | |||
| 3f2bb42221 | |||
| 5dc3ae4c7f | |||
| ffb6c64de0 | |||
| 2cbc6fb128 | |||
| 75084028d7 | |||
| f44a7c55dd | |||
| 72fa1d6a14 | |||
| c3820a4e70 | |||
| 6b56c00782 | |||
| 60c1b572ba | |||
| 604dee9aa5 | |||
| ee42846c90 | |||
| 22ac711dc6 | |||
| d09668b20b | |||
| 16abe98fd9 | |||
| d240201361 | |||
| b7081aff11 | |||
| a55fb21e53 | |||
| e5e7b79712 | |||
| de48a0529e | |||
| 3f37408dae | |||
| a2882857ff | |||
| 476d92b3ac | |||
| bf604f25e9 | |||
| 34a0d2a68b | |||
| 62c7e0a13e | |||
| 753358a17d | |||
| c859393418 | |||
| d747b45f0b | |||
| a24091257a | |||
| 1c60041390 | |||
| 95c3f74a33 | |||
| 16c0f4eef4 | |||
| a08c6d70fe | |||
| a6568692b7 | |||
| a1196d3da6 | |||
| 70bc4c0b30 | |||
| a0fef89031 | |||
| ea1badf4e0 | |||
| f15654ed11 | |||
| 4435a669a6 | |||
| 0b41fe3d49 | |||
| 90eceb457a | |||
| f39cbc9bf4 | |||
| 50da863bb7 | |||
| c6ddd5c402 | |||
| 0fb5ace9c7 | |||
| cedccd8cdb | |||
| b136bd2246 | |||
| 7a275c86c2 | |||
| 4b703b5c11 | |||
| 1b6e9e8cfe | |||
| fe55956079 | |||
| 4cd0b9a0bb | |||
| ab4d567af9 | |||
| 38203e522b | |||
| 92d611df9a | |||
| 13b691fd7d | |||
| 89f3f3c8cd | |||
| 44c16d69af |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
Don't write or edit migrations in `server/setup` unless specificall instructed to do so.
|
||||
+19
-29
@@ -1,52 +1,42 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 1
|
||||
groups:
|
||||
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"
|
||||
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 1
|
||||
groups:
|
||||
patch-updates:
|
||||
update-types:
|
||||
- "patch"
|
||||
minor-updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
docker-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- 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:
|
||||
patch-updates:
|
||||
update-types:
|
||||
- "patch"
|
||||
minor-updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
go-install-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make dev-build-pg
|
||||
|
||||
Vendored
+4
-1
@@ -18,5 +18,8 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"cSpell.words": [
|
||||
"nessicary"
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -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.43.0
|
||||
golang.org/x/term v0.44.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.44.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
||||
|
||||
+4
-4
@@ -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.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/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/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=
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"local": "Локална",
|
||||
"edit": "Редактиране",
|
||||
"siteConfirmDelete": "Потвърждение на изтриване на сайта",
|
||||
"siteConfirmDeleteAndResources": "Потвърдете изтриването на сайта и ресурсите",
|
||||
"siteDelete": "Изтриване на сайта",
|
||||
"siteDeleteAndResources": "Изтриване на сайта и ресурсите",
|
||||
"siteMessageRemove": "След премахване, сайтът вече няма да бъде достъпен. Всички цели, свързани със сайта, също ще бъдат премахнати.",
|
||||
"siteMessageRemoveAndResources": "Това ще изтрие окончателно всички публични и частни ресурси, свързани с този сайт, дори ако ресурсът е асоцииран и с други сайтове.",
|
||||
"siteQuestionRemove": "Сигурни ли сте, че искате да премахнете сайта от организацията?",
|
||||
"siteQuestionRemoveAndResources": "Наистина ли желаете да изтриете този сайт и всички свързани ресурси?",
|
||||
"sitesTableDeleteSite": "Изтриване на сайта",
|
||||
"sitesTableDeleteSiteAndResources": "Изтриване на сайта и ресурсите",
|
||||
"siteManageSites": "Управление на сайтове",
|
||||
"siteDescription": "Създайте и управлявайте сайтове, за да осигурите свързаност със частни мрежи",
|
||||
"sitesBannerTitle": "Свържете се с мрежа.",
|
||||
@@ -117,6 +123,16 @@
|
||||
"siteUpdated": "Сайтът е обновен",
|
||||
"siteUpdatedDescription": "Сайтът е актуализиран.",
|
||||
"siteGeneralDescription": "Конфигурирайте общи настройки за този сайт",
|
||||
"siteRestartTitle": "Рестартирайте Сайта",
|
||||
"siteRestartDescription": "Рестартирайте WireGuard тунела за този сайт. Това ще прекъсне връзката за кратко.",
|
||||
"siteRestartBody": "Използвайте това, ако тунелът на сайта не функционира правилно и искате да принудите повторно свързване без да рестартирате хоста.",
|
||||
"siteRestartButton": "Рестартирайте Сайта",
|
||||
"siteRestartDialogMessage": "Сигурни ли сте, че искате да рестартирате WireGuard тунела за <b>{name}</b>? Сайтът ще изгуби връзка за кратко.",
|
||||
"siteRestartWarning": "Сайтът ще се изключи за кратко, докато тунелът се рестартира.",
|
||||
"siteRestarted": "Сайтът е рестартиран",
|
||||
"siteRestartedDescription": "WireGuard тунелът е рестартиран.",
|
||||
"siteErrorRestart": "Неуспешно рестартиране на сайта",
|
||||
"siteErrorRestartDescription": "Възникна грешка при рестартирането на сайта.",
|
||||
"siteSettingDescription": "Конфигурирайте настройките на сайта",
|
||||
"siteResourcesTab": "Ресурси",
|
||||
"siteResourcesNoneOnSite": "Този сайт все още няма публични или частни ресурси.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"actionApplyBlueprint": "Приложи Чернова",
|
||||
"actionListBlueprints": "Списък с планове.",
|
||||
"actionGetBlueprint": "Вземи план.",
|
||||
"actionCreateOrgWideLauncherView": "Създайте Изглед на Стартирача за Цялата Организация",
|
||||
"setupToken": "Конфигурация на токен",
|
||||
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
|
||||
"setupTokenRequired": "Необходим е конфигурационен токен",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"subnetPlaceholder": "Мрежа",
|
||||
"addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.",
|
||||
"selectSites": "Избор на сайтове",
|
||||
"selectLabels": "Изберете етикети",
|
||||
"sitesDescription": "Клиентът ще има връзка с избраните сайтове",
|
||||
"clientInstallOlm": "Инсталиране на Olm",
|
||||
"clientInstallOlmDescription": "Конфигурирайте Olm да работи на вашата система",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"createInternalResourceDialogSite": "Сайт",
|
||||
"selectSite": "Изберете сайт...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, one {# сайт} other {# сайтове}}",
|
||||
"labelsSelectorLabelsCount": "{count, plural, one {# етикет} other {# етикета}}",
|
||||
"noSitesFound": "Не са намерени сайтове.",
|
||||
"createInternalResourceDialogProtocol": "Протокол",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.",
|
||||
"createInternalResourceDialogAlias": "Псевдоним",
|
||||
"createInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
|
||||
"internalResourceAliasLocalWarning": "Синоними с окончание .local могат да причинят проблеми с резолюцията поради mDNS в някои мрежи.",
|
||||
"internalResourceDownstreamSchemeRequired": "Методът е задължителен за HTTP ресурси",
|
||||
"internalResourceHttpPortRequired": "Портът към целта е задължителен за HTTP ресурси",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"sidebarRemoteExitNodes": "Отдалечени възли",
|
||||
"remoteExitNodeId": "ID.",
|
||||
"remoteExitNodeSecretKey": "Секретен ключ.",
|
||||
"remoteExitNodeNetworkingTitle": "Настройки на Мрежата",
|
||||
"remoteExitNodeNetworkingDescription": "Настройте как този отдалечен край маршрутизира трафика и кои сайтове предпочитат да се свържат чрез него. Усъвършенствани функции за използване при конфигурации на бекаул мрежи.",
|
||||
"remoteExitNodeNetworkingSave": "Запазване на Настройките",
|
||||
"remoteExitNodeNetworkingSaveSuccessTitle": "Настройките на мрежата са успешно запазени",
|
||||
"remoteExitNodeNetworkingSaveSuccessDescription": "Настройките на мрежата бяха успешно обновени.",
|
||||
"remoteExitNodeNetworkingSaveError": "Неуспешно запазване на мрежовите настройки",
|
||||
"remoteExitNodeNetworkingSubnetsTitle": "Отдалечени Подмрежи",
|
||||
"remoteExitNodeNetworkingSubnetsDescription": "Определете CIDR диапазоните, които този отдалечен край ще маршрутизира трафика към. Въведете валиден CIDR (e.g. <code>10.0.0.0/8</code>) и натиснете Enter, за да добавите.",
|
||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "Добавете CIDR диапазон (напр. 10.0.0.0/8)",
|
||||
"remoteExitNodeNetworkingSubnetsLoadError": "Неуспешно зареждане на подмрежи",
|
||||
"remoteExitNodeNetworkingLabelsTitle": "Етикети за Предпочитания",
|
||||
"remoteExitNodeNetworkingLabelsDescription": "Сайтове с тези етикети ще бъдат принудени да се свържат чрез този отдалечен край.",
|
||||
"remoteExitNodeNetworkingLabelsButtonText": "Изберете етикети...",
|
||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Търсене на етикети...",
|
||||
"remoteExitNodeNetworkingLabelsLoadError": "Неуспешно зареждане на етикети",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Създаване на отдалечен възел.",
|
||||
"description": "Създайте нов самохостнал отдалечен ретранслатор и прокси сървърен възел.",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC доставчик",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC доставчик",
|
||||
"subnet": "Подмрежа",
|
||||
"utilitySubnet": "Мрежа на Помощни Подмрежи",
|
||||
"subnetDescription": "Подмрежата за конфигурацията на мрежата на тази организация.",
|
||||
"customDomain": "Персонализиран домейн.",
|
||||
"authPage": "Страници за автентификация.",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"orgOrDomainIdMissing": "Липсва идентификатор на организация или домейн",
|
||||
"loadingDNSRecords": "Зареждане на DNS записи...",
|
||||
"olmUpdateAvailableInfo": "Налична е актуализирана версия на Olm. Моля, актуализирайте до най-новата версия за най-добро преживяване.",
|
||||
"updateAvailableInfo": "На разположение е обновена версия. Моля, обновете до най-новата версия за най-добър опит.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки на прокси протокол",
|
||||
"proxyProtocolDescription": "Конфигурирайте Proxy Protocol, за да запазите IP адресите на клиентите за TCP услуги.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Бял списък на имейли",
|
||||
"memberPortalResourceDisabled": "Ресурсът е деактивиран",
|
||||
"memberPortalShowingResources": "Показва {start}-{end} от {total} ресурси",
|
||||
"resourceLauncherTitle": "Стартер за Ресурси",
|
||||
"resourceLauncherDescription": "Преглеждайте детайлите на ресурса и ги стартирайте от едно място",
|
||||
"resourceLauncherSearchPlaceholder": "Търсете във всички сайтове...",
|
||||
"resourceLauncherDefaultView": "По Подразбиране",
|
||||
"resourceLauncherSaveView": "Запазете Изгледа",
|
||||
"resourceLauncherSaveToCurrentView": "Запазете в Текущ Изглед",
|
||||
"resourceLauncherResetView": "Нулирайте Изгледа",
|
||||
"resourceLauncherSaveAsNewView": "Запазете като Нов Изглед",
|
||||
"resourceLauncherSaveAsNewViewDescription": "Дайте име на този изглед, за да запазите текущите си филтри и оформление.",
|
||||
"resourceLauncherSaveForEveryone": "Запазете за Всеки",
|
||||
"resourceLauncherSaveForEveryoneDescription": "Споделете този изглед с всички членове на организацията. Когато е изключено, изгледът е видим само за вас.",
|
||||
"resourceLauncherMakePersonal": "Направи Личен",
|
||||
"resourceLauncherFilter": "Филтър",
|
||||
"resourceLauncherSort": "Сортиране",
|
||||
"resourceLauncherSortAscending": "Сортиране възходящо",
|
||||
"resourceLauncherSortDescending": "Сортиране низходящо",
|
||||
"resourceLauncherSettings": "Настройки",
|
||||
"resourceLauncherGroupBy": "Групирай По",
|
||||
"resourceLauncherGroupBySite": "Сайт",
|
||||
"resourceLauncherGroupByLabel": "Етикет",
|
||||
"resourceLauncherLayout": "Оформление",
|
||||
"resourceLauncherLayoutGrid": "Мрежа",
|
||||
"resourceLauncherLayoutList": "Списък",
|
||||
"resourceLauncherShowLabels": "Показване на Етикети",
|
||||
"resourceLauncherShowSiteTags": "Показване на Тагове на Сайт",
|
||||
"resourceLauncherShowRecents": "Показване на Последни",
|
||||
"resourceLauncherDeleteView": "Изтриване на Изглед",
|
||||
"resourceLauncherViewAsAdmin": "Вижте като Админ",
|
||||
"resourceLauncherResourceDetailsDescription": "Вижте детайлите за този ресурс.",
|
||||
"resourceLauncherUnlabeled": "Без Етикет",
|
||||
"resourceLauncherNoSite": "Няма Сайт",
|
||||
"resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
|
||||
"resourceLauncherEmptyStateTitle": "Няма Налични Ресурси",
|
||||
"resourceLauncherEmptyStateDescription": "Все още нямате достъп до никакви ресурси. Свържете се с вашия администратор, за да поискате достъп.",
|
||||
"resourceLauncherEmptyStateNoResultsTitle": "Няма Намерени Ресурси",
|
||||
"resourceLauncherEmptyStateNoResultsDescription": "Никакви ресурси не съвпадат с текущото ви търсене или филтри. Опитайте да ги коригирате, за да намерите това, което търсите.",
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery": "Никакви ресурси не съвпадат с \"{query}\". Опитайте да коригирате търсенето или да изтриете филтри, за да видите всички ресурси.",
|
||||
"resourceLauncherCopiedToClipboard": "Копирано в клипборда",
|
||||
"resourceLauncherCopiedAccessDescription": "Достъпът до ресурса е копиран на вашия клипборд.",
|
||||
"resourceLauncherViewNamePlaceholder": "Име на Изгледа",
|
||||
"resourceLauncherViewNameLabel": "Име на Изгледа",
|
||||
"resourceLauncherViewSaved": "Изгледът е запазен",
|
||||
"resourceLauncherViewSavedDescription": "Вашият изглед на стартер е запазен.",
|
||||
"resourceLauncherViewSaveFailed": "Неуспешно запазване на изгледа",
|
||||
"resourceLauncherViewSaveFailedDescription": "Не можеше да се запази изгледът на стартер. Моля, опитайте отново.",
|
||||
"resourceLauncherViewDeleted": "Изгледът е изтрит",
|
||||
"resourceLauncherViewDeletedDescription": "Изгледът на стартер е изтрит.",
|
||||
"resourceLauncherViewDeleteFailed": "Неуспешно изтриване на изгледа",
|
||||
"resourceLauncherViewDeleteFailedDescription": "Не можахте да изтриете изгледа на стартер. Моля, опитайте отново.",
|
||||
"memberPortalPrevious": "Предишен",
|
||||
"memberPortalNext": "Следващ",
|
||||
"httpSettings": "HTTP настройки",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----НАЧАЛО НА OPENSSH ЧАСТЕН КЛЮЧ-----",
|
||||
"sshPrivateKeyRequired": "Изисква се частен ключ",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Въведете вашата VNC парола за свързване",
|
||||
"vncSignInDescription": "Въведете VNC данните си за връзка",
|
||||
"vncUsernameOptional": "Потребителско име (по избор)",
|
||||
"vncPasswordOptional": "Парола (по избор)",
|
||||
"vncNoResourceTarget": "Не е налична цел за ресурса",
|
||||
"vncFailedToLoadNovnc": "Неуспешно зареждане на noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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íť",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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í",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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ř. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Seznam povolených emailů",
|
||||
"memberPortalResourceDisabled": "Zdroj je zakázán",
|
||||
"memberPortalShowingResources": "Zobrazeny {start}-{end} z {total} zdrojů",
|
||||
"resourceLauncherTitle": "Spouštěč zdrojů",
|
||||
"resourceLauncherDescription": "Podívejte se na podrobnosti o zdrojích a spusťte je z jednoho místa",
|
||||
"resourceLauncherSearchPlaceholder": "Hledat všechny lokality...",
|
||||
"resourceLauncherDefaultView": "Výchozí",
|
||||
"resourceLauncherSaveView": "Uložit pohled",
|
||||
"resourceLauncherSaveToCurrentView": "Uložit do aktuálního pohledu",
|
||||
"resourceLauncherResetView": "Obnovit pohled",
|
||||
"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",
|
||||
"resourceLauncherSort": "Řadit",
|
||||
"resourceLauncherSortAscending": "Řadit vzestupně",
|
||||
"resourceLauncherSortDescending": "Řadit sestupně",
|
||||
"resourceLauncherSettings": "Nastavení",
|
||||
"resourceLauncherGroupBy": "Seskupit podle",
|
||||
"resourceLauncherGroupBySite": "Lokalita",
|
||||
"resourceLauncherGroupByLabel": "Název",
|
||||
"resourceLauncherLayout": "Rozvržení",
|
||||
"resourceLauncherLayoutGrid": "Mřížka",
|
||||
"resourceLauncherLayoutList": "Seznam",
|
||||
"resourceLauncherShowLabels": "Zobrazit název",
|
||||
"resourceLauncherShowSiteTags": "Zobrazit značky lokality",
|
||||
"resourceLauncherShowRecents": "Zobrazit nedávné",
|
||||
"resourceLauncherDeleteView": "Smazat pohled",
|
||||
"resourceLauncherViewAsAdmin": "Zobrazit jako administrátor",
|
||||
"resourceLauncherResourceDetailsDescription": "Podívejte se na podrobnosti o tomto zdroji.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----ZAČÁTEK SOUKROMÉHO KLÍČE OPENSSH-----",
|
||||
"sshPrivateKeyRequired": "Je vyžadován soukromý klíč",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Zadejte své heslo VNC pro připojení",
|
||||
"vncSignInDescription": "Zadejte své VNC přihlašovací údaje pro připojení",
|
||||
"vncUsernameOptional": "Uživatelské jméno (nepovinné)",
|
||||
"vncPasswordOptional": "Heslo (nepovinné)",
|
||||
"vncNoResourceTarget": "Není k dispozici žádný cíl zdroje",
|
||||
"vncFailedToLoadNovnc": "Nepodařilo se načíst noVNC",
|
||||
|
||||
+3687
File diff suppressed because it is too large
Load Diff
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b> 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",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "E-Mail-Whitelist",
|
||||
"memberPortalResourceDisabled": "Ressource deaktiviert",
|
||||
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
|
||||
"resourceLauncherTitle": "Ressourcenstarter",
|
||||
"resourceLauncherDescription": "Sehen Sie sich Ressourcendetails an und starten Sie sie von einem Ort aus",
|
||||
"resourceLauncherSearchPlaceholder": "Alle Standorte durchsuchen...",
|
||||
"resourceLauncherDefaultView": "Standard",
|
||||
"resourceLauncherSaveView": "Ansicht speichern",
|
||||
"resourceLauncherSaveToCurrentView": "In aktueller Ansicht speichern",
|
||||
"resourceLauncherResetView": "Ansicht zurücksetzen",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sortieren",
|
||||
"resourceLauncherSortAscending": "Aufsteigend sortieren",
|
||||
"resourceLauncherSortDescending": "Absteigend sortieren",
|
||||
"resourceLauncherSettings": "Einstellungen",
|
||||
"resourceLauncherGroupBy": "Gruppieren nach",
|
||||
"resourceLauncherGroupBySite": "Standort",
|
||||
"resourceLauncherGroupByLabel": "Etikett",
|
||||
"resourceLauncherLayout": "Layout",
|
||||
"resourceLauncherLayoutGrid": "Raster",
|
||||
"resourceLauncherLayoutList": "Liste",
|
||||
"resourceLauncherShowLabels": "Etiketten anzeigen",
|
||||
"resourceLauncherShowSiteTags": "Standort-Tags anzeigen",
|
||||
"resourceLauncherShowRecents": "Kürzlich anzeigen",
|
||||
"resourceLauncherDeleteView": "Ansicht löschen",
|
||||
"resourceLauncherViewAsAdmin": "Ansicht als Administrator anzeigen",
|
||||
"resourceLauncherResourceDetailsDescription": "Anzeigen von Details zu dieser Ressource.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Privater Schlüssel ist erforderlich",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Geben Sie Ihr VNC-Passwort ein, um sich zu verbinden",
|
||||
"vncSignInDescription": "Geben Sie Ihre VNC-Zugangsdaten ein, um sich zu verbinden",
|
||||
"vncUsernameOptional": "Benutzername (optional)",
|
||||
"vncPasswordOptional": "Passwort (optional)",
|
||||
"vncNoResourceTarget": "Kein Ressourcen-Ziel verfügbar",
|
||||
"vncFailedToLoadNovnc": "Fehler beim Laden von noVNC",
|
||||
|
||||
+100
-6
@@ -66,9 +66,15 @@
|
||||
"local": "Local",
|
||||
"edit": "Edit",
|
||||
"siteConfirmDelete": "Confirm Delete Site",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Delete Site",
|
||||
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -204,7 +220,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 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.",
|
||||
"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.",
|
||||
"clientResourceTitle": "Manage Private Resources",
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -1486,6 +1503,7 @@
|
||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||
"otpAuthSubmit": "Submit Code",
|
||||
"idpContinue": "Or continue with",
|
||||
"idpLastUsed": "Last used",
|
||||
"otpAuthBack": "Back to Password",
|
||||
"navbar": "Navigation Menu",
|
||||
"navbarDescription": "Main navigation menu for the application",
|
||||
@@ -1638,7 +1656,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)",
|
||||
@@ -1898,6 +1916,9 @@
|
||||
"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",
|
||||
@@ -1926,6 +1947,9 @@
|
||||
"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",
|
||||
@@ -2071,6 +2095,7 @@
|
||||
"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",
|
||||
@@ -2171,10 +2196,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, separated by commas, spaces, or new lines. Absolute paths must be used.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Create Home Directory",
|
||||
"sshUnixGroups": "Unix Groups",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, separated by commas, spaces, or new lines.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Enter values, or drop a .txt or .csv file",
|
||||
"roleTextImportTitle": "Import from File",
|
||||
"roleTextImportDescription": "Importing {fileName} into {fieldLabel}.",
|
||||
@@ -2298,6 +2323,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2364,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2398,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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,6 +2591,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3010,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3577,55 @@
|
||||
"memberPortalEmailWhitelist": "Email Whitelist",
|
||||
"memberPortalResourceDisabled": "Resource Disabled",
|
||||
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
|
||||
"resourceLauncherTitle": "Resource Launcher",
|
||||
"resourceLauncherDescription": "View resource details and launch them from one place",
|
||||
"resourceLauncherSearchPlaceholder": "Search all sites...",
|
||||
"resourceLauncherDefaultView": "Default",
|
||||
"resourceLauncherSaveView": "Save View",
|
||||
"resourceLauncherSaveToCurrentView": "Save to Current View",
|
||||
"resourceLauncherResetView": "Reset View",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sort",
|
||||
"resourceLauncherSortAscending": "Sort ascending",
|
||||
"resourceLauncherSortDescending": "Sort descending",
|
||||
"resourceLauncherSettings": "Settings",
|
||||
"resourceLauncherGroupBy": "Group By",
|
||||
"resourceLauncherGroupBySite": "Site",
|
||||
"resourceLauncherGroupByLabel": "Label",
|
||||
"resourceLauncherLayout": "Layout",
|
||||
"resourceLauncherLayoutGrid": "Grid",
|
||||
"resourceLauncherLayoutList": "List",
|
||||
"resourceLauncherShowLabels": "Show Labels",
|
||||
"resourceLauncherShowSiteTags": "Show Site Tags",
|
||||
"resourceLauncherShowRecents": "Show Recents",
|
||||
"resourceLauncherDeleteView": "Delete View",
|
||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "View details for this resource.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3661,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Private key is required",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Enter your VNC password to connect",
|
||||
"vncSignInDescription": "Enter your VNC credentials to connect",
|
||||
"vncUsernameOptional": "Username (optional)",
|
||||
"vncPasswordOptional": "Password (optional)",
|
||||
"vncNoResourceTarget": "No resource target is available",
|
||||
"vncFailedToLoadNovnc": "Failed to load noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Lista Blanca de Correo",
|
||||
"memberPortalResourceDisabled": "Recurso Deshabilitado",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
||||
"resourceLauncherTitle": "Lanzador de Recursos",
|
||||
"resourceLauncherDescription": "Ve los detalles de los recursos y lánzalos desde un solo lugar",
|
||||
"resourceLauncherSearchPlaceholder": "Buscar en todos los sitios...",
|
||||
"resourceLauncherDefaultView": "Predeterminado",
|
||||
"resourceLauncherSaveView": "Guardar Vista",
|
||||
"resourceLauncherSaveToCurrentView": "Guardar en la Vista Actual",
|
||||
"resourceLauncherResetView": "Restablecer Vista",
|
||||
"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",
|
||||
"resourceLauncherSort": "Ordenar",
|
||||
"resourceLauncherSortAscending": "Ordenar Ascendente",
|
||||
"resourceLauncherSortDescending": "Ordenar Descendente",
|
||||
"resourceLauncherSettings": "Ajustes",
|
||||
"resourceLauncherGroupBy": "Agrupar Por",
|
||||
"resourceLauncherGroupBySite": "Sitio",
|
||||
"resourceLauncherGroupByLabel": "Etiqueta",
|
||||
"resourceLauncherLayout": "Disposición",
|
||||
"resourceLauncherLayoutGrid": "Cuadrícula",
|
||||
"resourceLauncherLayoutList": "Lista",
|
||||
"resourceLauncherShowLabels": "Mostrar Etiquetas",
|
||||
"resourceLauncherShowSiteTags": "Mostrar Etiquetas del Sitio",
|
||||
"resourceLauncherShowRecents": "Mostrar Recientes",
|
||||
"resourceLauncherDeleteView": "Eliminar Vista",
|
||||
"resourceLauncherViewAsAdmin": "Ver como Administrador",
|
||||
"resourceLauncherResourceDetailsDescription": "Ver detalles de este recurso.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----COMIENZO DE LA CLAVE PRIVADA OPENSSH-----",
|
||||
"sshPrivateKeyRequired": "Se requiere clave privada",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Introduce tu contraseña VNC para conectar",
|
||||
"vncSignInDescription": "Introduce tus credenciales VNC para conectarte",
|
||||
"vncUsernameOptional": "Nombre de usuario (opcional)",
|
||||
"vncPasswordOptional": "Contraseña (opcional)",
|
||||
"vncNoResourceTarget": "No hay objetivo de recurso disponible",
|
||||
"vncFailedToLoadNovnc": "Error al cargar noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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.",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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 <code>10.0.0.0/8</code>) 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é",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Liste blanche des e-mails",
|
||||
"memberPortalResourceDisabled": "Ressource désactivée",
|
||||
"memberPortalShowingResources": "Affichage de {start}-{end} sur {total} ressources",
|
||||
"resourceLauncherTitle": "Lanceur de ressources",
|
||||
"resourceLauncherDescription": "Afficher les détails des ressources et les lancer depuis un seul endroit",
|
||||
"resourceLauncherSearchPlaceholder": "Rechercher sur tous les sites...",
|
||||
"resourceLauncherDefaultView": "Par défaut",
|
||||
"resourceLauncherSaveView": "Enregistrer la vue",
|
||||
"resourceLauncherSaveToCurrentView": "Enregistrer dans la vue actuelle",
|
||||
"resourceLauncherResetView": "Réinitialiser la vue",
|
||||
"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",
|
||||
"resourceLauncherSort": "Trier",
|
||||
"resourceLauncherSortAscending": "Trier par ordre croissant",
|
||||
"resourceLauncherSortDescending": "Trier par ordre décroissant",
|
||||
"resourceLauncherSettings": "Réglages",
|
||||
"resourceLauncherGroupBy": "Grouper par",
|
||||
"resourceLauncherGroupBySite": "Nœud",
|
||||
"resourceLauncherGroupByLabel": "Étiquette",
|
||||
"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",
|
||||
"resourceLauncherViewAsAdmin": "Voir en tant qu'admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Afficher les détails pour cette ressource.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Une clé privée est requise",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Entrez votre mot de passe VNC pour vous connecter",
|
||||
"vncSignInDescription": "Entrez vos identifiants VNC pour vous connecter",
|
||||
"vncUsernameOptional": "Nom d'utilisateur (optionnel)",
|
||||
"vncPasswordOptional": "Mot de passe (facultatif)",
|
||||
"vncNoResourceTarget": "Aucune cible de ressource disponible",
|
||||
"vncFailedToLoadNovnc": "Échec du chargement de noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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 <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Lista Autorizzazioni Email",
|
||||
"memberPortalResourceDisabled": "Risorsa Disabilitata",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} di {total} risorse",
|
||||
"resourceLauncherTitle": "Lanscia Risorse",
|
||||
"resourceLauncherDescription": "Visualizza i dettagli delle risorse e lanciale da un solo posto",
|
||||
"resourceLauncherSearchPlaceholder": "Cerca tutti i siti...",
|
||||
"resourceLauncherDefaultView": "Predefinito",
|
||||
"resourceLauncherSaveView": "Salva Visualizzazione",
|
||||
"resourceLauncherSaveToCurrentView": "Salva alla Visualizzazione Corrente",
|
||||
"resourceLauncherResetView": "Reimposta Visualizzazione",
|
||||
"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",
|
||||
"resourceLauncherSort": "Ordina",
|
||||
"resourceLauncherSortAscending": "Ordina in ordine crescente",
|
||||
"resourceLauncherSortDescending": "Ordina in ordine decrescente",
|
||||
"resourceLauncherSettings": "Impostazioni",
|
||||
"resourceLauncherGroupBy": "Raggruppa per",
|
||||
"resourceLauncherGroupBySite": "Sito",
|
||||
"resourceLauncherGroupByLabel": "Etichetta",
|
||||
"resourceLauncherLayout": "Layout",
|
||||
"resourceLauncherLayoutGrid": "Griglia",
|
||||
"resourceLauncherLayoutList": "Lista",
|
||||
"resourceLauncherShowLabels": "Mostra Etichette",
|
||||
"resourceLauncherShowSiteTags": "Mostra Tag di Sito",
|
||||
"resourceLauncherShowRecents": "Mostra Recenti",
|
||||
"resourceLauncherDeleteView": "Elimina Visualizzazione",
|
||||
"resourceLauncherViewAsAdmin": "Visualizza come Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Visualizza i dettagli per questa risorsa.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "È richiesta una chiave privata",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Inserisci la tua password VNC per connetterti",
|
||||
"vncSignInDescription": "Inserisci le tue credenziali VNC per connetterti",
|
||||
"vncUsernameOptional": "Nome utente (facoltativo)",
|
||||
"vncPasswordOptional": "Password (opzionale)",
|
||||
"vncNoResourceTarget": "Nessun bersaglio di risorsa disponibile",
|
||||
"vncFailedToLoadNovnc": "Impossibile caricare noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"local": "로컬",
|
||||
"edit": "편집",
|
||||
"siteConfirmDelete": "사이트 삭제 확인",
|
||||
"siteConfirmDeleteAndResources": "사이트 및 리소스 삭제 확인",
|
||||
"siteDelete": "사이트 삭제",
|
||||
"siteDeleteAndResources": "사이트 및 리소스 삭제",
|
||||
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
|
||||
"siteMessageRemoveAndResources": "이 사이트와 연결된 모든 공용 및 개인 리소스는 다른 사이트에도 연결되어 있더라도 영구적으로 삭제됩니다.",
|
||||
"siteQuestionRemove": "조직에서 사이트를 제거하시겠습니까?",
|
||||
"siteQuestionRemoveAndResources": "이 사이트와 모든 관련 리소스를 삭제하시겠습니까?",
|
||||
"sitesTableDeleteSite": "사이트 삭제",
|
||||
"sitesTableDeleteSiteAndResources": "사이트 및 리소스 삭제",
|
||||
"siteManageSites": "사이트 관리",
|
||||
"siteDescription": "프라이빗 네트워크로의 연결을 활성화하려면 사이트를 생성하고 관리하세요.",
|
||||
"sitesBannerTitle": "모든 네트워크 연결",
|
||||
@@ -117,6 +123,16 @@
|
||||
"siteUpdated": "사이트가 업데이트되었습니다",
|
||||
"siteUpdatedDescription": "사이트가 업데이트되었습니다.",
|
||||
"siteGeneralDescription": "이 사이트에 대한 일반 설정을 구성하세요.",
|
||||
"siteRestartTitle": "사이트 다시 시작",
|
||||
"siteRestartDescription": "이 사이트의 WireGuard 터널을 다시 시작합니다. 일시적으로 연결이 중단될 수 있습니다.",
|
||||
"siteRestartBody": "사이트 터널이 제대로 작동하지 않을 경우, 호스트를 재시작하지 않고 다시 연결을 강제하려면 이 옵션을 사용하세요.",
|
||||
"siteRestartButton": "사이트 다시 시작",
|
||||
"siteRestartDialogMessage": "<b>{name}</b>의 WireGuard 터널을 재시작하시겠습니까? 이 작업으로 인해 사이트의 연결이 일시적으로 중단될 수 있습니다.",
|
||||
"siteRestartWarning": "터널을 재시작하는 동안 사이트가 일시적으로 연결이 끊깁니다.",
|
||||
"siteRestarted": "사이트가 재시작되었습니다",
|
||||
"siteRestartedDescription": "WireGuard 터널이 재시작되었습니다.",
|
||||
"siteErrorRestart": "사이트 재시작 실패",
|
||||
"siteErrorRestartDescription": "사이트를 재시작하는 중 오류가 발생했습니다.",
|
||||
"siteSettingDescription": "사이트에서 설정을 구성하세요.",
|
||||
"siteResourcesTab": "리소스",
|
||||
"siteResourcesNoneOnSite": "이 사이트에는 아직 공용 또는 개인 리소스가 없습니다.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"actionApplyBlueprint": "청사진 적용",
|
||||
"actionListBlueprints": "청사진 목록",
|
||||
"actionGetBlueprint": "청사진 가져오기",
|
||||
"actionCreateOrgWideLauncherView": "조직 전체 런처 보기 생성",
|
||||
"setupToken": "설정 토큰",
|
||||
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"subnetPlaceholder": "서브넷",
|
||||
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
|
||||
"selectSites": "사이트 선택",
|
||||
"selectLabels": "레이블 선택",
|
||||
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
|
||||
"clientInstallOlm": "Olm 설치",
|
||||
"clientInstallOlmDescription": "시스템에서 Olm을 실행하기",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"createInternalResourceDialogSite": "사이트",
|
||||
"selectSite": "사이트 선택...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, other {# 사이트}}",
|
||||
"labelsSelectorLabelsCount": "{count, plural, one {# 레이블} other {# 레이블}}",
|
||||
"noSitesFound": "사이트를 찾을 수 없습니다.",
|
||||
"createInternalResourceDialogProtocol": "프로토콜",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||
"createInternalResourceDialogAlias": "별칭",
|
||||
"createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
|
||||
"internalResourceAliasLocalWarning": ".local로 끝나는 별칭은 일부 네트워크에서 mDNS로 인해 해결 문제가 발생할 수 있습니다.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 리소스에 스킴이 필요합니다",
|
||||
"internalResourceHttpPortRequired": "HTTP 리소스에 목적지 포트가 필요합니다",
|
||||
"siteConfiguration": "설정",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"sidebarRemoteExitNodes": "원격 노드",
|
||||
"remoteExitNodeId": "ID",
|
||||
"remoteExitNodeSecretKey": "비밀",
|
||||
"remoteExitNodeNetworkingTitle": "네트워크 설정",
|
||||
"remoteExitNodeNetworkingDescription": "이 원격 출구 노드의 트래픽 라우팅 방법과 어떤 사이트가 이를 통해 연결하는지 구성합니다. 백홀 네트워킹 구성을 사용한 고급 기능입니다.",
|
||||
"remoteExitNodeNetworkingSave": "설정 저장",
|
||||
"remoteExitNodeNetworkingSaveSuccessTitle": "네트워크 설정이 저장되었습니다",
|
||||
"remoteExitNodeNetworkingSaveSuccessDescription": "네트워크 설정이 성공적으로 업데이트되었습니다.",
|
||||
"remoteExitNodeNetworkingSaveError": "네트워크 설정 저장 실패",
|
||||
"remoteExitNodeNetworkingSubnetsTitle": "원격 서브넷",
|
||||
"remoteExitNodeNetworkingSubnetsDescription": "이 원격 출구 노드가 트래픽을 라우팅할 CIDR 범위를 정의합니다. 유효한 CIDR을 입력하고 Enter를 눌러 추가하세요 (예: <code>10.0.0.0/8</code>).",
|
||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "CIDR 범위 추가 (예: 10.0.0.0/8)",
|
||||
"remoteExitNodeNetworkingSubnetsLoadError": "서브넷 로드 실패",
|
||||
"remoteExitNodeNetworkingLabelsTitle": "우선순위 레이블",
|
||||
"remoteExitNodeNetworkingLabelsDescription": "이 레이블이 있는 사이트는 이 원격 출구 노드를 통해 연결됩니다.",
|
||||
"remoteExitNodeNetworkingLabelsButtonText": "레이블 선택...",
|
||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "레이블 검색...",
|
||||
"remoteExitNodeNetworkingLabelsLoadError": "레이블 로드 실패",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "원격 노드 생성",
|
||||
"description": "새로운 자체 호스팅 원격 중계 및 프록시 서버 노드를 생성하십시오.",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC 공급자",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자",
|
||||
"subnet": "서브넷",
|
||||
"utilitySubnet": "유틸리티 서브넷",
|
||||
"subnetDescription": "이 조직의 네트워크 구성에 대한 서브넷입니다.",
|
||||
"customDomain": "사용자 정의 도메인",
|
||||
"authPage": "인증 페이지",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
|
||||
"loadingDNSRecords": "DNS 레코드를 로드하는 중...",
|
||||
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"updateAvailableInfo": "업데이트된 버전이 있습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"client": "클라이언트",
|
||||
"proxyProtocol": "프록시 프로토콜 설정",
|
||||
"proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "이메일 화이트리스트",
|
||||
"memberPortalResourceDisabled": "리소스 비활성화됨",
|
||||
"memberPortalShowingResources": "{start}-{end} 중 {total}개의 리소스를 표시 중",
|
||||
"resourceLauncherTitle": "리소스 런처",
|
||||
"resourceLauncherDescription": "리소스 세부 정보를 보고 한 곳에서 실행하세요",
|
||||
"resourceLauncherSearchPlaceholder": "모든 사이트 검색...",
|
||||
"resourceLauncherDefaultView": "기본값",
|
||||
"resourceLauncherSaveView": "보기를 저장",
|
||||
"resourceLauncherSaveToCurrentView": "현재 보기로 저장",
|
||||
"resourceLauncherResetView": "보기를 재설정",
|
||||
"resourceLauncherSaveAsNewView": "새 보기로 저장",
|
||||
"resourceLauncherSaveAsNewViewDescription": "현재 필터와 레이아웃을 저장할 이름을 입력하세요.",
|
||||
"resourceLauncherSaveForEveryone": "모두에게 저장",
|
||||
"resourceLauncherSaveForEveryoneDescription": "이 보기를 모든 조직 구성원과 공유합니다. 체크 해제하면 해당 뷰는 사용자에게만 표시됩니다.",
|
||||
"resourceLauncherMakePersonal": "개인적으로 만들기",
|
||||
"resourceLauncherFilter": "필터",
|
||||
"resourceLauncherSort": "정렬",
|
||||
"resourceLauncherSortAscending": "오름차순 정렬",
|
||||
"resourceLauncherSortDescending": "내림차순 정렬",
|
||||
"resourceLauncherSettings": "설정",
|
||||
"resourceLauncherGroupBy": "그룹화 기준",
|
||||
"resourceLauncherGroupBySite": "사이트",
|
||||
"resourceLauncherGroupByLabel": "레이블",
|
||||
"resourceLauncherLayout": "레이아웃",
|
||||
"resourceLauncherLayoutGrid": "그리드",
|
||||
"resourceLauncherLayoutList": "목록",
|
||||
"resourceLauncherShowLabels": "레이블 표시",
|
||||
"resourceLauncherShowSiteTags": "사이트 태그 표시",
|
||||
"resourceLauncherShowRecents": "최근 항목 표시",
|
||||
"resourceLauncherDeleteView": "보기 삭제",
|
||||
"resourceLauncherViewAsAdmin": "관리자로 보기",
|
||||
"resourceLauncherResourceDetailsDescription": "이 리소스의 세부정보를 봅니다.",
|
||||
"resourceLauncherUnlabeled": "레이블 없음",
|
||||
"resourceLauncherNoSite": "사이트 없음",
|
||||
"resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
|
||||
"resourceLauncherEmptyStateTitle": "사용 가능한 리소스 없음",
|
||||
"resourceLauncherEmptyStateDescription": "아직 리소스에 대한 액세스 권한이 없습니다. 액세스를 요청하려면 관리자에게 문의하세요.",
|
||||
"resourceLauncherEmptyStateNoResultsTitle": "리소스를 찾을 수 없음",
|
||||
"resourceLauncherEmptyStateNoResultsDescription": "현재 검색이나 필터에 맞는 리소스가 없습니다. 필터를 조정하여 찾으려는 항목을 확인해보세요.",
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery": "\"{query}\"와 일치하는 리소스가 없습니다. 검색을 조정하거나 필터를 지워서 모든 리소스를 확인해보세요.",
|
||||
"resourceLauncherCopiedToClipboard": "클립보드에 복사됨",
|
||||
"resourceLauncherCopiedAccessDescription": "리소스 액세스가 클립보드에 복사되었습니다.",
|
||||
"resourceLauncherViewNamePlaceholder": "보기 이름",
|
||||
"resourceLauncherViewNameLabel": "뷰 이름",
|
||||
"resourceLauncherViewSaved": "보기 저장됨",
|
||||
"resourceLauncherViewSavedDescription": "런처 뷰가 저장되었습니다.",
|
||||
"resourceLauncherViewSaveFailed": "뷰 저장 실패",
|
||||
"resourceLauncherViewSaveFailedDescription": "런처 뷰를 저장할 수 없습니다. 다시 시도하세요.",
|
||||
"resourceLauncherViewDeleted": "보기 삭제됨",
|
||||
"resourceLauncherViewDeletedDescription": "런처 뷰가 삭제되었습니다.",
|
||||
"resourceLauncherViewDeleteFailed": "뷰 삭제 실패",
|
||||
"resourceLauncherViewDeleteFailedDescription": "런처 뷰를 삭제할 수 없습니다. 다시 시도하세요.",
|
||||
"memberPortalPrevious": "이전",
|
||||
"memberPortalNext": "다음",
|
||||
"httpSettings": "HTTP 설정",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "프라이빗 키가 필요합니다",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "연결하려면 VNC 비밀번호를 입력하세요",
|
||||
"vncSignInDescription": "연결하기 위해 VNC 자격 증명을 입력하세요",
|
||||
"vncUsernameOptional": "사용자 이름 (선택 사항)",
|
||||
"vncPasswordOptional": "비밀번호 (선택 사항)",
|
||||
"vncNoResourceTarget": "사용할 수 있는 리소스 대상이 없습니다",
|
||||
"vncFailedToLoadNovnc": "noVNC 로드를 실패했습니다",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b> 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "E-post-hviteliste",
|
||||
"memberPortalResourceDisabled": "Ressurs deaktivert",
|
||||
"memberPortalShowingResources": "Viser {start}-{end} av {total} ressurser",
|
||||
"resourceLauncherTitle": "Ressurslansering",
|
||||
"resourceLauncherDescription": "Vis ressursdetaljer og start dem fra ett sted",
|
||||
"resourceLauncherSearchPlaceholder": "Søk i alle områder...",
|
||||
"resourceLauncherDefaultView": "Standard",
|
||||
"resourceLauncherSaveView": "Lagre visning",
|
||||
"resourceLauncherSaveToCurrentView": "Lagre til nåværende visning",
|
||||
"resourceLauncherResetView": "Tilbakestill visning",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sorter",
|
||||
"resourceLauncherSortAscending": "Sorter stigende",
|
||||
"resourceLauncherSortDescending": "Sorter synkende",
|
||||
"resourceLauncherSettings": "Innstillinger",
|
||||
"resourceLauncherGroupBy": "Grupper etter",
|
||||
"resourceLauncherGroupBySite": "Område",
|
||||
"resourceLauncherGroupByLabel": "Etikett",
|
||||
"resourceLauncherLayout": "Oppsett",
|
||||
"resourceLauncherLayoutGrid": "Rutenett",
|
||||
"resourceLauncherLayoutList": "Liste",
|
||||
"resourceLauncherShowLabels": "Vis etiketter",
|
||||
"resourceLauncherShowSiteTags": "Vis områdestikkord",
|
||||
"resourceLauncherShowRecents": "Vis nylige",
|
||||
"resourceLauncherDeleteView": "Slett visning",
|
||||
"resourceLauncherViewAsAdmin": "Vis som administrator",
|
||||
"resourceLauncherResourceDetailsDescription": "Vis detaljer for denne ressursen.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGYNN OPENSSH PRIVAT NØKKEL-----",
|
||||
"sshPrivateKeyRequired": "Privat nøkkel er påkrevd",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Skriv inn VNC-passordet for å koble til",
|
||||
"vncSignInDescription": "Skriv inn VNC-kredentialene dine for å koble til",
|
||||
"vncUsernameOptional": "Brukernavn (valgfritt)",
|
||||
"vncPasswordOptional": "Passord (valgfritt)",
|
||||
"vncNoResourceTarget": "Ingen ressursemål tilgjengelig",
|
||||
"vncFailedToLoadNovnc": "Klarte ikke å laste noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b> 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "E-mail whitelist",
|
||||
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
|
||||
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
|
||||
"resourceLauncherTitle": "Bron Launcher",
|
||||
"resourceLauncherDescription": "Bekijk brongegevens en start ze vanaf één plek",
|
||||
"resourceLauncherSearchPlaceholder": "Zoek alle sites...",
|
||||
"resourceLauncherDefaultView": "Standaard",
|
||||
"resourceLauncherSaveView": "Weergave Opslaan",
|
||||
"resourceLauncherSaveToCurrentView": "Opslaan naar huidige weergave",
|
||||
"resourceLauncherResetView": "Weergave Herstellen",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sorteren",
|
||||
"resourceLauncherSortAscending": "Oplopend sorteren",
|
||||
"resourceLauncherSortDescending": "Aflopend sorteren",
|
||||
"resourceLauncherSettings": "Instellingen",
|
||||
"resourceLauncherGroupBy": "Groep Op",
|
||||
"resourceLauncherGroupBySite": "Site",
|
||||
"resourceLauncherGroupByLabel": "Label",
|
||||
"resourceLauncherLayout": "Lay-out",
|
||||
"resourceLauncherLayoutGrid": "Raster",
|
||||
"resourceLauncherLayoutList": "Lijst",
|
||||
"resourceLauncherShowLabels": "Labels Weergeven",
|
||||
"resourceLauncherShowSiteTags": "Site Tags Weergeven",
|
||||
"resourceLauncherShowRecents": "Recente Weergeven",
|
||||
"resourceLauncherDeleteView": "Weergave Verwijderen",
|
||||
"resourceLauncherViewAsAdmin": "Bekijk als Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Bekijk details voor deze bron.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Privésleutel is vereist",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Voer uw VNC-wachtwoord in om verbinding te maken",
|
||||
"vncSignInDescription": "Voer uw VNC-referenties in om verbinding te maken",
|
||||
"vncUsernameOptional": "Gebruikersnaam (optioneel)",
|
||||
"vncPasswordOptional": "Wachtwoord (optioneel)",
|
||||
"vncNoResourceTarget": "Geen bron doelwit beschikbaar",
|
||||
"vncFailedToLoadNovnc": "Laden van noVNC mislukt",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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ć",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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. <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"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",
|
||||
"resourceLauncherDescription": "Przeglądaj szczegóły zasobów i uruchamiaj je z jednego miejsca",
|
||||
"resourceLauncherSearchPlaceholder": "Szukaj we wszystkich stronach...",
|
||||
"resourceLauncherDefaultView": "Domyślny",
|
||||
"resourceLauncherSaveView": "Zapisz Widok",
|
||||
"resourceLauncherSaveToCurrentView": "Zapisz do bieżącego widoku",
|
||||
"resourceLauncherResetView": "Resetuj Widok",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sortuj",
|
||||
"resourceLauncherSortAscending": "Sortuj rosnąco",
|
||||
"resourceLauncherSortDescending": "Sortuj malejąco",
|
||||
"resourceLauncherSettings": "Ustawienia",
|
||||
"resourceLauncherGroupBy": "Grupuj według",
|
||||
"resourceLauncherGroupBySite": "Witryna",
|
||||
"resourceLauncherGroupByLabel": "Etykieta",
|
||||
"resourceLauncherLayout": "Układ",
|
||||
"resourceLauncherLayoutGrid": "Siatka",
|
||||
"resourceLauncherLayoutList": "Lista",
|
||||
"resourceLauncherShowLabels": "Pokaż etykiety",
|
||||
"resourceLauncherShowSiteTags": "Pokaż tagi stron",
|
||||
"resourceLauncherShowRecents": "Pokaż ostatnie",
|
||||
"resourceLauncherDeleteView": "Usuń Widok",
|
||||
"resourceLauncherViewAsAdmin": "Przeglądaj jako Administrator",
|
||||
"resourceLauncherResourceDetailsDescription": "Pokaż szczegóły tego zasobu.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Wymagany jest klucz prywatny",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Wprowadź hasło VNC, aby się połączyć",
|
||||
"vncSignInDescription": "Wprowadź swoje dane uwierzytelniające VNC aby się połączyć",
|
||||
"vncUsernameOptional": "Nazwa użytkownika (opcjonalnie)",
|
||||
"vncPasswordOptional": "Hasło (opcjonalne)",
|
||||
"vncNoResourceTarget": "Brak dostępnego celu zasobu",
|
||||
"vncFailedToLoadNovnc": "Błąd ładowania noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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 <b>{name}</b>? 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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, <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Lista de E-mails Permitidos",
|
||||
"memberPortalResourceDisabled": "Recurso Desativado",
|
||||
"memberPortalShowingResources": "Mostrando {start}-{end} de {total} recursos",
|
||||
"resourceLauncherTitle": "Lançador de Recursos",
|
||||
"resourceLauncherDescription": "Veja os detalhes do recurso e lance-os de um só lugar",
|
||||
"resourceLauncherSearchPlaceholder": "Procurar todos os sites...",
|
||||
"resourceLauncherDefaultView": "Padrão",
|
||||
"resourceLauncherSaveView": "Salvar Visualização",
|
||||
"resourceLauncherSaveToCurrentView": "Salvar na Visualização Atual",
|
||||
"resourceLauncherResetView": "Redefinir Visualização",
|
||||
"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",
|
||||
"resourceLauncherSort": "Ordenar",
|
||||
"resourceLauncherSortAscending": "Ordenar ascendente",
|
||||
"resourceLauncherSortDescending": "Ordenar descendente",
|
||||
"resourceLauncherSettings": "Configurações",
|
||||
"resourceLauncherGroupBy": "Agrupar por",
|
||||
"resourceLauncherGroupBySite": "Site",
|
||||
"resourceLauncherGroupByLabel": "Marcador",
|
||||
"resourceLauncherLayout": "Layout",
|
||||
"resourceLauncherLayoutGrid": "Grade",
|
||||
"resourceLauncherLayoutList": "Lista",
|
||||
"resourceLauncherShowLabels": "Mostrar Marcadores",
|
||||
"resourceLauncherShowSiteTags": "Mostrar Etiquetas de Site",
|
||||
"resourceLauncherShowRecents": "Mostrar Recents",
|
||||
"resourceLauncherDeleteView": "Excluir Visualização",
|
||||
"resourceLauncherViewAsAdmin": "Visualizar como Administrador",
|
||||
"resourceLauncherResourceDetailsDescription": "Veja detalhes deste recurso.",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "Chave privada é necessária",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Digite sua senha VNC para conectar",
|
||||
"vncSignInDescription": "Digite suas credenciais VNC para conectar",
|
||||
"vncUsernameOptional": "Nome de usuário (opcional)",
|
||||
"vncPasswordOptional": "Senha (opcional)",
|
||||
"vncNoResourceTarget": "Nenhum alvo de recurso disponível",
|
||||
"vncFailedToLoadNovnc": "Falha ao carregar noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"local": "Локальный",
|
||||
"edit": "Редактировать",
|
||||
"siteConfirmDelete": "Подтвердить удаление сайта",
|
||||
"siteConfirmDeleteAndResources": "Подтвердите удаление сайта и ресурсов",
|
||||
"siteDelete": "Удалить сайт",
|
||||
"siteDeleteAndResources": "Удалить сайт и ресурсы",
|
||||
"siteMessageRemove": "После удаления сайт больше не будет доступен. Все цели, связанные с сайтом, также будут удалены.",
|
||||
"siteMessageRemoveAndResources": "Это навсегда удалит все общественные и частные ресурсы, связанные с этим сайтом, даже если ресурс также связан с другими сайтами.",
|
||||
"siteQuestionRemove": "Вы уверены, что хотите удалить сайт из организации?",
|
||||
"siteQuestionRemoveAndResources": "Вы уверены, что хотите удалить этот сайт и все связанные с ним ресурсы?",
|
||||
"sitesTableDeleteSite": "Удалить сайт",
|
||||
"sitesTableDeleteSiteAndResources": "Удалить сайт и ресурсы",
|
||||
"siteManageSites": "Управление сайтами",
|
||||
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
|
||||
"sitesBannerTitle": "Подключить любую сеть",
|
||||
@@ -117,6 +123,16 @@
|
||||
"siteUpdated": "Сайт обновлён",
|
||||
"siteUpdatedDescription": "Сайт был успешно обновлён.",
|
||||
"siteGeneralDescription": "Настройте общие параметры для этого сайта",
|
||||
"siteRestartTitle": "Перезагрузить сайт",
|
||||
"siteRestartDescription": "Перезапустите туннель WireGuard для этого сайта. Это кратковременно прервет соединение.",
|
||||
"siteRestartBody": "Используйте это, если туннель сайта не работает должным образом и вам нужно принудительно переподключиться без перезапуска хоста.",
|
||||
"siteRestartButton": "Перезагрузить сайт",
|
||||
"siteRestartDialogMessage": "Вы уверены, что хотите перезапустить туннель WireGuard для <b>{name}</b>? Сайт кратковременно потеряет соединение.",
|
||||
"siteRestartWarning": "Сайт кратковременно отключится во время перезапуска туннеля.",
|
||||
"siteRestarted": "Сайт перезапущен",
|
||||
"siteRestartedDescription": "Туннель WireGuard был перезапущен.",
|
||||
"siteErrorRestart": "Не удалось перезапустить сайт",
|
||||
"siteErrorRestartDescription": "Произошла ошибка во время перезапуска сайта.",
|
||||
"siteSettingDescription": "Настройка параметров на сайте",
|
||||
"siteResourcesTab": "Ресурсы",
|
||||
"siteResourcesNoneOnSite": "На этом сайте пока нет публичных или частных ресурсов.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"actionApplyBlueprint": "Применить чертёж",
|
||||
"actionListBlueprints": "Список чертежей",
|
||||
"actionGetBlueprint": "Получить чертёж",
|
||||
"actionCreateOrgWideLauncherView": "Создать вид запуска на уровне организации",
|
||||
"setupToken": "Код настройки",
|
||||
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
||||
"setupTokenRequired": "Токен настройки обязателен",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"subnetPlaceholder": "Подсеть",
|
||||
"addressDescription": "Внутренний адрес клиента. Должен находиться в подсети организации.",
|
||||
"selectSites": "Выберите сайты",
|
||||
"selectLabels": "Выберите метки",
|
||||
"sitesDescription": "Клиент будет иметь подключение к выбранным сайтам",
|
||||
"clientInstallOlm": "Установить Olm",
|
||||
"clientInstallOlmDescription": "Запустите Olm на вашей системе",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"createInternalResourceDialogSite": "Сайт",
|
||||
"selectSite": "Выберите сайт...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, one {# сайт} few {# сайта} many {# сайтов} other {# сайтов}}",
|
||||
"labelsSelectorLabelsCount": "{count, plural, one {# метка} few {# метки} many {# меток} other {# меток}}",
|
||||
"noSitesFound": "Сайты не найдены.",
|
||||
"createInternalResourceDialogProtocol": "Протокол",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
|
||||
"internalResourceAliasLocalWarning": "Псевдонимы, оканчивающиеся на .local, могут вызывать проблемы с разрешением из-за mDNS в некоторых сетях.",
|
||||
"internalResourceDownstreamSchemeRequired": "Схема обязательна для HTTP ресурсов",
|
||||
"internalResourceHttpPortRequired": "Порт назначения обязателен для HTTP ресурсов",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"sidebarRemoteExitNodes": "Удаленные узлы",
|
||||
"remoteExitNodeId": "ID",
|
||||
"remoteExitNodeSecretKey": "Секретный ключ",
|
||||
"remoteExitNodeNetworkingTitle": "Настройки сети",
|
||||
"remoteExitNodeNetworkingDescription": "Настройте, как этот удаленный узел выхода маршрутизирует трафик и какие сайты предпочитают подключаться через него. Расширенные функции для использования с конфигурациями магистральной сети.",
|
||||
"remoteExitNodeNetworkingSave": "Сохранить настройки",
|
||||
"remoteExitNodeNetworkingSaveSuccessTitle": "Сетевые настройки сохранены",
|
||||
"remoteExitNodeNetworkingSaveSuccessDescription": "Сетевые настройки были успешно обновлены.",
|
||||
"remoteExitNodeNetworkingSaveError": "Не удалось сохранить сетевые настройки",
|
||||
"remoteExitNodeNetworkingSubnetsTitle": "Удалённые подсети",
|
||||
"remoteExitNodeNetworkingSubnetsDescription": "Определите диапазоны CIDR, которые этот удаленный узел выхода будет использовать для маршрутизации трафика. Введите действительный CIDR (например, <code>10.0.0.0/8</code>) и нажмите Enter, чтобы добавить.",
|
||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "Добавить диапазон CIDR (например, 10.0.0.0/8)",
|
||||
"remoteExitNodeNetworkingSubnetsLoadError": "Не удалось загрузить подсети",
|
||||
"remoteExitNodeNetworkingLabelsTitle": "Этикетки предпочтений",
|
||||
"remoteExitNodeNetworkingLabelsDescription": "Сайты с этими метками будут обязаны подключаться через этот удаленный узел выхода.",
|
||||
"remoteExitNodeNetworkingLabelsButtonText": "Выберите метки...",
|
||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Поиск меток...",
|
||||
"remoteExitNodeNetworkingLabelsLoadError": "Не удалось загрузить метки",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Создать удалённый узел",
|
||||
"description": "Создайте новый самостоятельный удалённый ретранслятор и узел прокси-сервера",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC провайдер",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"subnet": "Подсеть",
|
||||
"utilitySubnet": "Утилита подсети",
|
||||
"subnetDescription": "Подсеть для конфигурации сети этой организации.",
|
||||
"customDomain": "Пользовательский домен",
|
||||
"authPage": "Страницы аутентификации",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"orgOrDomainIdMissing": "Отсутствует организация или ID домена",
|
||||
"loadingDNSRecords": "Загрузка записей DNS...",
|
||||
"olmUpdateAvailableInfo": "Доступна обновленная версия Олма. Пожалуйста, обновитесь до последней версии.",
|
||||
"updateAvailableInfo": "Доступна обновленная версия. Пожалуйста, обновитесь до последней версии для получения лучшего опыта.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки протокола прокси",
|
||||
"proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "Белый список email",
|
||||
"memberPortalResourceDisabled": "Ресурс отключён",
|
||||
"memberPortalShowingResources": "Показаны {start}-{end} из {total} ресурсов",
|
||||
"resourceLauncherTitle": "Запуск ресурса",
|
||||
"resourceLauncherDescription": "Просмотр деталей ресурса и запуск их из одного места",
|
||||
"resourceLauncherSearchPlaceholder": "Поиск всех сайтов...",
|
||||
"resourceLauncherDefaultView": "По умолчанию",
|
||||
"resourceLauncherSaveView": "Сохранить вид",
|
||||
"resourceLauncherSaveToCurrentView": "Сохранить в текущий вид",
|
||||
"resourceLauncherResetView": "Сбросить вид",
|
||||
"resourceLauncherSaveAsNewView": "Сохранить как новый вид",
|
||||
"resourceLauncherSaveAsNewViewDescription": "Дайте этому виду имя, чтобы сохранить текущие фильтры и макет.",
|
||||
"resourceLauncherSaveForEveryone": "Сохранить для всех",
|
||||
"resourceLauncherSaveForEveryoneDescription": "Поделитесь этим видом со всеми членами организации. Если не отмечено, видимость только для вас.",
|
||||
"resourceLauncherMakePersonal": "Сделать личным",
|
||||
"resourceLauncherFilter": "Фильтр",
|
||||
"resourceLauncherSort": "Сортировать",
|
||||
"resourceLauncherSortAscending": "Сортировать по возрастанию",
|
||||
"resourceLauncherSortDescending": "Сортировать по убыванию",
|
||||
"resourceLauncherSettings": "Настройки",
|
||||
"resourceLauncherGroupBy": "Группировать по",
|
||||
"resourceLauncherGroupBySite": "Сайт",
|
||||
"resourceLauncherGroupByLabel": "Метка",
|
||||
"resourceLauncherLayout": "Макет",
|
||||
"resourceLauncherLayoutGrid": "Сетка",
|
||||
"resourceLauncherLayoutList": "Список",
|
||||
"resourceLauncherShowLabels": "Показать метки",
|
||||
"resourceLauncherShowSiteTags": "Показать теги сайта",
|
||||
"resourceLauncherShowRecents": "Показать недавно",
|
||||
"resourceLauncherDeleteView": "Удалить вид",
|
||||
"resourceLauncherViewAsAdmin": "Просмотр как администратор",
|
||||
"resourceLauncherResourceDetailsDescription": "Просмотр деталей этого ресурса.",
|
||||
"resourceLauncherUnlabeled": "Без меток",
|
||||
"resourceLauncherNoSite": "Без сайта",
|
||||
"resourceLauncherNoResourcesInGroup": "Нет ресурсов в данной группе",
|
||||
"resourceLauncherEmptyStateTitle": "Нет доступных ресурсов",
|
||||
"resourceLauncherEmptyStateDescription": "У вас пока нет доступа ни к одному ресурсу. Обратитесь к администратору, чтобы запросить доступ.",
|
||||
"resourceLauncherEmptyStateNoResultsTitle": "Ресурсы не найдены",
|
||||
"resourceLauncherEmptyStateNoResultsDescription": "Ни один ресурс не соответствует вашему текущему поисковому запросу или фильтрам. Попробуйте их изменить, чтобы найти нужное.",
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery": "Ни один ресурс не соответствует \"{query}\". Попробуйте изменить параметры поиска или очистить фильтры, чтобы увидеть все ресурсы.",
|
||||
"resourceLauncherCopiedToClipboard": "Скопировано в буфер обмена",
|
||||
"resourceLauncherCopiedAccessDescription": "Доступ к ресурсу был скопирован в ваш буфер обмена.",
|
||||
"resourceLauncherViewNamePlaceholder": "Имя вида",
|
||||
"resourceLauncherViewNameLabel": "Имя вида",
|
||||
"resourceLauncherViewSaved": "Вид сохранён",
|
||||
"resourceLauncherViewSavedDescription": "Ваш вид запуска был сохранён.",
|
||||
"resourceLauncherViewSaveFailed": "Не удалось сохранить вид",
|
||||
"resourceLauncherViewSaveFailedDescription": "Не удалось сохранить вид. Пожалуйста, попробуйте еще раз.",
|
||||
"resourceLauncherViewDeleted": "Вид удалён",
|
||||
"resourceLauncherViewDeletedDescription": "Вид запуска был удалён.",
|
||||
"resourceLauncherViewDeleteFailed": "Не удалось удалить вид",
|
||||
"resourceLauncherViewDeleteFailedDescription": "Не удалось удалить вид. Пожалуйста, попробуйте еще раз.",
|
||||
"memberPortalPrevious": "Предыдущий",
|
||||
"memberPortalNext": "Следующий",
|
||||
"httpSettings": "Настройки HTTP",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----НАЧАЛО ЛИЧНОГО КЛЮЧА OPENSSH-----",
|
||||
"sshPrivateKeyRequired": "Требуется личный ключ",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Введите пароль VNC для подключения",
|
||||
"vncSignInDescription": "Введите ваши учетные данные VNC для подключения",
|
||||
"vncUsernameOptional": "Имя пользователя (необязательно)",
|
||||
"vncPasswordOptional": "Пароль (необязательно)",
|
||||
"vncNoResourceTarget": "Отсутствует целевой ресурс",
|
||||
"vncFailedToLoadNovnc": "Не удалось загрузить noVNC",
|
||||
|
||||
+88
-1
@@ -66,9 +66,15 @@
|
||||
"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",
|
||||
@@ -117,6 +123,16 @@
|
||||
"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": "<b>{name}</b> 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.",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"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",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"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",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"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",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"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",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"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, <code>10.0.0.0/8</code>) 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",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"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ı",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"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.",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "E-posta Beyaz Listesi",
|
||||
"memberPortalResourceDisabled": "Kaynak Devre Dışı",
|
||||
"memberPortalShowingResources": "{total} kaynaktan {start}-{end} gösteriliyor",
|
||||
"resourceLauncherTitle": "Kaynak Başlatıcı",
|
||||
"resourceLauncherDescription": "Kaynağın detaylarını görüntüleyin ve tek bir yerden başlatın",
|
||||
"resourceLauncherSearchPlaceholder": "Tüm siteleri ara...",
|
||||
"resourceLauncherDefaultView": "Varsayılan",
|
||||
"resourceLauncherSaveView": "Görünümü Kaydet",
|
||||
"resourceLauncherSaveToCurrentView": "Mevcut Görünüme Kaydet",
|
||||
"resourceLauncherResetView": "Görünümü Sıfırla",
|
||||
"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",
|
||||
"resourceLauncherSort": "Sıralama",
|
||||
"resourceLauncherSortAscending": "Artan sırala",
|
||||
"resourceLauncherSortDescending": "Azalan sırala",
|
||||
"resourceLauncherSettings": "Ayarlar",
|
||||
"resourceLauncherGroupBy": "Grupla",
|
||||
"resourceLauncherGroupBySite": "Site",
|
||||
"resourceLauncherGroupByLabel": "Etiket",
|
||||
"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",
|
||||
"resourceLauncherViewAsAdmin": "Yönetici Olarak Görüntüle",
|
||||
"resourceLauncherResourceDetailsDescription": "Bu kaynağın detaylarını görüntüleyin.",
|
||||
"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.",
|
||||
"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ı",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BAŞLANGIÇ OPENSSH ÖZEL ANAHTARI-----",
|
||||
"sshPrivateKeyRequired": "Özel anahtar gereklidir",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Bağlanmak için VNC parolanızı girin",
|
||||
"vncSignInDescription": "Bağlanmak için VNC kimlik bilgilerinizi girin",
|
||||
"vncUsernameOptional": "Kullanıcı Adı (isteğe bağlı)",
|
||||
"vncPasswordOptional": "Parola (isteğe bağlı)",
|
||||
"vncNoResourceTarget": "Kaynak hedefi mevcut değil",
|
||||
"vncFailedToLoadNovnc": "NoVNC yüklenemedi",
|
||||
|
||||
+147
-60
@@ -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,21 +58,27 @@
|
||||
"name": "名称",
|
||||
"online": "在线",
|
||||
"offline": "离线的",
|
||||
"site": "站点",
|
||||
"site": "节点",
|
||||
"dataIn": "数据输入",
|
||||
"dataOut": "数据输出",
|
||||
"connectionType": "连接类型",
|
||||
"tunnelType": "隧道类型",
|
||||
"local": "本地的",
|
||||
"edit": "编辑",
|
||||
"siteConfirmDelete": "确认删除站点",
|
||||
"siteDelete": "删除站点",
|
||||
"siteMessageRemove": "一旦移除,站点将无法访问。与站点相关的所有目标也将被移除。",
|
||||
"siteQuestionRemove": "您确定要从组织中删除该站点吗?",
|
||||
"siteConfirmDelete": "确认删除节点",
|
||||
"siteConfirmDeleteAndResources": "确认删除站点及资源",
|
||||
"siteDelete": "删除节点",
|
||||
"siteDeleteAndResources": "删除站点及资源",
|
||||
"siteMessageRemove": "一旦移除,节点将无法访问。与节点相关的所有目标也将被移除。",
|
||||
"siteMessageRemoveAndResources": "这将永久删除与该站点关联的所有公共和私人资源,即使资源也与其他站点相关联。",
|
||||
"siteQuestionRemove": "您确定要从组织中删除该节点吗?",
|
||||
"siteQuestionRemoveAndResources": "您确定要删除此站点及所有关联资源吗?",
|
||||
"sitesTableDeleteSite": "删除站点",
|
||||
"sitesTableDeleteSiteAndResources": "删除站点及资源",
|
||||
"siteManageSites": "管理站点",
|
||||
"siteDescription": "创建和管理站点,启用与私人网络的连接",
|
||||
"sitesBannerTitle": "连接任何网络",
|
||||
"sitesBannerDescription": "站点是连接到远程网络的链接,允许Pangolin为用户提供资源访问,无论是公共还是私人。可以在任何可以运行二进制文件或容器的地方安装站点网络连接器(Newt)以建立连接。",
|
||||
"sitesBannerDescription": "站点是到远程网络的连接,使 Pangolin 能够向任何位置的用户提提供公共或私有的资源访问。你可以在任何能够运行二进制文件或容器的地方安装站点网络连接器(Newt),以建立连接。",
|
||||
"sitesBannerButtonText": "安装站点",
|
||||
"approvalsBannerTitle": "批准或拒绝设备访问",
|
||||
"approvalsBannerDescription": "审核、批准或拒绝用户的设备访问请求。 当需要设备批准时,用户必须先获得管理员批准,然后他们的设备才能连接到您的组织资源。",
|
||||
@@ -117,6 +123,16 @@
|
||||
"siteUpdated": "站点已更新",
|
||||
"siteUpdatedDescription": "网站已更新。",
|
||||
"siteGeneralDescription": "配置此站点的常规设置",
|
||||
"siteRestartTitle": "重启站点",
|
||||
"siteRestartDescription": "重启此站点的WireGuard隧道。此操作将暂时中断连接。",
|
||||
"siteRestartBody": "如果站点隧道无法正常工作,并且您希望在不重启主机的情况下强制重新连接,请使用此选项。",
|
||||
"siteRestartButton": "重启站点",
|
||||
"siteRestartDialogMessage": "确定要重启<b>{name}</b>的WireGuard隧道吗?站点将暂时断开连接。",
|
||||
"siteRestartWarning": "隧道重启时,站点将暂时断开连接。",
|
||||
"siteRestarted": "站点已重启",
|
||||
"siteRestartedDescription": "WireGuard隧道已重启。",
|
||||
"siteErrorRestart": "重启站点失败",
|
||||
"siteErrorRestartDescription": "重启站点时发生错误。",
|
||||
"siteSettingDescription": "配置站点设置",
|
||||
"siteResourcesTab": "资源",
|
||||
"siteResourcesNoneOnSite": "此站点尚无公开或私人资源。",
|
||||
@@ -134,7 +150,7 @@
|
||||
"siteResourcesHowToAccess": "如何访问",
|
||||
"siteResourcesTargetsOnSite": "此站点上的目标",
|
||||
"siteSetting": "{siteName} 设置",
|
||||
"siteNewtTunnel": "新站点 (推荐)",
|
||||
"siteNewtTunnel": "新节点 (推荐)",
|
||||
"siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。",
|
||||
"siteWg": "基本 WireGuard",
|
||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||
@@ -143,23 +159,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": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
|
||||
@@ -204,11 +220,11 @@
|
||||
"proxyResourceTitle": "管理公共资源",
|
||||
"proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源",
|
||||
"publicResourcesBannerTitle": "基于 Web 的公共访问",
|
||||
"publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可以通过网络浏览器在互联网上的任何人访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
|
||||
"publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可供互联网上的任何人通过 Web 浏览器访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
|
||||
"clientResourceTitle": "管理私有资源",
|
||||
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
|
||||
"privateResourcesBannerTitle": "零信任的私人访问",
|
||||
"privateResourcesBannerDescription": "私人资源使用零信任安全性,确保只允许明确授予的用户和机器访问资源。可以连接用户设备或机器客户端,通过安全的虚拟专用网络访问这些资源。",
|
||||
"privateResourcesBannerTitle": "零信任私有访问",
|
||||
"privateResourcesBannerDescription": "私有资源采用零信任安全机制,确保只有获得明确授权的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
|
||||
"resourcesSearch": "搜索资源...",
|
||||
"resourceAdd": "添加资源",
|
||||
"resourceErrorDelte": "删除资源时出错",
|
||||
@@ -327,7 +343,7 @@
|
||||
"passToAuth": "传递至认证",
|
||||
"orgSettingsDescription": "配置组织设置",
|
||||
"orgGeneralSettings": "组织设置",
|
||||
"orgGeneralSettingsDescription": "管理机构的详细信息和配置",
|
||||
"orgGeneralSettingsDescription": "管理组织的详细信息和配置",
|
||||
"saveGeneralSettings": "保存常规设置",
|
||||
"saveSettings": "保存设置",
|
||||
"orgDangerZone": "危险区域",
|
||||
@@ -381,7 +397,7 @@
|
||||
"accessApprovalsDescription": "查看和管理待审批的组织访问权限",
|
||||
"description": "描述",
|
||||
"inviteTitle": "打开邀请",
|
||||
"inviteDescription": "管理其他用户加入机构的邀请",
|
||||
"inviteDescription": "管理其他用户加入组织的邀请",
|
||||
"inviteSearch": "搜索邀请...",
|
||||
"minutes": "分钟",
|
||||
"hours": "小时",
|
||||
@@ -425,24 +441,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": "保存预配键",
|
||||
@@ -462,16 +478,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": "管理所有用户",
|
||||
@@ -883,11 +899,11 @@
|
||||
"resourcesErrorUpdateDescription": "更新资源时出错",
|
||||
"access": "访问权限",
|
||||
"accessControl": "访问控制",
|
||||
"shareLink": "{resource} 可共享链接",
|
||||
"shareLink": "{resource} 的共享链接",
|
||||
"resourceSelect": "选择资源",
|
||||
"shareLinks": "可共享链接",
|
||||
"shareLinks": "共享链接",
|
||||
"share": "分享链接",
|
||||
"shareDescription2": "创建资源的可共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
|
||||
"shareDescription2": "创建资源的共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
|
||||
"shareEasyCreate": "轻松创建和分享",
|
||||
"shareConfigurableExpirationDuration": "可配置的过期时间",
|
||||
"shareSecureAndRevocable": "安全和可撤销的",
|
||||
@@ -1059,7 +1075,7 @@
|
||||
"network": "网络",
|
||||
"manage": "管理",
|
||||
"sitesNotFound": "未找到站点。",
|
||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||
"pangolinServerAdmin": "服务器管理 - Pangolin",
|
||||
"licenseTierProfessional": "专业许可证",
|
||||
"licenseTierEnterprise": "企业许可证",
|
||||
"licenseTierPersonal": "个人许可证",
|
||||
@@ -1366,7 +1382,7 @@
|
||||
"supportKeyBuy": "购买支持者密钥",
|
||||
"logoutError": "注销错误",
|
||||
"signingAs": "登录为",
|
||||
"serverAdmin": "服务器管理员",
|
||||
"serverAdmin": "服务器管理",
|
||||
"managedSelfhosted": "托管自托管",
|
||||
"otpEnable": "启用双因子认证",
|
||||
"otpDisable": "禁用双因子认证",
|
||||
@@ -1395,6 +1411,7 @@
|
||||
"actionApplyBlueprint": "应用蓝图",
|
||||
"actionListBlueprints": "列表蓝图",
|
||||
"actionGetBlueprint": "获取蓝图",
|
||||
"actionCreateOrgWideLauncherView": "创建组织范围的启动器视图",
|
||||
"setupToken": "设置令牌",
|
||||
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
||||
"setupTokenRequired": "需要设置令牌",
|
||||
@@ -1536,8 +1553,8 @@
|
||||
"sidebarSites": "站点",
|
||||
"sidebarApprovals": "审批请求",
|
||||
"sidebarResources": "资源",
|
||||
"sidebarProxyResources": "公开的",
|
||||
"sidebarClientResources": "非公开的",
|
||||
"sidebarProxyResources": "公开资源",
|
||||
"sidebarClientResources": "私有资源",
|
||||
"sidebarPolicies": "共享策略",
|
||||
"sidebarResourcePolicies": "公共资源",
|
||||
"sidebarAccessControl": "访问控制",
|
||||
@@ -1547,17 +1564,17 @@
|
||||
"sidebarAdmin": "管理员",
|
||||
"sidebarInvitations": "邀请",
|
||||
"sidebarRoles": "角色",
|
||||
"sidebarShareableLinks": "可共享链接",
|
||||
"sidebarShareableLinks": "共享链接",
|
||||
"sidebarApiKeys": "API密钥",
|
||||
"sidebarProvisioning": "置备中",
|
||||
"sidebarProvisioning": "预配",
|
||||
"sidebarSettings": "设置",
|
||||
"sidebarAllUsers": "所有用户",
|
||||
"sidebarIdentityProviders": "身份提供商",
|
||||
"sidebarLicense": "证书",
|
||||
"sidebarClients": "客户端",
|
||||
"sidebarUserDevices": "用户设备",
|
||||
"sidebarMachineClients": "机",
|
||||
"sidebarDomains": "域",
|
||||
"sidebarMachineClients": "机器身份",
|
||||
"sidebarDomains": "域名",
|
||||
"sidebarGeneral": "管理",
|
||||
"sidebarLogAndAnalytics": "日志与分析",
|
||||
"sidebarBluePrints": "蓝图",
|
||||
@@ -1689,8 +1706,8 @@
|
||||
"alertingTabHealthChecks": "健康检查",
|
||||
"alertingRulesBannerTitle": "获取通知",
|
||||
"alertingRulesBannerDescription": "每条规则都连接要监视的对象(站点、健康检查或资源),触发时间(例如离线或不健康),以及如何通过电子邮件、Webhooks 或集成将通知发送给团队。使用此列表创建、启用和管理这些规则。",
|
||||
"alertingHealthChecksBannerTitle": "监视健康和资源",
|
||||
"alertingHealthChecksBannerDescription": "健康检查是您一次定义的 HTTP 或 TCP 监控。然后可以将它们用作告警规则中的来源,以便目标变得正常或不正常时得到通知。资源上的健康检查也会出现在此处。",
|
||||
"alertingHealthChecksBannerTitle": "资源与健康监控",
|
||||
"alertingHealthChecksBannerDescription": "通过 HTTP 或 TCP 检查目标状态,并在服务异常或恢复时发送通知。资源中配置的健康检查也会显示在这里。",
|
||||
"standaloneHcTableTitle": "健康检查",
|
||||
"standaloneHcSearchPlaceholder": "搜索健康检查…",
|
||||
"standaloneHcAddButton": "创建健康检查",
|
||||
@@ -1791,17 +1808,17 @@
|
||||
"theme": "主题",
|
||||
"subnetRequired": "子网是必填项",
|
||||
"initialSetupTitle": "初始服务器设置",
|
||||
"initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
|
||||
"initialSetupDescription": "创建初始的管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
|
||||
"createAdminAccount": "创建管理员帐户",
|
||||
"setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。",
|
||||
"setupErrorCreateAdmin": "创建管理员账户时发生错误。",
|
||||
"certificateStatus": "证书",
|
||||
"certificateStatusAutoRefreshHint": "状态自动刷新。",
|
||||
"loading": "加载中",
|
||||
"loadingEllipsis": "加载中……",
|
||||
"loadingAnalytics": "加载分析",
|
||||
"restart": "重启",
|
||||
"domains": "域",
|
||||
"domainsDescription": "创建和管理组织中可用的域",
|
||||
"domains": "域名",
|
||||
"domainsDescription": "创建和管理组织中可用的域名",
|
||||
"domainsSearch": "搜索域...",
|
||||
"domainAdd": "添加域",
|
||||
"domainAddDescription": "注册一个新域名到组织",
|
||||
@@ -2071,6 +2088,7 @@
|
||||
"subnetPlaceholder": "子网",
|
||||
"addressDescription": "客户的内部地址。必须属于组织的子网。",
|
||||
"selectSites": "选择站点",
|
||||
"selectLabels": "选择标签",
|
||||
"sitesDescription": "客户端将与所选站点进行连接",
|
||||
"clientInstallOlm": "安装 Olm",
|
||||
"clientInstallOlmDescription": "在您的系统上运行 Olm",
|
||||
@@ -2165,12 +2183,12 @@
|
||||
"sshSudoMode": "Sudo 访问",
|
||||
"sshSudoModeNone": "无",
|
||||
"sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
|
||||
"sshSudoModeFull": "全苏多",
|
||||
"sshSudoModeFull": "完整 Sudo 权限",
|
||||
"sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
|
||||
"sshSudoModeCommands": "命令",
|
||||
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
|
||||
"sshSudo": "允许Sudo",
|
||||
"sshSudoCommands": "Sudo 命令",
|
||||
"sshSudoCommands": "可用 Sudo 命令",
|
||||
"sshSudoCommandsDescription": "用户可以使用 sudo 运行的命令列表,以逗号、空格或新行分隔。必须使用绝对路径。",
|
||||
"sshCreateHomeDir": "创建主目录",
|
||||
"sshUnixGroups": "Unix 组",
|
||||
@@ -2183,7 +2201,7 @@
|
||||
"roleTextImportAppend": "附加到现有",
|
||||
"roleTextImportMode": "导入模式",
|
||||
"roleTextImportPreview": "预览",
|
||||
"roleTextImportItemCount": "{count, plural, =0 {No items to import} one {1 item to import} other {# items to import}}",
|
||||
"roleTextImportItemCount": "{count, plural, =0 {没有可导入的项目} one {1 个可导入项目} other {# 个可导入项目}}",
|
||||
"roleTextImportTotalCount": "{existing} 个现有 + {imported} 个导入 = {total} 个总计",
|
||||
"roleTextImportConfirm": "导入",
|
||||
"roleTextImportInvalidFile": "不支持的文件类型",
|
||||
@@ -2235,8 +2253,8 @@
|
||||
"resourceEditDomain": "编辑域名",
|
||||
"siteName": "站点名称",
|
||||
"proxyPort": "端口",
|
||||
"resourcesTableProxyResources": "公开的",
|
||||
"resourcesTableClientResources": "非公开的",
|
||||
"resourcesTableProxyResources": "",
|
||||
"resourcesTableClientResources": "私有资源",
|
||||
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
|
||||
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
||||
"resourcesTableDestination": "目标",
|
||||
@@ -2298,6 +2316,7 @@
|
||||
"createInternalResourceDialogSite": "站点",
|
||||
"selectSite": "选择站点...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, other {# 个网站}}",
|
||||
"labelsSelectorLabelsCount": "{count, plural, other {# 标签}}",
|
||||
"noSitesFound": "未找到站点。",
|
||||
"createInternalResourceDialogProtocol": "协议",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
@@ -2338,6 +2357,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
|
||||
"internalResourceAliasLocalWarning": "以 .local 结尾的别名可能会因某些网络上的 mDNS 而导致解析问题。",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 资源需要方案",
|
||||
"internalResourceHttpPortRequired": "HTTP 资源需要目的端口",
|
||||
"siteConfiguration": "配置",
|
||||
@@ -2371,6 +2391,21 @@
|
||||
"sidebarRemoteExitNodes": "远程节点",
|
||||
"remoteExitNodeId": "ID",
|
||||
"remoteExitNodeSecretKey": "密钥",
|
||||
"remoteExitNodeNetworkingTitle": "网络设置",
|
||||
"remoteExitNodeNetworkingDescription": "配置此远程出口节点如何路由流量以及哪些站点优先通过其连接。高级功能可用于回程网络配置。",
|
||||
"remoteExitNodeNetworkingSave": "保存设置",
|
||||
"remoteExitNodeNetworkingSaveSuccessTitle": "网络设置已保存",
|
||||
"remoteExitNodeNetworkingSaveSuccessDescription": "网络设置已成功更新。",
|
||||
"remoteExitNodeNetworkingSaveError": "保存网络设置失败",
|
||||
"remoteExitNodeNetworkingSubnetsTitle": "远程子网",
|
||||
"remoteExitNodeNetworkingSubnetsDescription": "定义此远程出口节点将路由流量的CIDR范围。输入有效的CIDR(例如<code>10.0.0.0/8</code>)并按Enter键添加。",
|
||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "添加CIDR范围(例如10.0.0.0/8)",
|
||||
"remoteExitNodeNetworkingSubnetsLoadError": "无法加载子网",
|
||||
"remoteExitNodeNetworkingLabelsTitle": "首选标签",
|
||||
"remoteExitNodeNetworkingLabelsDescription": "拥有这些标签的站点将强制通过此远程出口节点连接。",
|
||||
"remoteExitNodeNetworkingLabelsButtonText": "选择标签……",
|
||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "搜索标签……",
|
||||
"remoteExitNodeNetworkingLabelsLoadError": "无法加载标签",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "创建远程节点",
|
||||
"description": "创建一个新的自托管远程中继和代理服务器节点",
|
||||
@@ -2549,6 +2584,7 @@
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"subnet": "子网",
|
||||
"utilitySubnet": "实用程序子网",
|
||||
"subnetDescription": "此组织网络配置的子网。",
|
||||
"customDomain": "自定义域",
|
||||
"authPage": "身份验证页面",
|
||||
@@ -2925,7 +2961,7 @@
|
||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||
"logRetentionAccessLabel": "访问日志保留",
|
||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||
"logRetentionActionLabel": "动作日志保留",
|
||||
"logRetentionActionLabel": "审计日志保留",
|
||||
"logRetentionActionDescription": "保留操作日志的时间",
|
||||
"logRetentionConnectionLabel": "连接日志保留",
|
||||
"logRetentionConnectionDescription": "保留连接日志的时间",
|
||||
@@ -2938,11 +2974,11 @@
|
||||
"logRetentionForever": "永远的",
|
||||
"logRetentionEndOfFollowingYear": "下一年结束",
|
||||
"actionLogsDescription": "查看此机构执行的操作历史",
|
||||
"accessLogsDescription": "查看此机构资源的访问认证请求",
|
||||
"accessLogsDescription": "查看此组织资源的访问认证请求",
|
||||
"connectionLogs": "连接日志",
|
||||
"connectionLogsDescription": "查看此机构隧道的连接日志",
|
||||
"sidebarLogsConnection": "连接日志",
|
||||
"sidebarLogsStreaming": "流流",
|
||||
"sidebarLogsStreaming": "事件流",
|
||||
"sourceAddress": "源地址",
|
||||
"destinationAddress": "目的地址",
|
||||
"duration": "期限",
|
||||
@@ -2967,6 +3003,7 @@
|
||||
"orgOrDomainIdMissing": "缺少机构或域 ID",
|
||||
"loadingDNSRecords": "正在载入DNS记录...",
|
||||
"olmUpdateAvailableInfo": "有最新版本的 Olm 可用。请更新到最新版本以获取最佳体验。",
|
||||
"updateAvailableInfo": "有新版本可用。请更新到最新版本以获得最佳体验。",
|
||||
"client": "客户端:",
|
||||
"proxyProtocol": "代理协议设置",
|
||||
"proxyProtocolDescription": "配置代理协议以保留TCP服务的客户端 IP 地址。",
|
||||
@@ -3533,6 +3570,55 @@
|
||||
"memberPortalEmailWhitelist": "电子邮件白名单",
|
||||
"memberPortalResourceDisabled": "资源已禁用",
|
||||
"memberPortalShowingResources": "显示 {start}-{end} 共 {total} 个资源",
|
||||
"resourceLauncherTitle": "资源启动器",
|
||||
"resourceLauncherDescription": "查看资源详情并从一个地方启动它们",
|
||||
"resourceLauncherSearchPlaceholder": "搜索所有站点……",
|
||||
"resourceLauncherDefaultView": "默认",
|
||||
"resourceLauncherSaveView": "保存视图",
|
||||
"resourceLauncherSaveToCurrentView": "保存至当前视图",
|
||||
"resourceLauncherResetView": "重置视图",
|
||||
"resourceLauncherSaveAsNewView": "另存为新视图",
|
||||
"resourceLauncherSaveAsNewViewDescription": "为此视图命名,以便保存您当前的过滤器和布局。",
|
||||
"resourceLauncherSaveForEveryone": "为所有人保存",
|
||||
"resourceLauncherSaveForEveryoneDescription": "与所有组织成员共享此视图。如果未选中,此视图仅对您可见。",
|
||||
"resourceLauncherMakePersonal": "创建个人",
|
||||
"resourceLauncherFilter": "筛选",
|
||||
"resourceLauncherSort": "排序",
|
||||
"resourceLauncherSortAscending": "按升序排序",
|
||||
"resourceLauncherSortDescending": "按降序排序",
|
||||
"resourceLauncherSettings": "设置",
|
||||
"resourceLauncherGroupBy": "按组",
|
||||
"resourceLauncherGroupBySite": "站点",
|
||||
"resourceLauncherGroupByLabel": "标签",
|
||||
"resourceLauncherLayout": "布局",
|
||||
"resourceLauncherLayoutGrid": "网格",
|
||||
"resourceLauncherLayoutList": "列表",
|
||||
"resourceLauncherShowLabels": "显示标签",
|
||||
"resourceLauncherShowSiteTags": "显示站点标签",
|
||||
"resourceLauncherShowRecents": "显示最近使用",
|
||||
"resourceLauncherDeleteView": "删除视图",
|
||||
"resourceLauncherViewAsAdmin": "以管理员身份查看",
|
||||
"resourceLauncherResourceDetailsDescription": "查看此资源的详细信息。",
|
||||
"resourceLauncherUnlabeled": "未标记",
|
||||
"resourceLauncherNoSite": "无站点",
|
||||
"resourceLauncherNoResourcesInGroup": "此组中没有资源",
|
||||
"resourceLauncherEmptyStateTitle": "没有可用资源",
|
||||
"resourceLauncherEmptyStateDescription": "您还没有访问任何资源。请联系您的管理员以请求访问。",
|
||||
"resourceLauncherEmptyStateNoResultsTitle": "未找到资源",
|
||||
"resourceLauncherEmptyStateNoResultsDescription": "没有资源与您当前的搜索或过滤器匹配。尝试调整它们以找到您想要的内容。",
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery": "没有资源匹配\"{query}\"。尝试调整您的搜索或清除过滤器以查看所有资源。",
|
||||
"resourceLauncherCopiedToClipboard": "已复制到剪贴板",
|
||||
"resourceLauncherCopiedAccessDescription": "资源访问权限已复制到剪贴板。",
|
||||
"resourceLauncherViewNamePlaceholder": "查看名称",
|
||||
"resourceLauncherViewNameLabel": "查看名称",
|
||||
"resourceLauncherViewSaved": "视图已保存",
|
||||
"resourceLauncherViewSavedDescription": "您的启动器视图已保存。",
|
||||
"resourceLauncherViewSaveFailed": "保存视图失败",
|
||||
"resourceLauncherViewSaveFailedDescription": "无法保存启动器视图。请再试一次。",
|
||||
"resourceLauncherViewDeleted": "视图已删除",
|
||||
"resourceLauncherViewDeletedDescription": "启动器视图已删除。",
|
||||
"resourceLauncherViewDeleteFailed": "删除视图失败",
|
||||
"resourceLauncherViewDeleteFailedDescription": "无法删除启动器视图。请再试一次。",
|
||||
"memberPortalPrevious": "上一页",
|
||||
"memberPortalNext": "下一页",
|
||||
"httpSettings": "HTTP 设置",
|
||||
@@ -3568,7 +3654,8 @@
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyRequired": "需要私钥",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "输入您的 VNC 密码以连接",
|
||||
"vncSignInDescription": "输入您的VNC凭据以连接",
|
||||
"vncUsernameOptional": "用户名(可选)",
|
||||
"vncPasswordOptional": "密码 (可选)",
|
||||
"vncNoResourceTarget": "没有可用的资源目标",
|
||||
"vncFailedToLoadNovnc": "加载 noVNC 失败",
|
||||
|
||||
Generated
+200
-238
@@ -70,7 +70,7 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.2.0",
|
||||
"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": "8.0.9",
|
||||
"nodemailer": "9.0.1",
|
||||
"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.0",
|
||||
"esbuild": "0.28.1",
|
||||
"esbuild-node-externals": "1.22.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
@@ -1248,9 +1248,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1265,9 +1265,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1282,9 +1282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1299,9 +1299,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1316,9 +1316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1333,9 +1333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1350,9 +1350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1367,9 +1367,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1384,9 +1384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1401,9 +1401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1418,9 +1418,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1435,9 +1435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1452,9 +1452,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -1469,9 +1469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1486,9 +1486,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1503,9 +1503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1537,9 +1537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1554,9 +1554,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1571,9 +1571,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1588,9 +1588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1605,9 +1605,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1622,9 +1622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1639,9 +1639,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1656,9 +1656,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1673,9 +1673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2076,9 +2076,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2095,9 +2092,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2114,9 +2108,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2133,9 +2124,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2152,9 +2140,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2187,9 +2172,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2222,9 +2204,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2247,9 +2226,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2272,9 +2248,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2297,9 +2270,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2322,9 +2292,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2369,9 +2336,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2664,9 +2628,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2683,9 +2644,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2941,9 +2899,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2960,9 +2915,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3200,9 +3152,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3219,9 +3168,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3582,9 +3528,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3605,9 +3548,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3628,9 +3568,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3651,9 +3588,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6873,9 +6807,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6892,9 +6823,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6911,9 +6839,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6930,9 +6855,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7200,9 +7122,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7220,9 +7139,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7296,6 +7212,72 @@
|
||||
"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",
|
||||
@@ -8448,9 +8430,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8465,9 +8444,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8482,9 +8458,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8499,9 +8472,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8516,9 +8486,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8533,9 +8500,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11261,9 +11225,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -11274,32 +11238,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-node-externals": {
|
||||
@@ -12191,16 +12155,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -12629,9 +12593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -13425,9 +13389,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -13767,9 +13741,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13791,9 +13762,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -14574,9 +14542,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.9.tgz",
|
||||
"integrity": "sha512-5ofa7BUN8+C+Hckh5V2GjeeOGRQBx0CJQA6KxrvuZfC8iU4/q7sLn8XrtEEhJkjV6HdyIiQs7Bba6bTao8JhkA==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
|
||||
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -15001,9 +14969,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -15020,9 +14985,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
+4
-4
@@ -93,7 +93,7 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.2.0",
|
||||
"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": "8.0.9",
|
||||
"nodemailer": "9.0.1",
|
||||
"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.0",
|
||||
"esbuild": "0.28.1",
|
||||
"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.0",
|
||||
"esbuild": "0.28.1",
|
||||
"dompurify": "3.4.0",
|
||||
"postcss": "8.5.15"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export enum ActionsEnum {
|
||||
getSite = "getSite",
|
||||
listSites = "listSites",
|
||||
updateSite = "updateSite",
|
||||
restartSite = "restartSite",
|
||||
resetSiteBandwidth = "resetSiteBandwidth",
|
||||
reGenerateSecret = "reGenerateSecret",
|
||||
createResource = "createResource",
|
||||
@@ -178,7 +179,8 @@ export enum ActionsEnum {
|
||||
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules"
|
||||
setResourcePolicyRules = "setResourcePolicyRules",
|
||||
createOrgWideLauncherView = "createOrgWideLauncherView"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
users
|
||||
} from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import type { RandomReader } from "@oslojs/crypto/random";
|
||||
import { generateRandomString } from "@oslojs/crypto/random";
|
||||
@@ -136,6 +136,45 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function invalidateAllSessionsExceptCurrent(
|
||||
userId: string,
|
||||
currentSessionId: string
|
||||
): Promise<void> {
|
||||
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,
|
||||
|
||||
@@ -795,10 +795,13 @@ export const COUNTRIES = [
|
||||
name: "Serbia",
|
||||
code: "RS"
|
||||
},
|
||||
{
|
||||
name: "Serbia and Montenegro",
|
||||
code: "CS"
|
||||
},
|
||||
// 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: "Seychelles",
|
||||
code: "SC"
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
pgTable,
|
||||
serial,
|
||||
varchar,
|
||||
unique,
|
||||
boolean,
|
||||
integer,
|
||||
bigint,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
primaryKey,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { InferSelectModel, sql } from "drizzle-orm";
|
||||
import {
|
||||
domains,
|
||||
orgs,
|
||||
@@ -19,12 +20,13 @@ import {
|
||||
roles,
|
||||
users,
|
||||
exitNodes,
|
||||
sessions,
|
||||
clients,
|
||||
resources,
|
||||
siteResources,
|
||||
targetHealthCheck,
|
||||
sites
|
||||
sites,
|
||||
clients,
|
||||
sessions,
|
||||
labels
|
||||
} from "./schema";
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
@@ -197,6 +199,42 @@ 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")
|
||||
@@ -207,17 +245,28 @@ 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"
|
||||
})
|
||||
});
|
||||
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 loginPageOrg = pgTable("loginPageOrg", {
|
||||
loginPageId: integer("loginPageId")
|
||||
|
||||
+511
-361
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { InferSelectModel, sql } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
@@ -25,7 +25,8 @@ export const domains = pgTable("domains", {
|
||||
certResolver: varchar("certResolver"),
|
||||
customCertResolver: varchar("customCertResolver"),
|
||||
preferWildcardCert: boolean("preferWildcardCert"),
|
||||
errorMessage: text("errorMessage")
|
||||
errorMessage: text("errorMessage"),
|
||||
lastCheckedAt: integer("lastCheckedAt")
|
||||
});
|
||||
|
||||
export const dnsRecords = pgTable("dnsRecords", {
|
||||
@@ -82,107 +83,133 @@ 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")
|
||||
});
|
||||
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 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 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 labels = pgTable("labels", {
|
||||
labelId: serial("labelId").primaryKey(),
|
||||
@@ -195,6 +222,20 @@ 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(),
|
||||
createdAt: varchar("createdAt").notNull(),
|
||||
updatedAt: varchar("updatedAt").notNull()
|
||||
});
|
||||
|
||||
export const siteLabels = pgTable(
|
||||
"siteLabels",
|
||||
{
|
||||
@@ -267,71 +308,84 @@ 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")
|
||||
});
|
||||
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, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
export const targetHealthCheck = pgTable(
|
||||
"targetHealthCheck",
|
||||
{
|
||||
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
||||
targetId: integer("targetId").references(() => targets.targetId, {
|
||||
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)
|
||||
});
|
||||
}),
|
||||
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)]
|
||||
);
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
exitNodeId: serial("exitNodeId").primaryKey(),
|
||||
@@ -348,101 +402,146 @@ 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"
|
||||
}),
|
||||
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"
|
||||
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"
|
||||
}),
|
||||
networkId: integer("networkId")
|
||||
.notNull()
|
||||
.references(() => networks.networkId, { 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)]
|
||||
);
|
||||
|
||||
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 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 roleSiteResources = pgTable("roleSiteResources", {
|
||||
roleId: integer("roleId")
|
||||
.notNull()
|
||||
.references(() => roles.roleId, { 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 userSiteResources = pgTable("userSiteResources", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { 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 users = pgTable("user", {
|
||||
userId: varchar("id").primaryKey(),
|
||||
@@ -467,15 +566,19 @@ 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"
|
||||
})
|
||||
});
|
||||
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 twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
|
||||
codeId: serial("id").primaryKey(),
|
||||
@@ -576,29 +679,49 @@ 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" })
|
||||
});
|
||||
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 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 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 roleSites = pgTable("roleSites", {
|
||||
roleId: integer("roleId")
|
||||
@@ -895,28 +1018,32 @@ export const resourcePolicyRules = pgTable("resourcePolicyRules", {
|
||||
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()
|
||||
});
|
||||
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 supporterKey = pgTable("supporterKey", {
|
||||
keyId: serial("keyId").primaryKey(),
|
||||
@@ -1004,40 +1131,47 @@ export const idpOrg = pgTable("idpOrg", {
|
||||
orgMapping: varchar("orgMapping")
|
||||
});
|
||||
|
||||
export const clients = pgTable("clients", {
|
||||
clientId: serial("clientId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
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
|
||||
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
|
||||
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"
|
||||
>()
|
||||
});
|
||||
}),
|
||||
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)
|
||||
]
|
||||
);
|
||||
|
||||
export const clientSitesAssociationsCache = pgTable(
|
||||
"clientSitesAssociationsCache",
|
||||
@@ -1049,7 +1183,11 @@ 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(
|
||||
@@ -1058,7 +1196,14 @@ 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", {
|
||||
@@ -1071,23 +1216,27 @@ 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)
|
||||
});
|
||||
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 currentFingerprint = pgTable("currentFingerprint", {
|
||||
fingerprintId: serial("id").primaryKey(),
|
||||
@@ -1440,6 +1589,7 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
export type Network = InferSelectModel<typeof networks>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type Label = InferSelectModel<typeof labels>;
|
||||
export type LauncherView = InferSelectModel<typeof launcherViews>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||
|
||||
+12
-50
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -12,68 +11,31 @@ 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 = <T extends (...args: any[]) => 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");
|
||||
}
|
||||
|
||||
// Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
|
||||
// retry loops that accumulate memory.
|
||||
sqlite.pragma("busy_timeout = 5000");
|
||||
// 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.
|
||||
|
||||
// 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 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.
|
||||
|
||||
// 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));
|
||||
};
|
||||
// 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.
|
||||
|
||||
return DrizzleSqlite(sqlite, {
|
||||
schema
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
clients,
|
||||
domains,
|
||||
exitNodes,
|
||||
labels,
|
||||
orgs,
|
||||
resources,
|
||||
roles,
|
||||
@@ -21,9 +22,6 @@ 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 }),
|
||||
@@ -195,6 +193,44 @@ 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")
|
||||
|
||||
@@ -20,8 +20,10 @@ 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")
|
||||
errorMessage: text("errorMessage"),
|
||||
lastCheckedAt: integer("lastCheckedAt")
|
||||
});
|
||||
|
||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||
@@ -221,6 +223,20 @@ 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(),
|
||||
createdAt: text("createdAt").notNull(),
|
||||
updatedAt: text("updatedAt").notNull()
|
||||
});
|
||||
|
||||
export const siteLabels = sqliteTable(
|
||||
"siteLabels",
|
||||
{
|
||||
@@ -1549,6 +1565,7 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type Label = InferSelectModel<typeof labels>;
|
||||
export type LauncherView = InferSelectModel<typeof launcherViews>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type ResourcePolicyPincode = InferSelectModel<
|
||||
typeof resourcePolicyPincode
|
||||
|
||||
@@ -24,6 +24,7 @@ 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();
|
||||
@@ -41,6 +42,7 @@ async function startServers() {
|
||||
|
||||
initLogCleanupInterval();
|
||||
initAcmeCertSync();
|
||||
startRebuildQueueProcessor();
|
||||
|
||||
// Start all servers
|
||||
const apiServer = createApiServer();
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
export enum FeatureId {
|
||||
export enum LimitId {
|
||||
USERS = "users",
|
||||
SITES = "sites",
|
||||
EGRESS_DATA_MB = "egressDataMb",
|
||||
DOMAINS = "domains",
|
||||
REMOTE_EXIT_NODES = "remoteExitNodes",
|
||||
ORGINIZATIONS = "organizations",
|
||||
ORGANIZATIONS = "organizations",
|
||||
PUBLIC_RESOURCES = "publicResources",
|
||||
PRIVATE_RESOURCES = "privateResources",
|
||||
MACHINE_CLIENTS = "machineClients",
|
||||
TIER1 = "tier1"
|
||||
}
|
||||
|
||||
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
|
||||
export async function getFeatureDisplayName(
|
||||
featureId: LimitId
|
||||
): Promise<string> {
|
||||
switch (featureId) {
|
||||
case FeatureId.USERS:
|
||||
case LimitId.USERS:
|
||||
return "Users";
|
||||
case FeatureId.SITES:
|
||||
case LimitId.SITES:
|
||||
return "Sites";
|
||||
case FeatureId.EGRESS_DATA_MB:
|
||||
case LimitId.EGRESS_DATA_MB:
|
||||
return "Egress Data (MB)";
|
||||
case FeatureId.DOMAINS:
|
||||
case LimitId.DOMAINS:
|
||||
return "Domains";
|
||||
case FeatureId.REMOTE_EXIT_NODES:
|
||||
case LimitId.REMOTE_EXIT_NODES:
|
||||
return "Remote Exit Nodes";
|
||||
case FeatureId.ORGINIZATIONS:
|
||||
case LimitId.ORGANIZATIONS:
|
||||
return "Organizations";
|
||||
case FeatureId.TIER1:
|
||||
case LimitId.PUBLIC_RESOURCES:
|
||||
return "Public Resources";
|
||||
case LimitId.PRIVATE_RESOURCES:
|
||||
return "Private Resources";
|
||||
case LimitId.MACHINE_CLIENTS:
|
||||
return "Machine Clients";
|
||||
case LimitId.TIER1:
|
||||
return "Home Lab";
|
||||
default:
|
||||
return featureId;
|
||||
@@ -30,15 +41,16 @@ export async function getFeatureDisplayName(featureId: FeatureId): Promise<strin
|
||||
}
|
||||
|
||||
// this is from the old system
|
||||
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
||||
export const FeatureMeterIds: Partial<Record<LimitId, string>> = {
|
||||
// right now we are not charging for any data
|
||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||
};
|
||||
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<LimitId, string>> = {
|
||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
|
||||
};
|
||||
|
||||
export function getFeatureMeterId(featureId: FeatureId): string | undefined {
|
||||
export function getFeatureMeterId(featureId: LimitId): string | undefined {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
@@ -49,22 +61,20 @@ export function getFeatureMeterId(featureId: FeatureId): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureIdByMetricId(
|
||||
metricId: string
|
||||
): FeatureId | undefined {
|
||||
return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find(
|
||||
export function getFeatureIdByMetricId(metricId: string): LimitId | undefined {
|
||||
return (Object.entries(FeatureMeterIds) as [LimitId, string][]).find(
|
||||
([_, v]) => v === metricId
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
||||
export type FeaturePriceSet = Partial<Record<LimitId, string>>;
|
||||
|
||||
export const tier1FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
|
||||
[LimitId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
|
||||
};
|
||||
|
||||
export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||
[LimitId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
|
||||
};
|
||||
|
||||
export function getTier1FeaturePriceSet(): FeaturePriceSet {
|
||||
@@ -79,11 +89,11 @@ export function getTier1FeaturePriceSet(): FeaturePriceSet {
|
||||
}
|
||||
|
||||
export const tier2FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
|
||||
[LimitId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN"
|
||||
};
|
||||
|
||||
export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||
[LimitId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
|
||||
};
|
||||
|
||||
export function getTier2FeaturePriceSet(): FeaturePriceSet {
|
||||
@@ -98,11 +108,11 @@ export function getTier2FeaturePriceSet(): FeaturePriceSet {
|
||||
}
|
||||
|
||||
export const tier3FeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
|
||||
[LimitId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv"
|
||||
};
|
||||
|
||||
export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||
[LimitId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
|
||||
};
|
||||
|
||||
export function getTier3FeaturePriceSet(): FeaturePriceSet {
|
||||
@@ -116,7 +126,7 @@ export function getTier3FeaturePriceSet(): FeaturePriceSet {
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||
export function getFeatureIdByPriceId(priceId: string): LimitId | undefined {
|
||||
// Check all feature price sets
|
||||
const allPriceSets = [
|
||||
getTier1FeaturePriceSet(),
|
||||
@@ -125,7 +135,7 @@ export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||
];
|
||||
|
||||
for (const priceSet of allPriceSets) {
|
||||
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
|
||||
const entry = (Object.entries(priceSet) as [LimitId, string][]).find(
|
||||
([_, price]) => price === priceId
|
||||
);
|
||||
if (entry) {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import Stripe from "stripe";
|
||||
import { FeatureId, FeaturePriceSet } from "./features";
|
||||
import { LimitId, FeaturePriceSet } from "./features";
|
||||
import { usageService } from "./usageService";
|
||||
|
||||
export async function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet,
|
||||
orgId: string,
|
||||
orgId: string
|
||||
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||
const users = await usageService.getUsage(orgId, LimitId.USERS);
|
||||
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||
let quantity: number | undefined;
|
||||
|
||||
if (featureId === FeatureId.USERS) {
|
||||
if (featureId === LimitId.USERS) {
|
||||
quantity = users?.instantaneousValue || 1;
|
||||
} else if (featureId === FeatureId.TIER1) {
|
||||
} else if (featureId === LimitId.TIER1) {
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,82 @@
|
||||
import { FeatureId } from "./features";
|
||||
import { LimitId } from "./features";
|
||||
|
||||
export type LimitSet = Partial<{
|
||||
[key in FeatureId]: {
|
||||
[key in LimitId]: {
|
||||
value: number | null; // null indicates no limit
|
||||
description?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export const freeLimitSet: LimitSet = {
|
||||
[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" },
|
||||
[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" }
|
||||
};
|
||||
|
||||
export const tier1LimitSet: LimitSet = {
|
||||
[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" },
|
||||
[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" }
|
||||
};
|
||||
|
||||
export const tier2LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: {
|
||||
[LimitId.USERS]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
[LimitId.SITES]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.DOMAINS]: {
|
||||
[LimitId.DOMAINS]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
[LimitId.REMOTE_EXIT_NODES]: {
|
||||
value: 3,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.ORGINIZATIONS]: {
|
||||
[LimitId.ORGANIZATIONS]: {
|
||||
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 = {
|
||||
[FeatureId.USERS]: {
|
||||
[LimitId.USERS]: {
|
||||
value: 250,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
[LimitId.SITES]: {
|
||||
value: 250,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.DOMAINS]: {
|
||||
[LimitId.DOMAINS]: {
|
||||
value: 100,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
[LimitId.REMOTE_EXIT_NODES]: {
|
||||
value: 20,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.ORGINIZATIONS]: {
|
||||
[LimitId.ORGANIZATIONS]: {
|
||||
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" }
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db, limits } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { LimitSet } from "./limitSet";
|
||||
import { FeatureId } from "./features";
|
||||
import { LimitId } from "./features";
|
||||
import logger from "@server/logger";
|
||||
|
||||
class LimitService {
|
||||
@@ -38,7 +38,7 @@ class LimitService {
|
||||
|
||||
async getOrgLimit(
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
featureId: LimitId
|
||||
): Promise<number | null> {
|
||||
const limitId = `${orgId}-${featureId}`;
|
||||
const [limit] = await db
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
Transaction,
|
||||
orgs
|
||||
} from "@server/db";
|
||||
import { FeatureId, getFeatureMeterId } from "./features";
|
||||
import { LimitId, getFeatureMeterId } from "./features";
|
||||
import logger from "@server/logger";
|
||||
import { build } from "@server/build";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
export function noop() {
|
||||
if (build !== "saas") {
|
||||
@@ -22,7 +22,6 @@ export function noop() {
|
||||
}
|
||||
|
||||
export class UsageService {
|
||||
|
||||
constructor() {
|
||||
if (noop()) {
|
||||
return;
|
||||
@@ -38,7 +37,7 @@ export class UsageService {
|
||||
|
||||
public async add(
|
||||
orgId: string,
|
||||
featureId: FeatureId,
|
||||
featureId: LimitId,
|
||||
value: number,
|
||||
transaction: any = null
|
||||
): Promise<Usage | null> {
|
||||
@@ -57,7 +56,10 @@ 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,
|
||||
@@ -112,7 +114,7 @@ export class UsageService {
|
||||
|
||||
private async internalAddUsage(
|
||||
orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount
|
||||
featureId: FeatureId,
|
||||
featureId: LimitId,
|
||||
value: number,
|
||||
trx: Transaction
|
||||
): Promise<Usage> {
|
||||
@@ -161,7 +163,7 @@ export class UsageService {
|
||||
|
||||
async updateCount(
|
||||
orgId: string,
|
||||
featureId: FeatureId,
|
||||
featureId: LimitId,
|
||||
value?: number,
|
||||
customerId?: string
|
||||
): Promise<void> {
|
||||
@@ -225,7 +227,7 @@ export class UsageService {
|
||||
|
||||
private async getCustomerId(
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
featureId: LimitId
|
||||
): Promise<string | null> {
|
||||
const orgIdToUse = await this.getBillingOrg(orgId);
|
||||
|
||||
@@ -267,18 +269,19 @@ export class UsageService {
|
||||
|
||||
public async getUsage(
|
||||
orgId: string,
|
||||
featureId: FeatureId,
|
||||
featureId: LimitId,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<Usage | null> {
|
||||
if (noop()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
|
||||
const usageId = `${orgIdToUse}-${featureId}`;
|
||||
|
||||
let orgIdToUse = orgId;
|
||||
try {
|
||||
orgIdToUse = await this.getBillingOrg(orgId, trx);
|
||||
|
||||
const usageId = `${orgIdToUse}-${featureId}`;
|
||||
|
||||
const [result] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
@@ -338,8 +341,12 @@ export class UsageService {
|
||||
`Failed to get usage for ${orgIdToUse}/${featureId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getBillingOrg(
|
||||
@@ -374,7 +381,7 @@ export class UsageService {
|
||||
|
||||
public async checkLimitSet(
|
||||
orgId: string,
|
||||
featureId?: FeatureId,
|
||||
featureId?: LimitId,
|
||||
usage?: Usage,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<boolean> {
|
||||
@@ -382,13 +389,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);
|
||||
|
||||
let orgLimits: Limit[] = [];
|
||||
if (featureId) {
|
||||
// Get all limits set for this organization
|
||||
@@ -422,7 +429,7 @@ export class UsageService {
|
||||
} else {
|
||||
currentUsage = await this.getUsage(
|
||||
orgIdToUse,
|
||||
limit.featureId as FeatureId,
|
||||
limit.featureId as LimitId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ import {
|
||||
newts,
|
||||
blueprints,
|
||||
Blueprint,
|
||||
Site,
|
||||
siteResources,
|
||||
roleSiteResources,
|
||||
userSiteResources,
|
||||
clientSiteResources
|
||||
} from "@server/db";
|
||||
import { Config, ConfigSchema } from "./types";
|
||||
import { Config, ConfigSchema, isTargetsOnlyResource } from "./types";
|
||||
import {
|
||||
PublicResourcesResults,
|
||||
updatePublicResources
|
||||
@@ -30,8 +29,17 @@ 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 } from "@server/routers/siteResource";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
|
||||
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";
|
||||
|
||||
type ApplyBlueprintArgs = {
|
||||
orgId: string;
|
||||
@@ -48,42 +56,39 @@ export async function applyBlueprint({
|
||||
name,
|
||||
source = "API"
|
||||
}: ApplyBlueprintArgs): Promise<Blueprint> {
|
||||
// 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: string;
|
||||
let blueprintMessage = "";
|
||||
let error: any | null = null;
|
||||
|
||||
try {
|
||||
let proxyResourcesResults: PublicResourcesResults = [];
|
||||
let clientResourcesResults: ClientResourcesResults = [];
|
||||
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 = [];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await updateResourcePolicies(orgId, config, trx);
|
||||
|
||||
proxyResourcesResults = await updatePublicResources(
|
||||
publicResourcesResults = await updatePublicResources(
|
||||
orgId,
|
||||
config,
|
||||
trx,
|
||||
siteId
|
||||
);
|
||||
clientResourcesResults = await updatePrivateResources(
|
||||
privateResourcesResults = 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 proxyResourcesResults) {
|
||||
for (const result of publicResourcesResults) {
|
||||
for (const target of result.targetsToUpdate) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
@@ -136,166 +141,37 @@ export async function applyBlueprint({
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
|
||||
`Successfully updated public resources for org ${orgId}: ${JSON.stringify(publicResourcesResults)}`
|
||||
);
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
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
|
||||
)
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
result.newSiteResource.siteResourceId
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.roleId));
|
||||
|
||||
const existingUserIds = await trx
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
userSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((s) => s.siteId),
|
||||
result.newSites.map((s) => s.siteId)
|
||||
)
|
||||
.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
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
|
||||
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
|
||||
// );
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated private resources for org ${orgId}: ${JSON.stringify(privateResourcesResults)}`
|
||||
);
|
||||
});
|
||||
|
||||
blueprintSucceeded = true;
|
||||
@@ -303,7 +179,9 @@ export async function applyBlueprint({
|
||||
} catch (err) {
|
||||
blueprintSucceeded = false;
|
||||
blueprintMessage = `Blueprint applied with errors: ${err}`;
|
||||
logger.error(blueprintMessage);
|
||||
logger.debug(
|
||||
`Org ${orgId} blueprint apply issues: ${blueprintMessage}`
|
||||
);
|
||||
error = err;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ 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,
|
||||
@@ -413,6 +419,34 @@ 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<void>) | null = null;
|
||||
if (
|
||||
@@ -609,6 +643,8 @@ 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,
|
||||
|
||||
@@ -51,6 +51,11 @@ import { build } from "@server/build";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import serverConfig from "@server/lib/config";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import next from "next";
|
||||
import { LimitId } from "../billing";
|
||||
import { usageService } from "../billing/usageService";
|
||||
|
||||
export type PublicResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
@@ -1005,6 +1010,33 @@ 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 || "")
|
||||
@@ -1294,6 +1326,8 @@ export async function updatePublicResources(
|
||||
await createTarget(newResource.resourceId, targetData);
|
||||
}
|
||||
|
||||
await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
|
||||
|
||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
db,
|
||||
olms,
|
||||
orgs,
|
||||
primaryDb,
|
||||
roleClients,
|
||||
roles,
|
||||
Transaction,
|
||||
@@ -23,415 +24,427 @@ import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations
|
||||
import { OlmErrorCodes } from "@server/routers/olm/error";
|
||||
import { tierMatrix } from "./billing/tierMatrix";
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
type ClientRow = typeof clients.$inferSelect;
|
||||
|
||||
function runQueuedClientAssociationRebuilds(
|
||||
userId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<void> {
|
||||
const execute = async (transaction: Transaction | typeof db) => {
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<
|
||||
string,
|
||||
typeof roles.$inferSelect | null
|
||||
>();
|
||||
const exitNodesCache = new Map<
|
||||
string,
|
||||
Awaited<ReturnType<typeof listExitNodes>>
|
||||
>();
|
||||
const isOrgLicensedCache = new Map<string, boolean>();
|
||||
const existingClientCache = new Map<
|
||||
string,
|
||||
typeof clients.$inferSelect | null
|
||||
>();
|
||||
const roleClientAccessCache = new Map<string, boolean>();
|
||||
const userClientAccessCache = new Map<string, boolean>();
|
||||
queuedClients: ClientRow[]
|
||||
) {
|
||||
if (queuedClients.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 uniqueClientsById = new Map<number, ClientRow>();
|
||||
for (const client of queuedClients) {
|
||||
uniqueClientsById.set(client.clientId, client);
|
||||
}
|
||||
|
||||
const getOrg = async (orgId: string) => {
|
||||
if (orgCache.has(orgId)) {
|
||||
return orgCache.get(orgId) ?? null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const [adminRole] = await transaction
|
||||
.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
|
||||
for (const client of uniqueClientsById.values()) {
|
||||
rebuildClientAssociationsFromClient(client).catch((error) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for client ${client.clientId} (user ${userId}): ${String(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
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<string, boolean>();
|
||||
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;
|
||||
}
|
||||
|
||||
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 (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,
|
||||
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(
|
||||
`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<typeof clients> = {
|
||||
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(
|
||||
`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);
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
|
||||
);
|
||||
}
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
|
||||
const queuedAssociationRebuilds: ClientRow[] = [];
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<string, typeof roles.$inferSelect | null>();
|
||||
const exitNodesCache = new Map<
|
||||
string,
|
||||
Awaited<ReturnType<typeof listExitNodes>>
|
||||
>();
|
||||
const isOrgLicensedCache = new Map<string, boolean>();
|
||||
const existingClientCache = new Map<
|
||||
string,
|
||||
typeof clients.$inferSelect | null
|
||||
>();
|
||||
const roleClientAccessCache = new Map<string, boolean>();
|
||||
const userClientAccessCache = new Map<string, boolean>();
|
||||
|
||||
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<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<string, boolean>();
|
||||
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;
|
||||
}
|
||||
|
||||
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 (existingClient) {
|
||||
// Ensure admin role has access to the client
|
||||
const hasRoleAccess = await hasRoleClientAccess(
|
||||
adminRole.roleId,
|
||||
existingClient.clientId
|
||||
);
|
||||
|
||||
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})`
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure user has access to the client
|
||||
const hasUserAccess = await hasUserClientAccess(
|
||||
userId,
|
||||
existingClient.clientId
|
||||
);
|
||||
|
||||
if (!hasUserAccess) {
|
||||
await trx.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(
|
||||
`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, 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<typeof clients> = {
|
||||
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,
|
||||
trx,
|
||||
userOrgIds,
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
|
||||
runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
|
||||
}
|
||||
|
||||
async function cleanupOrphanedClients(
|
||||
userId: string,
|
||||
trx: Transaction | typeof db,
|
||||
userOrgIds: string[] = []
|
||||
userOrgIds: string[] = [],
|
||||
queuedAssociationRebuilds: ClientRow[] = []
|
||||
): Promise<void> {
|
||||
// Find all OLM clients for this user that should be deleted
|
||||
// If userOrgIds is empty, delete all OLM clients (user has no orgs)
|
||||
@@ -461,9 +474,9 @@ async function cleanupOrphanedClients(
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Rebuild associations for each deleted client to clean up related data
|
||||
// Queue deleted clients for post-trx association cleanup.
|
||||
for (const deletedClient of deletedClients) {
|
||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||
queuedAssociationRebuilds.push(deletedClient);
|
||||
|
||||
if (deletedClient.olmId) {
|
||||
await sendTerminateClient(
|
||||
|
||||
@@ -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 { FeatureId } from "./billing";
|
||||
import { LimitId } from "./billing";
|
||||
|
||||
export type DeleteOrgByIdResult = {
|
||||
deletedNewtIds: string[];
|
||||
@@ -140,7 +140,9 @@ 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);
|
||||
}
|
||||
@@ -152,7 +154,7 @@ export async function deleteOrgById(
|
||||
.where(inArray(domains.domainId, domainIdsToDelete));
|
||||
}
|
||||
|
||||
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 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 trx.delete(orgs).where(eq(orgs.orgId, orgId));
|
||||
|
||||
@@ -199,22 +201,22 @@ export async function deleteOrgById(
|
||||
if (org.billingOrgId) {
|
||||
usageService.updateCount(
|
||||
org.billingOrgId,
|
||||
FeatureId.DOMAINS,
|
||||
LimitId.DOMAINS,
|
||||
domainCount ?? 0
|
||||
);
|
||||
usageService.updateCount(
|
||||
org.billingOrgId,
|
||||
FeatureId.SITES,
|
||||
LimitId.SITES,
|
||||
siteCount ?? 0
|
||||
);
|
||||
usageService.updateCount(
|
||||
org.billingOrgId,
|
||||
FeatureId.USERS,
|
||||
LimitId.USERS,
|
||||
userCount ?? 0
|
||||
);
|
||||
usageService.updateCount(
|
||||
org.billingOrgId,
|
||||
FeatureId.REMOTE_EXIT_NODES,
|
||||
LimitId.REMOTE_EXIT_NODES,
|
||||
remoteExitNodeCount ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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<DeleteResourceResult[]> {
|
||||
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<number, Target[]>();
|
||||
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<number, TargetHealthCheck[]>();
|
||||
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<DeleteResourceResult | null> {
|
||||
const [result] = await performDeleteResources([resourceId], trx);
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
export async function runResourceDeleteSideEffects(
|
||||
result: DeleteResourceResult
|
||||
): Promise<void> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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<number[]> {
|
||||
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<number[]> {
|
||||
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<number> {
|
||||
const [publicCountResult, privateCountResult] = await Promise.all([
|
||||
trx
|
||||
.select({
|
||||
count: sql<number>`count(distinct ${targets.resourceId})`
|
||||
})
|
||||
.from(targets)
|
||||
.where(eq(targets.siteId, siteId)),
|
||||
trx
|
||||
.select({
|
||||
count: sql<number>`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<DeleteSiteAssociatedResourcesSideEffects> {
|
||||
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<void> {
|
||||
for (const result of sideEffects.resources) {
|
||||
await runResourceDeleteSideEffects(result);
|
||||
}
|
||||
|
||||
for (const removed of sideEffects.siteResources) {
|
||||
runSiteResourceDeleteSideEffects(removed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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<SiteResource[]> {
|
||||
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<SiteResource | null> {
|
||||
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
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,11 @@ export async function verifyExitNodeOrgAccess(
|
||||
export async function listExitNodes(
|
||||
orgId: string,
|
||||
filterOnline = false,
|
||||
noCloud = 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
|
||||
) {
|
||||
// TODO: pick which nodes to send and ping better than just all of them that are not remote
|
||||
const allExitNodes = await db
|
||||
|
||||
+3
-3
@@ -333,7 +333,7 @@ export async function getNextAvailableClientSubnet(
|
||||
if (!acquired) {
|
||||
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||
}
|
||||
const release = () => lockManager.releaseLock(lockKey);
|
||||
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||
|
||||
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);
|
||||
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||
|
||||
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);
|
||||
const release = () => lockManager.releaseLock(lockKey, acquired);
|
||||
|
||||
try {
|
||||
const existingAddresses = await db
|
||||
|
||||
+144
-24
@@ -1,28 +1,87 @@
|
||||
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<string, LocalLockRecord>();
|
||||
|
||||
export class LockManager {
|
||||
/**
|
||||
* 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<boolean> - true if lock acquired, false otherwise
|
||||
*/
|
||||
async acquireLock(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
private clearExpiredLocalLock(lockKey: string): void {
|
||||
const current = localLocks.get(lockKey);
|
||||
if (current && current.expiresAt <= Date.now()) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock using Lua script to ensure atomicity
|
||||
* Acquire a local in-process lock using an optimistic Map-based check.
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
* @param ttlMs - Time to live in milliseconds
|
||||
* @returns Promise<string | null> - a token identifying this specific acquisition
|
||||
* (truthy) on success, or null if the lock could not be acquired.
|
||||
*/
|
||||
async releaseLock(lockKey: string): Promise<void> {}
|
||||
async acquireLock(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 3,
|
||||
retryDelayMs: number = 100
|
||||
): Promise<string | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock previously acquired via acquireLock/acquireLockWithRetry.
|
||||
* @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<void> {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (existing && existing.owner === token) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force release a lock regardless of owner (use with caution)
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
*/
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {}
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a lock exists and get its info
|
||||
@@ -35,16 +94,44 @@ export class LockManager {
|
||||
ttl: number;
|
||||
owner?: string;
|
||||
}> {
|
||||
return { exists: true, ownedByMe: true, ttl: 0 };
|
||||
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]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the TTL of an existing lock owned by this worker
|
||||
* Extend the TTL of an existing lock, provided the token matches the
|
||||
* acquisition currently holding it.
|
||||
* @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<boolean> - true if extended successfully
|
||||
*/
|
||||
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||
async extendLock(
|
||||
lockKey: string,
|
||||
ttlMs: number,
|
||||
token: string
|
||||
): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -54,15 +141,34 @@ 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<boolean> - true if lock acquired
|
||||
* @returns Promise<string | null> - token if acquired, null otherwise
|
||||
*/
|
||||
async acquireLockWithRetry(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 5,
|
||||
baseDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
): Promise<string | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,16 +183,16 @@ export class LockManager {
|
||||
fn: () => Promise<T>,
|
||||
ttlMs: number = 30000
|
||||
): Promise<T> {
|
||||
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||
const token = await this.acquireLock(lockKey, ttlMs);
|
||||
|
||||
if (!acquired) {
|
||||
if (!token) {
|
||||
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.releaseLock(lockKey);
|
||||
await this.releaseLock(lockKey, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +205,21 @@ export class LockManager {
|
||||
activeLocksCount: number;
|
||||
locksOwnedByMe: number;
|
||||
}> {
|
||||
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const ORG_REBUILD_CONCURRENCY_LIMIT = 10;
|
||||
|
||||
const orgActiveRebuilds = new Map<string, number>();
|
||||
|
||||
export async function incrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
orgActiveRebuilds.set(orgId, (orgActiveRebuilds.get(orgId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
export async function decrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
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<number> {
|
||||
return orgActiveRebuilds.get(orgId) ?? 0;
|
||||
}
|
||||
|
||||
export async function checkOrgRebuildRateLimit(orgId: string): Promise<boolean> {
|
||||
return (orgActiveRebuilds.get(orgId) ?? 0) >= ORG_REBUILD_CONCURRENCY_LIMIT;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
const MAX_RECURSION_DEPTH = 100;
|
||||
|
||||
const segmentRegexCache = new Map<string, RegExp>();
|
||||
|
||||
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);
|
||||
}
|
||||
+1008
-165
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
import logger from "@server/logger";
|
||||
|
||||
export type RebuildJobType = "site-resource" | "client";
|
||||
|
||||
export interface RebuildJob {
|
||||
type: RebuildJobType;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface RebuildJobHandlers {
|
||||
onSiteResource(siteResourceId: number): Promise<void>;
|
||||
onClient(clientId: number): Promise<void>;
|
||||
}
|
||||
|
||||
export interface RebuildQueueManager {
|
||||
enqueue(job: RebuildJob): Promise<void>;
|
||||
startProcessing(handlers: RebuildJobHandlers): void;
|
||||
isQueued(job: RebuildJob): Promise<boolean>;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
function dedupeKey(job: RebuildJob): string {
|
||||
return `${job.type}:${job.id}`;
|
||||
}
|
||||
|
||||
class InMemoryRebuildQueue implements RebuildQueueManager {
|
||||
private queue: RebuildJob[] = [];
|
||||
private queuedSet = new Set<string>();
|
||||
private processing = false;
|
||||
private processingStarted = false;
|
||||
private handlers: RebuildJobHandlers | null = null;
|
||||
|
||||
async isQueued(job: RebuildJob): Promise<boolean> {
|
||||
return this.queuedSet.has(dedupeKey(job));
|
||||
}
|
||||
|
||||
async enqueue(job: RebuildJob): Promise<void> {
|
||||
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<void> {
|
||||
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) {
|
||||
logger.error(
|
||||
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.processing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const rebuildQueue: RebuildQueueManager = new InMemoryRebuildQueue();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { db, logsDb, statusHistory } from "@server/db";
|
||||
import { and, eq, gte, asc } from "drizzle-orm";
|
||||
import { and, eq, gte, lt, asc, desc } from "drizzle-orm";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||
@@ -42,7 +42,29 @@ export async function getCachedStatusHistory(
|
||||
)
|
||||
.orderBy(asc(statusHistory.timestamp));
|
||||
|
||||
const { buckets, totalDowntime } = computeBuckets(events, days);
|
||||
// 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 totalWindow = days * 86400;
|
||||
const overallUptime =
|
||||
totalWindow > 0
|
||||
@@ -110,7 +132,8 @@ export function computeBuckets(
|
||||
timestamp: number;
|
||||
id: number;
|
||||
}[],
|
||||
days: number
|
||||
days: number,
|
||||
priorStatus: string | null = null
|
||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -136,7 +159,10 @@ export function computeBuckets(
|
||||
.filter((e) => e.timestamp < dayStartSec)
|
||||
.at(-1);
|
||||
|
||||
const currentStatus = lastBeforeDay?.status ?? null;
|
||||
// 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 windows: { start: number; end: number | null; status: string }[] =
|
||||
[];
|
||||
|
||||
@@ -511,6 +511,12 @@ 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
|
||||
@@ -521,7 +527,8 @@ export class TraefikConfigManager {
|
||||
build == "saas"
|
||||
? false
|
||||
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
|
||||
build != "oss" // generate browser gateway targets on cloud and enterprise
|
||||
pangolinUIUrl, // generate maintenance pages on cloud and hybrid
|
||||
pangolinUIUrl // generate browser gateway targets on cloud and hybrid
|
||||
);
|
||||
|
||||
const domains = new Set<string>();
|
||||
|
||||
@@ -44,8 +44,8 @@ export async function getTraefikConfig(
|
||||
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
|
||||
allowBrowserGatewayResources = true
|
||||
maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
|
||||
browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
|
||||
): Promise<any> {
|
||||
// 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
|
||||
|
||||
@@ -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 { FeatureId } from "@server/lib/billing";
|
||||
import { LimitId } 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, FeatureId.USERS, 1, trx);
|
||||
await usageService.add(org.orgId, LimitId.USERS, 1, trx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export async function removeUserFromOrg(
|
||||
);
|
||||
|
||||
if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) {
|
||||
await usageService.add(org.orgId, FeatureId.USERS, -1, trx);
|
||||
await usageService.add(org.orgId, LimitId.USERS, -1, trx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { isValidUrlGlobPattern } from "./validators";
|
||||
import {
|
||||
getResourceRuleValueValidationError,
|
||||
isValidUrlGlobPattern
|
||||
} from "./validators";
|
||||
import { assertEquals } from "@test/assert";
|
||||
|
||||
function runTests() {
|
||||
@@ -236,6 +239,43 @@ 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!");
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ export function getResourceRuleValueValidationError(
|
||||
? null
|
||||
: "Invalid country code provided";
|
||||
case "ASN":
|
||||
return /^AS\d+$/i.test(value.trim())
|
||||
const normalizedValue = value.trim().toUpperCase();
|
||||
return /^AS\d+$/.test(normalizedValue) ||
|
||||
normalizedValue === "ALL" ||
|
||||
normalizedValue === "AS0"
|
||||
? null
|
||||
: "Invalid ASN provided";
|
||||
default:
|
||||
|
||||
@@ -119,8 +119,7 @@ export async function verifyAccessTokenAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@ export async function verifyAdmin(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,8 +113,7 @@ export async function verifyApiKeyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,8 +107,7 @@ export async function verifyClientAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -129,10 +128,7 @@ export async function verifyClientAccess(
|
||||
.where(
|
||||
and(
|
||||
eq(roleClients.clientId, client.clientId),
|
||||
inArray(
|
||||
roleClients.roleId,
|
||||
req.userOrgRoleIds!
|
||||
)
|
||||
inArray(roleClients.roleId, req.userOrgRoleIds!)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
@@ -88,8 +88,7 @@ export async function verifyDomainAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -59,8 +60,7 @@ export async function verifyOrgAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,8 +105,7 @@ export async function verifyResourceAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,8 +102,7 @@ export async function verifyResourcePolicyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,8 +132,7 @@ export async function verifyRoleAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ export async function verifySetResourceClients(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ export async function verifySetResourceUsers(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,8 +115,7 @@ export async function verifySiteAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,8 +115,7 @@ export async function verifySiteProvisioningKeyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ export async function verifySiteResourceAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,8 +122,7 @@ export async function verifyTargetAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,8 +59,7 @@ export async function verifyUserAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -693,9 +693,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 cache from "#private/lib/cache";
|
||||
import { regionalCache as cache } from "#private/lib/cache";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Define the return type for clarity and type safety
|
||||
|
||||
@@ -21,6 +21,49 @@ 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
|
||||
@@ -36,13 +79,17 @@ export function enforceResourceSessionLength(
|
||||
if (sessionAgeMs > maxSessionLengthMs) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Resource session has expired due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
error: `Your resource session has expired. ${formatMaxSessionLengthRequirement(
|
||||
maxSessionLengthHours
|
||||
)}`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Resource session is invalid due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
error: `Your resource session is invalid. ${formatMaxSessionLengthRequirement(
|
||||
maxSessionLengthHours
|
||||
)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -60,14 +107,20 @@ export async function checkOrgAccessPolicy(
|
||||
if (!orgId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Organization ID is required"
|
||||
error: "Unable to verify organization access. Organization information is missing."
|
||||
};
|
||||
}
|
||||
if (!userId) {
|
||||
return { allowed: false, error: "User ID is required" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Unable to verify organization access. User information is missing."
|
||||
};
|
||||
}
|
||||
if (!sessionId) {
|
||||
return { allowed: false, error: "Session ID is required" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your session is invalid. Please log in again."
|
||||
};
|
||||
}
|
||||
|
||||
if (build === "enterprise") {
|
||||
@@ -89,7 +142,10 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
props.org = orgQuery;
|
||||
if (!props.org) {
|
||||
return { allowed: false, error: "Organization not found" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "This organization could not be found."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +156,10 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(users.userId, userId));
|
||||
props.user = userQuery;
|
||||
if (!props.user) {
|
||||
return { allowed: false, error: "User not found" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your account could not be found."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +170,17 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(sessions.sessionId, sessionId));
|
||||
props.session = sessionQuery;
|
||||
if (!props.session) {
|
||||
return { allowed: false, error: "Session not found" };
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your session has expired. Please log in again."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (props.session.userId !== props.user.userId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Session does not belong to the user"
|
||||
error: "Your session is invalid. Please log in again."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,8 +249,14 @@ export async function checkOrgAccessPolicy(
|
||||
allowed = false;
|
||||
}
|
||||
|
||||
const policyError = buildOrgAccessPolicyError(policies);
|
||||
|
||||
return {
|
||||
allowed,
|
||||
policies
|
||||
policies,
|
||||
error: allowed
|
||||
? undefined
|
||||
: (policyError ??
|
||||
"You do not meet this organization's security requirements.")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,12 +18,15 @@ 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 } from "drizzle-orm";
|
||||
import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
|
||||
import axios from "axios";
|
||||
import config from "../config";
|
||||
|
||||
@@ -150,7 +153,8 @@ export async function verifyExitNodeOrgAccess(
|
||||
export async function listExitNodes(
|
||||
orgId: string,
|
||||
filterOnline = false,
|
||||
noCloud = false
|
||||
noCloud = false,
|
||||
siteId?: number
|
||||
) {
|
||||
const allExitNodes = await db
|
||||
.select({
|
||||
@@ -237,7 +241,7 @@ export async function listExitNodes(
|
||||
// })
|
||||
// );
|
||||
|
||||
const remoteExitNodes = allExitNodes.filter(
|
||||
let remoteExitNodesList = allExitNodes.filter(
|
||||
(node) =>
|
||||
node.type === "remoteExitNode" && (!filterOnline || node.online)
|
||||
);
|
||||
@@ -246,9 +250,82 @@ 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<string, Set<number>>();
|
||||
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 =
|
||||
remoteExitNodes.length > 0 ? remoteExitNodes : gerbilExitNodes;
|
||||
remoteExitNodesList.length > 0 ? remoteExitNodesList : gerbilExitNodes;
|
||||
|
||||
return exitNodesList;
|
||||
}
|
||||
|
||||
+140
-84
@@ -11,37 +11,80 @@
|
||||
* 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<string, LocalLockRecord>();
|
||||
|
||||
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<boolean> - true if lock acquired, false otherwise
|
||||
* @returns Promise<string | null> - a token identifying this specific acquisition
|
||||
* (truthy) on success, or null if the lock could not be acquired.
|
||||
*/
|
||||
async acquireLock(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 3,
|
||||
retryDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
): Promise<string | null> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -53,30 +96,8 @@ export class LockManager {
|
||||
);
|
||||
|
||||
if (result === "OK") {
|
||||
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;
|
||||
logger.debug(`Lock acquired: ${lockKey} by ${instanceId}`);
|
||||
return lockValue;
|
||||
}
|
||||
|
||||
// If this isn't our last attempt, wait before retrying with exponential backoff
|
||||
@@ -88,7 +109,10 @@ 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);
|
||||
@@ -100,27 +124,36 @@ export class LockManager {
|
||||
logger.debug(
|
||||
`Failed to acquire lock ${lockKey} after ${maxRetries} attempts`
|
||||
);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock using Lua script to ensure atomicity
|
||||
* 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.
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
* @param token - the exact token returned by the acquisition being released
|
||||
*/
|
||||
async releaseLock(lockKey: string): Promise<void> {
|
||||
async releaseLock(lockKey: string, token: string): Promise<void> {
|
||||
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 worker_prefix = ARGV[1]
|
||||
local expected_value = ARGV[1]
|
||||
local current_value = redis.call('GET', key)
|
||||
|
||||
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
|
||||
if current_value and current_value == expected_value then
|
||||
return redis.call('DEL', key)
|
||||
else
|
||||
return 0
|
||||
@@ -132,20 +165,14 @@ export class LockManager {
|
||||
luaScript,
|
||||
1,
|
||||
redisKey,
|
||||
`${instanceId}:`
|
||||
token
|
||||
)) 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 - not owned by worker: ${lockKey} by ${
|
||||
instanceId
|
||||
}`
|
||||
`Lock not released - token did not match current holder: ${lockKey} (attempted by ${instanceId})`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -159,6 +186,7 @@ export class LockManager {
|
||||
*/
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
localLocks.delete(lockKey);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -186,7 +214,20 @@ export class LockManager {
|
||||
owner?: string;
|
||||
}> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return { exists: false, ownedByMe: true, ttl: 0 };
|
||||
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]
|
||||
};
|
||||
}
|
||||
|
||||
const redisKey = `lock:${lockKey}`;
|
||||
@@ -198,11 +239,7 @@ 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 {
|
||||
@@ -218,26 +255,40 @@ export class LockManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the TTL of an existing lock owned by this worker
|
||||
* Extend the TTL of an existing lock, provided the token matches the
|
||||
* acquisition currently holding it.
|
||||
* @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<boolean> - true if extended successfully
|
||||
*/
|
||||
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||
async extendLock(
|
||||
lockKey: string,
|
||||
ttlMs: number,
|
||||
token: string
|
||||
): Promise<boolean> {
|
||||
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 worker_prefix = ARGV[1]
|
||||
local expected_value = ARGV[1]
|
||||
local ttl = tonumber(ARGV[2])
|
||||
local current_value = redis.call('GET', key)
|
||||
|
||||
if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then
|
||||
if current_value and current_value == expected_value then
|
||||
return redis.call('PEXPIRE', key, ttl)
|
||||
else
|
||||
return 0
|
||||
@@ -249,15 +300,13 @@ export class LockManager {
|
||||
luaScript,
|
||||
1,
|
||||
redisKey,
|
||||
`${instanceId}:`,
|
||||
token,
|
||||
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;
|
||||
}
|
||||
@@ -274,23 +323,24 @@ 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<boolean> - true if lock acquired
|
||||
* @returns Promise<string | null> - token if acquired, null otherwise
|
||||
*/
|
||||
async acquireLockWithRetry(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 5,
|
||||
baseDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return true;
|
||||
}
|
||||
|
||||
): Promise<string | null> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||
const acquired = await this.acquireLock(
|
||||
lockKey,
|
||||
ttlMs,
|
||||
1,
|
||||
baseDelayMs
|
||||
);
|
||||
|
||||
if (acquired) {
|
||||
return true;
|
||||
return acquired;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
@@ -304,7 +354,7 @@ export class LockManager {
|
||||
logger.warn(
|
||||
`Failed to acquire lock ${lockKey} after ${maxRetries + 1} attempts`
|
||||
);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,20 +369,16 @@ export class LockManager {
|
||||
fn: () => Promise<T>,
|
||||
ttlMs: number = 30000
|
||||
): Promise<T> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return await fn();
|
||||
}
|
||||
const token = await this.acquireLock(lockKey, ttlMs);
|
||||
|
||||
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||
|
||||
if (!acquired) {
|
||||
if (!token) {
|
||||
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.releaseLock(lockKey);
|
||||
await this.releaseLock(lockKey, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +392,21 @@ export class LockManager {
|
||||
locksOwnedByMe: number;
|
||||
}> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||
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 };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -356,11 +416,7 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 = 5;
|
||||
|
||||
// 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<string, number>();
|
||||
|
||||
function isRedisReady(): boolean {
|
||||
return !!(redis && redis.status === "ready");
|
||||
}
|
||||
|
||||
export async function incrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<number> {
|
||||
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<boolean> {
|
||||
return (
|
||||
(await getOrgActiveRebuildCount(orgId)) >= ORG_REBUILD_CONCURRENCY_LIMIT
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import redisManager from "#private/lib/redis";
|
||||
import { regionalRedisManager as redisManager } from "#private/lib/redis";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Rate limiting configuration
|
||||
@@ -152,10 +152,9 @@ export class RateLimitService {
|
||||
);
|
||||
|
||||
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||
if (redisManager.getClient()) {
|
||||
await redisManager
|
||||
.getClient()
|
||||
.expire(globalKey, RATE_LIMIT_WINDOW + 10);
|
||||
const writeClient = redisManager.getClient();
|
||||
if (writeClient) {
|
||||
await writeClient.expire(globalKey, RATE_LIMIT_WINDOW + 10);
|
||||
}
|
||||
|
||||
// Update tracking
|
||||
@@ -204,10 +203,12 @@ export class RateLimitService {
|
||||
);
|
||||
|
||||
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||
if (redisManager.getClient()) {
|
||||
await redisManager
|
||||
.getClient()
|
||||
.expire(messageTypeKey, RATE_LIMIT_WINDOW + 10);
|
||||
const writeClient = redisManager.getClient();
|
||||
if (writeClient) {
|
||||
await writeClient.expire(
|
||||
messageTypeKey,
|
||||
RATE_LIMIT_WINDOW + 10
|
||||
);
|
||||
}
|
||||
|
||||
// Update tracking
|
||||
@@ -487,16 +488,13 @@ export class RateLimitService {
|
||||
await redisManager.del(globalKey);
|
||||
|
||||
// Get all message type keys for this client and delete them
|
||||
const client = redisManager.getClient();
|
||||
if (client) {
|
||||
const messageTypeKeys = await client.keys(
|
||||
`ratelimit:${clientId}:*`
|
||||
const messageTypeKeys = await redisManager.keys(
|
||||
`ratelimit:${clientId}:*`
|
||||
);
|
||||
if (messageTypeKeys.length > 0) {
|
||||
await Promise.all(
|
||||
messageTypeKeys.map((key) => redisManager.del(key))
|
||||
);
|
||||
if (messageTypeKeys.length > 0) {
|
||||
await Promise.all(
|
||||
messageTypeKeys.map((key) => redisManager.del(key))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
export type RebuildJobType = "site-resource" | "client";
|
||||
|
||||
export interface RebuildJob {
|
||||
type: RebuildJobType;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface RebuildJobHandlers {
|
||||
onSiteResource(siteResourceId: number): Promise<void>;
|
||||
onClient(clientId: number): Promise<void>;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
class RedisRebuildQueue {
|
||||
private processingStarted = false;
|
||||
|
||||
async isQueued(job: RebuildJob): Promise<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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) {
|
||||
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();
|
||||
@@ -1000,6 +1000,45 @@ class RegionalRedisManager {
|
||||
}
|
||||
}
|
||||
|
||||
public getClient(): Redis | null {
|
||||
return this.writeClient;
|
||||
}
|
||||
|
||||
public async hget(key: string, field: string): Promise<string | null> {
|
||||
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<boolean> {
|
||||
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<Record<string, string>> {
|
||||
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<void> {
|
||||
try {
|
||||
if (this.writeClient) {
|
||||
|
||||
@@ -84,8 +84,8 @@ export async function getTraefikConfig(
|
||||
filterOutNamespaceDomains = false,
|
||||
generateLoginPageRouters = false,
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true,
|
||||
allowBrowserGatewayResources = true
|
||||
maintenancePageUiUrl: string | null = null,
|
||||
browserGatewayUiUrl: string | null = null
|
||||
): Promise<any> {
|
||||
// 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 (allowBrowserGatewayResources) {
|
||||
if (browserGatewayUiUrl) {
|
||||
for (const row of resourcesWithTargetsAndSites) {
|
||||
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
|
||||
continue;
|
||||
@@ -630,10 +630,11 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
if (showMaintenancePage && allowMaintenancePage) {
|
||||
if (showMaintenancePage && maintenancePageUiUrl) {
|
||||
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;
|
||||
@@ -646,15 +647,11 @@ 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: `http://${maintenanceHost}:${maintenancePort}`
|
||||
url: maintenancePageUiUrl
|
||||
}
|
||||
],
|
||||
passHostHeader: true
|
||||
@@ -673,12 +670,26 @@ 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],
|
||||
middlewares: [
|
||||
rewriteMiddlewareName,
|
||||
maintenanceHeadersMiddlewareName
|
||||
],
|
||||
rule: rule,
|
||||
priority: 2000,
|
||||
...(resource.ssl ? { tls } : {})
|
||||
@@ -691,6 +702,7 @@ 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 } : {})
|
||||
@@ -1027,7 +1039,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
if (allowBrowserGatewayResources) {
|
||||
if (browserGatewayUiUrl) {
|
||||
// Generate Traefik config for browser gateway resources
|
||||
const browserGatewayPort = 39999;
|
||||
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
|
||||
@@ -1119,20 +1131,17 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
if (showBgMaintenancePage && allowMaintenancePage) {
|
||||
if (showBgMaintenancePage && maintenancePageUiUrl) {
|
||||
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)
|
||||
@@ -1144,7 +1153,7 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
||||
url: maintenancePageUiUrl
|
||||
}
|
||||
],
|
||||
passHostHeader: true
|
||||
@@ -1158,12 +1167,26 @@ 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],
|
||||
middlewares: [
|
||||
bgRewriteMiddlewareName,
|
||||
bgMaintenanceHeadersMiddlewareName
|
||||
],
|
||||
rule: hostRule,
|
||||
priority: 2000,
|
||||
...(bgResource.ssl ? { tls } : {})
|
||||
@@ -1176,6 +1199,7 @@ 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 } : {})
|
||||
@@ -1234,9 +1258,8 @@ 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;
|
||||
@@ -1252,22 +1275,33 @@ 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: `http://${internalHost}:${internalPort}`
|
||||
url: browserGatewayUiUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Assets router at higher priority so /_next files load without rewrite
|
||||
// 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.
|
||||
config_output.http.routers![
|
||||
`bg-r${bgResource.resourceId}-assets-router`
|
||||
] = {
|
||||
entryPoints: [entrypoint],
|
||||
middlewares: routerMiddlewares,
|
||||
middlewares: [...routerMiddlewares, uiHeadersMiddlewareName],
|
||||
service: bgUiServiceName,
|
||||
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||
priority: 101,
|
||||
@@ -1279,7 +1313,11 @@ export async function getTraefikConfig(
|
||||
`bg-r${bgResource.resourceId}-ui-router`
|
||||
] = {
|
||||
entryPoints: [entrypoint],
|
||||
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName],
|
||||
middlewares: [
|
||||
...routerMiddlewares,
|
||||
uiRewriteMiddlewareName,
|
||||
uiHeadersMiddlewareName
|
||||
],
|
||||
service: bgUiServiceName,
|
||||
rule: hostRule,
|
||||
priority: 100,
|
||||
@@ -1312,10 +1350,6 @@ 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 = {};
|
||||
}
|
||||
@@ -1331,7 +1365,7 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
||||
url: maintenancePageUiUrl
|
||||
}
|
||||
],
|
||||
passHostHeader: true
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from "./queryAccessAuditLog";
|
||||
export * from "./exportAccessAuditLog";
|
||||
export * from "./queryConnectionAuditLog";
|
||||
export * from "./exportConnectionAuditLog";
|
||||
export * from "./logAccessAuditAttempt";
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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<any> {
|
||||
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<null>(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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 } from "drizzle-orm";
|
||||
import { eq, gt, lt, and, count, desc, inArray, isNull, or } from "drizzle-orm";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { z } from "zod";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -120,7 +120,10 @@ function getWhere(data: Q) {
|
||||
lt(accessAuditLog.timestamp, data.timeEnd),
|
||||
eq(accessAuditLog.orgId, data.orgId),
|
||||
data.resourceId
|
||||
? eq(accessAuditLog.resourceId, data.resourceId)
|
||||
? or(
|
||||
eq(accessAuditLog.resourceId, data.resourceId),
|
||||
eq(accessAuditLog.siteResourceId, data.resourceId)
|
||||
)
|
||||
: undefined,
|
||||
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
|
||||
data.actorType
|
||||
@@ -233,7 +236,6 @@ async function enrichWithResourceDetails(
|
||||
const details = siteResourceMap.get(log.siteResourceId);
|
||||
return {
|
||||
...log,
|
||||
resourceId: log.siteResourceId,
|
||||
resourceName: details?.name ?? null,
|
||||
resourceNiceId: details?.niceId ?? null
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
getTier1FeaturePriceSet,
|
||||
getTier3FeaturePriceSet,
|
||||
getTier2FeaturePriceSet,
|
||||
FeatureId,
|
||||
LimitId,
|
||||
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[FeatureId.USERS];
|
||||
const newPriceId = targetPriceSet[LimitId.USERS];
|
||||
|
||||
if (newPriceId) {
|
||||
return {
|
||||
|
||||
@@ -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 { FeatureId } from "@server/lib/billing";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
import { GetOrgUsageResponse } from "@server/routers/billing/types";
|
||||
|
||||
const getOrgSchema = z.strictObject({
|
||||
@@ -93,16 +93,28 @@ export async function getOrgUsage(
|
||||
// Get usage for org
|
||||
const usageData = [];
|
||||
|
||||
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 sites = await usageService.getUsage(orgId, LimitId.SITES);
|
||||
const users = await usageService.getUsage(orgId, LimitId.USERS);
|
||||
const domains = await usageService.getUsage(orgId, LimitId.DOMAINS);
|
||||
const remoteExitNodes = await usageService.getUsage(
|
||||
orgId,
|
||||
FeatureId.REMOTE_EXIT_NODES
|
||||
LimitId.REMOTE_EXIT_NODES
|
||||
);
|
||||
const organizations = await usageService.getUsage(
|
||||
orgId,
|
||||
FeatureId.ORGINIZATIONS
|
||||
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
|
||||
);
|
||||
// const egressData = await usageService.getUsage(
|
||||
// orgId,
|
||||
@@ -127,6 +139,15 @@ 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()
|
||||
|
||||
@@ -329,6 +329,44 @@ 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,
|
||||
@@ -495,29 +533,31 @@ authRouter.post(
|
||||
auth.transferSession
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/license/activate",
|
||||
verifyUserIsServerAdmin,
|
||||
license.activateLicense
|
||||
);
|
||||
if (build !== "saas") {
|
||||
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",
|
||||
@@ -878,3 +918,9 @@ authenticated.post(
|
||||
verifyClientAccess,
|
||||
client.rebuildClientAssociationsCacheRoute
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/logs/access/attempt",
|
||||
verifyOrgAccess,
|
||||
logs.logAccessAuditAttempt
|
||||
);
|
||||
|
||||
@@ -29,26 +29,40 @@ 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)
|
||||
});
|
||||
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"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CreateHealthCheckResponse = {
|
||||
targetHealthCheckId: number;
|
||||
@@ -57,7 +71,6 @@ const CreateHealthCheckResponseDataSchema = z.object({
|
||||
targetHealthCheckId: z.number()
|
||||
});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "put",
|
||||
path: "/org/{orgId}/health-check",
|
||||
@@ -78,7 +91,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
CreateHealthCheckResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ const UpdateHealthCheckResponseDataSchema = z.object({
|
||||
hcUnhealthyThreshold: z.number().nullable()
|
||||
});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/health-check/{healthCheckId}",
|
||||
@@ -126,7 +125,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(UpdateHealthCheckResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
UpdateHealthCheckResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +216,32 @@ 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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user