mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-14 23:00:26 +02:00
Compare commits
151 Commits
1.19.2
..
b940c3b718
| Author | SHA1 | Date | |
|---|---|---|---|
| b940c3b718 | |||
| a945971eda | |||
| c8e7a82df9 | |||
| 66941695e4 | |||
| c26e44c668 | |||
| 5f26378d4b | |||
| 115b3095bd | |||
| f4708e5cc7 | |||
| a23650fba2 | |||
| 804a9b2ae3 | |||
| 63a6d95a97 | |||
| 6d30c522ce | |||
| d2944c0fc6 | |||
| 05a1aa4e9c | |||
| 804896d854 | |||
| 39475aa362 | |||
| 21d852c494 | |||
| fb76b62233 | |||
| de5165bf30 | |||
| 3b1bfb33cf | |||
| 7ce26e5c89 | |||
| 36fb1021b0 | |||
| fa3d2d1632 | |||
| 3e84ee9d68 | |||
| 9598f84d8c | |||
| 65911e8e3d | |||
| 341f59f735 | |||
| e6a111a885 | |||
| 0852f13eb3 | |||
| fb2132e172 | |||
| 366e49a4e8 | |||
| b0092016e6 | |||
| 4ddac8a175 | |||
| 3ddb96cc48 | |||
| 2e82112d32 | |||
| bb64cc1509 | |||
| 99c5f12bd9 | |||
| 9c5bccec8a | |||
| 22444cb62f | |||
| 802009db24 | |||
| 35b4575272 | |||
| 545504eec2 | |||
| 6e9cb01f29 | |||
| 36727ee6fb | |||
| c29e85568e | |||
| 210a0b6b83 | |||
| c2ee1d3a81 | |||
| a7f0e4bb9f | |||
| fbcb73d08c | |||
| bc63747efe | |||
| 2a8ceeec1b | |||
| 91ef0d0153 | |||
| 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 |
+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
|
||||
|
||||
+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=
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Локална",
|
||||
"edit": "Редактиране",
|
||||
"siteConfirmDelete": "Потвърждение на изтриване на сайта",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Изтриване на сайта",
|
||||
"siteMessageRemove": "След премахване, сайтът вече няма да бъде достъпен. Всички цели, свързани със сайта, също ще бъдат премахнати.",
|
||||
"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": "Сигурни ли сте, че искате да премахнете сайта от организацията?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Управление на сайтове",
|
||||
"siteDescription": "Създайте и управлявайте сайтове, за да осигурите свързаност със частни мрежи",
|
||||
"sitesBannerTitle": "Свържете се с мрежа.",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Управление на обществени ресурси",
|
||||
"proxyResourceDescription": "Създайте и управлявайте ресурси, които са общодостъпни чрез уеб браузър.",
|
||||
"publicResourcesBannerTitle": "Публичен достъп през Интернет",
|
||||
"publicResourcesBannerDescription": "Публичните ресурси са HTTPS проксита, достъпни за всеки в Интернет чрез уеб браузър. За разлика от частните ресурси, те не изискват клиентски софтуер и могат да включват издентити и контексто-осъзнати политики за достъп.",
|
||||
"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": "Управление на частни ресурси",
|
||||
"clientResourceDescription": "Създайте и управлявайте ресурси, които са достъпни само чрез свързан клиент.",
|
||||
"privateResourcesBannerTitle": "Достъп до частни ресурси с нулево доверие.",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Тип на действието",
|
||||
"alertingNotifyUsers": "Потребители",
|
||||
"alertingNotifyRoles": "Роли",
|
||||
"alertingNotifyEmails": "Имейл адреси",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Добавете имейл и натиснете Enter",
|
||||
"alertingWebhookMethod": "HTTP метод",
|
||||
"alertingWebhookSecret": "Секрет за подписване (по избор)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Потребителят може да изпълнява само определени команди с sudo.",
|
||||
"sshSudo": "Разреши sudo",
|
||||
"sshSudoCommands": "Sudo команди",
|
||||
"sshSudoCommandsDescription": "Списък с команди, които потребителят има право да изпълнява със sudo, разделени със запетаи, интервали или нови редове. Необходимо е да се използват абсолютни пътища.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Създай начална директория",
|
||||
"sshUnixGroups": "Unix групи",
|
||||
"sshUnixGroupsDescription": "Unix групи, за добавяне на потребителя на целевия хост, разделени със запетаи, интервали или нови редове.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Въведете стойности или пуснете .txt или .csv файл",
|
||||
"roleTextImportTitle": "Импортиране от файл",
|
||||
"roleTextImportDescription": "Импортиране на {fileName} в {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.",
|
||||
"createInternalResourceDialogAlias": "Псевдоним",
|
||||
"createInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Методът е задължителен за HTTP ресурси",
|
||||
"internalResourceHttpPortRequired": "Портът към целта е задължителен за HTTP ресурси",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2967,6 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "Липсва идентификатор на организация или домейн",
|
||||
"loadingDNSRecords": "Зареждане на DNS записи...",
|
||||
"olmUpdateAvailableInfo": "Налична е актуализирана версия на Olm. Моля, актуализирайте до най-новата версия за най-добро преживяване.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки на прокси протокол",
|
||||
"proxyProtocolDescription": "Конфигурирайте Proxy Protocol, за да запазите IP адресите на клиентите за TCP услуги.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Místní",
|
||||
"edit": "Upravit",
|
||||
"siteConfirmDelete": "Potvrdit odstranění lokality",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Odstranění lokality",
|
||||
"siteMessageRemove": "Po odstranění webu již nebude přístupný. Všechny cíle spojené s webem budou také odstraněny.",
|
||||
"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": "Jste si jisti, že chcete odstranit tuto stránku z organizace?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"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íť",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Spravovat veřejné zdroje",
|
||||
"proxyResourceDescription": "Vytváření a správa zdrojů, které jsou veřejně přístupné prostřednictvím webového prohlížeče",
|
||||
"publicResourcesBannerTitle": "Webové Veřejné Přístupy",
|
||||
"publicResourcesBannerDescription": "Veřejné prostředky jsou HTTPS proxy přístupné každému na internetu prostřednictvím webového prohlížeče. Na rozdíl od soukromých prostředků nevyžadují software na straně klienta a mohou zahrnovat politiky přístupu orientované na identitu a kontext.",
|
||||
"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": "Spravovat soukromé zdroje",
|
||||
"clientResourceDescription": "Vytváření a správa zdrojů, které jsou přístupné pouze prostřednictvím připojeného klienta",
|
||||
"privateResourcesBannerTitle": "Zero-Trust soukromý přístup",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Typ akce",
|
||||
"alertingNotifyUsers": "Uživatelé",
|
||||
"alertingNotifyRoles": "Role",
|
||||
"alertingNotifyEmails": "Emailové adresy",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Přidejte e-mail a stiskněte Enter",
|
||||
"alertingWebhookMethod": "HTTP metoda",
|
||||
"alertingWebhookSecret": "Přihlašovací tajemství (volitelné)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Uživatel může spustit pouze zadané příkazy s sudo.",
|
||||
"sshSudo": "Povolit sudo",
|
||||
"sshSudoCommands": "Sudo příkazy",
|
||||
"sshSudoCommandsDescription": "Seznam příkazů, které je uživateli povoleno spouštět se sudo, oddělený čárkami, mezerami, nebo novými řádky. Je třeba používat absolutní cesty.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Vytvořit domovský adresář",
|
||||
"sshUnixGroups": "Unixové skupiny",
|
||||
"sshUnixGroupsDescription": "Unixové skupiny, do kterých má být uživatel přidán na cílovém hostu, oddělené čárkami, mezerami, nebo novými řádky.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Zadejte hodnoty nebo přetáhněte soubor .txt nebo .csv",
|
||||
"roleTextImportTitle": "Importovat ze souboru",
|
||||
"roleTextImportDescription": "Importuje se {fileName} do {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP metoda je vyžadována pro HTTP zdroje",
|
||||
"internalResourceHttpPortRequired": "Přípoječný port je nutný pro HTTP zdroj",
|
||||
"siteConfiguration": "Konfigurace",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Zákazník",
|
||||
"proxyProtocol": "Nastavení proxy protokolu",
|
||||
"proxyProtocolDescription": "Konfigurace Proxy protokolu pro zachování klientských IP adres pro služby TCP.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Lokal",
|
||||
"edit": "Bearbeiten",
|
||||
"siteConfirmDelete": "Löschen des Standorts bestätigen",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Standort löschen",
|
||||
"siteMessageRemove": "Sobald der Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle mit dem Standort verbundenen Ziele werden ebenfalls entfernt.",
|
||||
"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": "Sind Sie sicher, dass Sie den Standort aus der Organisation entfernen möchten?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Standorte verwalten",
|
||||
"siteDescription": "Erstellen und Verwalten von Standorten, um die Verbindung zu privaten Netzwerken zu ermöglichen",
|
||||
"sitesBannerTitle": "Verbinde ein beliebiges Netzwerk",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Öffentliche Ressourcen verwalten",
|
||||
"proxyResourceDescription": "Erstelle und verwalte Ressourcen, die über einen Webbrowser öffentlich zugänglich sind",
|
||||
"publicResourcesBannerTitle": "Web-basierter öffentlicher Zugang",
|
||||
"publicResourcesBannerDescription": "Öffentliche Ressourcen sind HTTPS-Proxys, die über einen Webbrowser für jeden im Internet zugänglich sind. Im Gegensatz zu privaten Ressourcen benötigen sie keine Client-seitige Software und können Identitäts- und kontextuelle Zugriffsrichtlinien enthalten.",
|
||||
"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": "Private Ressourcen verwalten",
|
||||
"clientResourceDescription": "Erstelle und verwalte Ressourcen, die nur über einen verbundenen Client zugänglich sind",
|
||||
"privateResourcesBannerTitle": "Zero-Trust-Zugriff auf private Ressourcen",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Aktionstyp",
|
||||
"alertingNotifyUsers": "Benutzer",
|
||||
"alertingNotifyRoles": "Rollen",
|
||||
"alertingNotifyEmails": "E-Mail-Adressen",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "E-Mail hinzufügen und Enter drücken",
|
||||
"alertingWebhookMethod": "HTTP-Methode",
|
||||
"alertingWebhookSecret": "Signatur geheim (optional)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.",
|
||||
"sshSudo": "sudo erlauben",
|
||||
"sshSudoCommands": "Sudo-Befehle",
|
||||
"sshSudoCommandsDescription": "Liste der Befehle, die der Benutzer mit sudo ausführen darf, durch Kommas, Leerzeichen oder neue Zeilen getrennt. Absolute Pfade müssen verwendet werden.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Home-Verzeichnis erstellen",
|
||||
"sshUnixGroups": "Unix-Gruppen",
|
||||
"sshUnixGroupsDescription": "Unix-Gruppen, in die der Benutzer auf dem Ziel-Host aufgenommen werden soll, getrennt durch Kommas, Leerzeichen oder neue Zeilen.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Werte eingeben oder eine .txt- oder .csv-Datei ablegen",
|
||||
"roleTextImportTitle": "Von Datei importieren",
|
||||
"roleTextImportDescription": "Importiere {fileName} in {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Der CIDR-Bereich der Ressource im Netzwerk der Website.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Ein optionaler interner DNS-Alias für diese Ressource.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Schema ist für HTTP-Ressourcen erforderlich",
|
||||
"internalResourceHttpPortRequired": "Zielport ist für HTTP-Ressourcen erforderlich",
|
||||
"siteConfiguration": "Konfiguration",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Client",
|
||||
"proxyProtocol": "Proxy-Protokoll-Einstellungen",
|
||||
"proxyProtocolDescription": "Konfigurieren Sie das Proxy-Protokoll, um die IP-Adressen des Clients für TCP-Dienste zu erhalten.",
|
||||
|
||||
@@ -2338,6 +2338,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",
|
||||
@@ -2967,6 +2968,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.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Local",
|
||||
"edit": "Editar",
|
||||
"siteConfirmDelete": "Confirmar Borrar Sitio",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Eliminar sitio",
|
||||
"siteMessageRemove": "Una vez eliminado, el sitio ya no será accesible. Todos los objetivos asociados con el sitio también serán eliminados.",
|
||||
"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": "¿Está seguro que desea eliminar el sitio de la organización?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Administrar Sitios",
|
||||
"siteDescription": "Crear y administrar sitios para permitir la conectividad a redes privadas",
|
||||
"sitesBannerTitle": "Conectar cualquier red",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Administrar recursos públicos",
|
||||
"proxyResourceDescription": "Crear y administrar recursos que sean accesibles públicamente a través de un navegador web",
|
||||
"publicResourcesBannerTitle": "Acceso Público basado en Web",
|
||||
"publicResourcesBannerDescription": "Los recursos públicos son proxies HTTPS accesibles para cualquiera en Internet a través de un navegador web. A diferencia de los recursos privados, no requieren software del lado del cliente e incluyen políticas de acceso basadas en identidad y contexto.",
|
||||
"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": "Administrar recursos privados",
|
||||
"clientResourceDescription": "Crear y administrar recursos que sólo son accesibles a través de un cliente conectado",
|
||||
"privateResourcesBannerTitle": "Acceso privado de confianza cero",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Tipo de acción",
|
||||
"alertingNotifyUsers": "Usuarios",
|
||||
"alertingNotifyRoles": "Roles",
|
||||
"alertingNotifyEmails": "Direcciones de correo electrónico",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Añadir email y presionar Enter",
|
||||
"alertingWebhookMethod": "Método HTTP",
|
||||
"alertingWebhookSecret": "Firma secreta (opcional)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "El usuario sólo puede ejecutar los comandos especificados con sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos que el usuario tiene permitido ejecutar con sudo, separados por comas, espacios o nuevas líneas. Se deben usar rutas absolutas.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Crear directorio principal",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix a los que añadir el usuario en el host de destino, separados por comas, espacios o nuevas líneas.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Introduce valores, o suelta un archivo .txt o .csv",
|
||||
"roleTextImportTitle": "Importar desde Archivo",
|
||||
"roleTextImportDescription": "Importando {fileName} en {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "El rango CIDR del recurso en la red del sitio.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Un alias DNS interno opcional para este recurso.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Se requiere el método para recursos HTTP",
|
||||
"internalResourceHttpPortRequired": "Se requiere el puerto de destino para recursos HTTP",
|
||||
"siteConfiguration": "Configuración",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"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.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Locale",
|
||||
"edit": "Modifier",
|
||||
"siteConfirmDelete": "Confirmer la suppression du nœud",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Supprimer le nœud",
|
||||
"siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.",
|
||||
"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": "Êtes-vous sûr de vouloir supprimer ce nœud de l'organisation ?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"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",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Gérer les ressources publiques",
|
||||
"proxyResourceDescription": "Créer et gérer des ressources accessibles au public via un navigateur web",
|
||||
"publicResourcesBannerTitle": "Accès public basé sur le Web",
|
||||
"publicResourcesBannerDescription": "Les ressources publiques sont des proxys HTTPS accessibles à quiconque sur Internet via un navigateur Web. Contrairement aux ressources privées, elles ne nécessitent pas de logiciel côté client et peuvent inclure des politiques d'accès fondées sur l'identité et le contexte.",
|
||||
"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": "Gérer les ressources privées",
|
||||
"clientResourceDescription": "Créer et gérer des ressources qui ne sont accessibles que via un client connecté",
|
||||
"privateResourcesBannerTitle": "Accès privé sans confiance",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Type d'action",
|
||||
"alertingNotifyUsers": "Utilisateurs",
|
||||
"alertingNotifyRoles": "Rôles",
|
||||
"alertingNotifyEmails": "Adresses e-mail",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Ajoutez un e-mail et appuyez sur Entrée",
|
||||
"alertingWebhookMethod": "Méthode HTTP",
|
||||
"alertingWebhookSecret": "Secret de signature (facultatif)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "L'utilisateur ne peut exécuter que les commandes spécifiées avec sudo.",
|
||||
"sshSudo": "Autoriser sudo",
|
||||
"sshSudoCommands": "Commandes Sudo",
|
||||
"sshSudoCommandsDescription": "Liste des commandes que l'utilisateur est autorisé à exécuter avec sudo, séparées par des virgules, des espaces ou des nouvelles lignes. Les chemins absolus doivent être utilisés.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Créer un répertoire personnel",
|
||||
"sshUnixGroups": "Groupes Unix",
|
||||
"sshUnixGroupsDescription": "Groupes Unix auxquels ajouter l'utilisateur sur l'hôte cible, séparés par des virgules, des espaces, ou des nouvelles lignes.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Entrez des valeurs, ou déposez un fichier .txt ou .csv",
|
||||
"roleTextImportTitle": "Importer depuis un fichier",
|
||||
"roleTextImportDescription": "Importation de {fileName} dans {fieldLabel}.",
|
||||
@@ -2338,6 +2344,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": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Un schéma est requis pour les ressources HTTP",
|
||||
"internalResourceHttpPortRequired": "Le port de destination est requis pour les ressources HTTP",
|
||||
"siteConfiguration": "Configuration",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"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.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Locale",
|
||||
"edit": "Modifica",
|
||||
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Elimina Sito",
|
||||
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.",
|
||||
"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": "Sei sicuro di voler rimuovere il sito dall'organizzazione?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Gestisci Siti",
|
||||
"siteDescription": "Creare e gestire siti per abilitare la connettività a reti private",
|
||||
"sitesBannerTitle": "Connetti Qualsiasi Rete",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Gestisci Risorse Pubbliche",
|
||||
"proxyResourceDescription": "Creare e gestire risorse pubbliche accessibili tramite un browser web",
|
||||
"publicResourcesBannerTitle": "Accesso Pubblico Basato sul Web",
|
||||
"publicResourcesBannerDescription": "Le risorse pubbliche sono proxy HTTPS accessibili a chiunque su Internet tramite un browser web. A differenza delle risorse private, non richiedono software lato client e possono includere politiche di accesso basate su identità e contesto.",
|
||||
"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": "Gestisci Risorse Private",
|
||||
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
|
||||
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Tipo di azione",
|
||||
"alertingNotifyUsers": "Utenti",
|
||||
"alertingNotifyRoles": "Ruoli",
|
||||
"alertingNotifyEmails": "Indirizzi email",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Aggiungi email e premi Invio",
|
||||
"alertingWebhookMethod": "Metodo HTTP",
|
||||
"alertingWebhookSecret": "Segreto di firma (opzionale)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "L'utente può eseguire solo i comandi specificati con sudo.",
|
||||
"sshSudo": "Consenti sudo",
|
||||
"sshSudoCommands": "Comandi Sudo",
|
||||
"sshSudoCommandsDescription": "Elenco di comandi che l'utente è autorizzato ad eseguire con sudo, separati da virgole, spazi o nuove righe. Devono essere utilizzati percorsi assoluti.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Crea Cartella Home",
|
||||
"sshUnixGroups": "Gruppi Unix",
|
||||
"sshUnixGroupsDescription": "Gruppi Unix a cui aggiungere l'utente sull'host di destinazione, separati da virgole, spazi o nuove righe.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Inserisci i valori o rilascia un file .txt o .csv",
|
||||
"roleTextImportTitle": "Importa da File",
|
||||
"roleTextImportDescription": "Importazione di {fileName} in {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "La gamma CIDR della risorsa sulla rete del sito.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Il metodo è richiesto per risorse HTTP",
|
||||
"internalResourceHttpPortRequired": "Porta di destinazione richiesta per risorse HTTP",
|
||||
"siteConfiguration": "Configurazione",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Client",
|
||||
"proxyProtocol": "Impostazioni Protocollo Proxy",
|
||||
"proxyProtocolDescription": "Configurare il protocollo proxy per preservare gli indirizzi IP client per i servizi TCP.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "로컬",
|
||||
"edit": "편집",
|
||||
"siteConfirmDelete": "사이트 삭제 확인",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "사이트 삭제",
|
||||
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
|
||||
"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": "조직에서 사이트를 제거하시겠습니까?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "사이트 관리",
|
||||
"siteDescription": "프라이빗 네트워크로의 연결을 활성화하려면 사이트를 생성하고 관리하세요.",
|
||||
"sitesBannerTitle": "모든 네트워크 연결",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "공개 리소스 관리",
|
||||
"proxyResourceDescription": "웹 브라우저를 통해 공용으로 접근할 수 있는 리소스를 생성하고 관리하세요.",
|
||||
"publicResourcesBannerTitle": "웹 기반의 공개 액세스",
|
||||
"publicResourcesBannerDescription": "공공 자원은 누구나 웹 브라우저를 통해 접근 가능한 HTTPS 프록시입니다. 개인 자원과 달리 클라이언트 측 소프트웨어가 필요하지 않으며, 아이덴티티 및 컨텍스트 인지 접근 정책을 포함할 수 있습니다.",
|
||||
"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": "개인 리소스 관리",
|
||||
"clientResourceDescription": "연결된 클라이언트를 통해서만 접근할 수 있는 리소스를 생성하고 관리하세요.",
|
||||
"privateResourcesBannerTitle": "제로 트러스트 개인 접근",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "작업 유형",
|
||||
"alertingNotifyUsers": "사용자들",
|
||||
"alertingNotifyRoles": "역할들",
|
||||
"alertingNotifyEmails": "이메일 주소들",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "이메일 추가 후 Enter 키를 누르세요",
|
||||
"alertingWebhookMethod": "HTTP 메소드",
|
||||
"alertingWebhookSecret": "서명 비밀 (선택 사항)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "사용자는 sudo로 지정된 명령만 실행할 수 있습니다.",
|
||||
"sshSudo": "Sudo 허용",
|
||||
"sshSudoCommands": "Sudo 명령",
|
||||
"sshSudoCommandsDescription": "사용자가 쉘에서 sudo로 실행할 수 있는 명령 목록, 쉼표, 공백 또는 새 줄로 구분됩니다. 절대 경로를 사용해야 합니다.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "홈 디렉터리 생성",
|
||||
"sshUnixGroups": "유닉스 그룹",
|
||||
"sshUnixGroupsDescription": "사용자를 대상 호스트에 추가할 유닉스 그룹들, 쉼표, 공백 또는 새 줄로 구분됩니다.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "값을 입력하거나 .txt나 .csv 파일을 드롭하세요",
|
||||
"roleTextImportTitle": "파일에서 가져오기",
|
||||
"roleTextImportDescription": "{fileName}을(를) {fieldLabel}에 가져오는 중",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||
"createInternalResourceDialogAlias": "별칭",
|
||||
"createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 리소스에 스킴이 필요합니다",
|
||||
"internalResourceHttpPortRequired": "HTTP 리소스에 목적지 포트가 필요합니다",
|
||||
"siteConfiguration": "설정",
|
||||
@@ -2967,6 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
|
||||
"loadingDNSRecords": "DNS 레코드를 로드하는 중...",
|
||||
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "클라이언트",
|
||||
"proxyProtocol": "프록시 프로토콜 설정",
|
||||
"proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Lokal",
|
||||
"edit": "Rediger",
|
||||
"siteConfirmDelete": "Bekreft Sletting av Område",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Slett Område",
|
||||
"siteMessageRemove": "Når nettstedet er fjernet, vil det ikke lenger være tilgjengelig. Alle målene for nettstedet vil også bli fjernet.",
|
||||
"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": "Er du sikker på at du vil fjerne nettstedet fra organisasjonen?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Administrer Områder",
|
||||
"siteDescription": "Opprette og administrere nettsteder for å aktivere tilkobling til private nettverk",
|
||||
"sitesBannerTitle": "Koble til alle nettverk",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Administrere offentlige ressurser",
|
||||
"proxyResourceDescription": "Opprett og administrer ressurser som er offentlig tilgjengelige via en nettleser",
|
||||
"publicResourcesBannerTitle": "Web-basert offentlig tilgang",
|
||||
"publicResourcesBannerDescription": "Offentlige ressurser er HTTPS-proxyer som er tilgjengelige for alle på internett via en nettleser. I motsetning til private ressurser, krever de ikke klientprogramvare og kan inkludere identitets- og kontekstsensitive tilgangspolicyer.",
|
||||
"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": "Administrer private ressurser",
|
||||
"clientResourceDescription": "Opprette og administrere ressurser som bare er tilgjengelige via en tilkoblet klient",
|
||||
"privateResourcesBannerTitle": "Zero-Trust privat tilgang",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Handlings type",
|
||||
"alertingNotifyUsers": "Brukere",
|
||||
"alertingNotifyRoles": "Roller",
|
||||
"alertingNotifyEmails": "E-postadresser",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Legg til e-post og trykk Enter",
|
||||
"alertingWebhookMethod": "HTTP-metode",
|
||||
"alertingWebhookSecret": "Signeringshemmelig (valgfritt)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.",
|
||||
"sshSudo": "Tillat sudo",
|
||||
"sshSudoCommands": "Sudo kommandoer",
|
||||
"sshSudoCommandsDescription": "Liste over kommandoer brukeren har lov til å kjøre med sudo, separert med komma, mellomrom eller nye linjer. Absolutte stier må brukes.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Opprett hjemmappe",
|
||||
"sshUnixGroups": "Unix grupper",
|
||||
"sshUnixGroupsDescription": "Unix-grupper å legge til brukeren i på målverten, separert med komma, mellomrom eller nye linjer.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Skriv inn verdier, eller slipp en .txt eller .csv fil",
|
||||
"roleTextImportTitle": "Importer fra fil",
|
||||
"roleTextImportDescription": "Importerer {fileName} til {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Skjema er påkrevd for HTTP-ressurser",
|
||||
"internalResourceHttpPortRequired": "Destinasjonsport er nødvendig for HTTP-ressurser",
|
||||
"siteConfiguration": "Konfigurasjon",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Klient",
|
||||
"proxyProtocol": "Protokoll innstillinger for Protokoll",
|
||||
"proxyProtocolDescription": "Konfigurer Proxy-protokoll for å bevare klientens IP-adresser til TCP-tjenester.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Lokaal",
|
||||
"edit": "Bewerken",
|
||||
"siteConfirmDelete": "Verwijderen van site bevestigen",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Site verwijderen",
|
||||
"siteMessageRemove": "Eenmaal verwijderd zal de site niet langer toegankelijk zijn. Alle aan de site gekoppelde doelen zullen ook worden verwijderd.",
|
||||
"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": "Weet u zeker dat u de site wilt verwijderen uit de organisatie?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Sites beheren",
|
||||
"siteDescription": "Maak en beheer sites om verbinding met privénetwerken in te schakelen",
|
||||
"sitesBannerTitle": "Verbind elk netwerk",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Openbare bronnen beheren",
|
||||
"proxyResourceDescription": "Creëer en beheer bronnen die openbaar toegankelijk zijn via een webbrowser",
|
||||
"publicResourcesBannerTitle": "Web-gebaseerde Openbare Toegang",
|
||||
"publicResourcesBannerDescription": "Openbare bronnen zijn HTTPS-proxies die toegankelijk zijn voor iedereen op het internet via een webbrowser. In tegenstelling tot privébronnen hoeven ze geen client-software te hebben en kunnen ze identiteit- en context bewuste toegangsmiddelen bevatten.",
|
||||
"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": "Privébronnen beheren",
|
||||
"clientResourceDescription": "Creëer en beheer bronnen die alleen toegankelijk zijn via een verbonden client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Actietype",
|
||||
"alertingNotifyUsers": "Gebruikers",
|
||||
"alertingNotifyRoles": "Rollen",
|
||||
"alertingNotifyEmails": "E-mailadressen",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Voeg e-mail toe en druk op Enter",
|
||||
"alertingWebhookMethod": "HTTP-methode",
|
||||
"alertingWebhookSecret": "Ondertekengeheim (optioneel)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.",
|
||||
"sshSudo": "sudo toestaan",
|
||||
"sshSudoCommands": "Sudo Commando's",
|
||||
"sshSudoCommandsDescription": "Lijst met commando's die de gebruiker mag uitvoeren met sudo, gescheiden door komma's, spaties of nieuwe regels. Absolute paden moeten worden gebruikt.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Maak Home Directory",
|
||||
"sshUnixGroups": "Unix groepen",
|
||||
"sshUnixGroupsDescription": "Unix-groepen om de gebruiker aan toe te voegen op de doelhost, gescheiden door komma's, spaties of nieuwe regels.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Voer waarden in, of sleep een .txt of .csv-bestand",
|
||||
"roleTextImportTitle": "Importeer vanuit Bestand",
|
||||
"roleTextImportDescription": "{fileName} importeren naar {fieldLabel}.",
|
||||
@@ -2338,6 +2344,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": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Schema is vereist voor HTTP-bronnen",
|
||||
"internalResourceHttpPortRequired": "Bestemmingspoort is vereist voor HTTP-bronnen",
|
||||
"siteConfiguration": "Configuratie",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Klant",
|
||||
"proxyProtocol": "Proxy Protocol Instellingen",
|
||||
"proxyProtocolDescription": "Proxyprotocol configureren om de IP-adressen van de client voor TCP-diensten te bewaren.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Lokalny",
|
||||
"edit": "Edytuj",
|
||||
"siteConfirmDelete": "Potwierdź usunięcie witryny",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Usuń witrynę",
|
||||
"siteMessageRemove": "Po usunięciu witryna nie będzie już dostępna. Wszystkie cele związane z witryną zostaną również usunięte.",
|
||||
"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": "Czy na pewno chcesz usunąć witrynę z organizacji?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Zarządzaj stronami",
|
||||
"siteDescription": "Tworzenie stron i zarządzanie nimi, aby włączyć połączenia z prywatnymi sieciami",
|
||||
"sitesBannerTitle": "Połącz dowolną sieć",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Zarządzaj zasobami publicznymi",
|
||||
"proxyResourceDescription": "Twórz i zarządzaj zasobami, które są publicznie dostępne w przeglądarce internetowej",
|
||||
"publicResourcesBannerTitle": "Publiczny dostęp przez przeglądarkę internetową",
|
||||
"publicResourcesBannerDescription": "Zasoby publiczne to serwery proxy HTTPS, dostępne dla każdego w Internecie za pośrednictwem przeglądarki. W przeciwieństwie do zasobów prywatnych, nie wymagają oprogramowania po stronie klienta i mogą obejmować polityki dostępu świadome tożsamości i kontekstu.",
|
||||
"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": "Zarządzaj zasobami prywatnymi",
|
||||
"clientResourceDescription": "Twórz i zarządzaj zasobami, które są dostępne tylko za pośrednictwem połączonego klienta",
|
||||
"privateResourcesBannerTitle": "Zero zaufania do prywatnego dostępu",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Typ akcji",
|
||||
"alertingNotifyUsers": "Użytkownicy",
|
||||
"alertingNotifyRoles": "Role",
|
||||
"alertingNotifyEmails": "Adres e-mail",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Dodaj e-mail i naciśnij Enter",
|
||||
"alertingWebhookMethod": "Metoda HTTP",
|
||||
"alertingWebhookSecret": "Sekret podpisu (opcjonalny)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Użytkownik może uruchamiać tylko określone polecenia z sudo.",
|
||||
"sshSudo": "Zezwól na sudo",
|
||||
"sshSudoCommands": "Komendy Sudo",
|
||||
"sshSudoCommandsDescription": "Lista poleceń, które użytkownik może uruchomić z sudo, oddzielone przecinkami, spacjami lub nowymi liniami. Absolutne ścieżki muszą być używane.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Utwórz katalog domowy",
|
||||
"sshUnixGroups": "Grupy Unix",
|
||||
"sshUnixGroupsDescription": "Grupy Uniksowe, do których dodać użytkownika na docelowym hoście, oddzielone przecinkami, spacjami, lub nowymi liniami.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Wprowadź wartości lub upuść plik .txt lub .csv",
|
||||
"roleTextImportTitle": "Importuj z pliku",
|
||||
"roleTextImportDescription": "Importowanie {fileName} do {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Schemat jest wymagany dla zasobów HTTP",
|
||||
"internalResourceHttpPortRequired": "Port docelowy jest wymagany dla zasobów HTTP",
|
||||
"siteConfiguration": "Konfiguracja",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Klient",
|
||||
"proxyProtocol": "Ustawienia protokołu proxy",
|
||||
"proxyProtocolDescription": "Skonfiguruj protokół Proxy aby zachować adresy IP klienta dla usług TCP.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Localização",
|
||||
"edit": "Alterar",
|
||||
"siteConfirmDelete": "Confirmar que pretende apagar o site",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Excluir site",
|
||||
"siteMessageRemove": "Uma vez removido, o site não estará mais acessível. Todas as metas associadas ao site também serão removidas.",
|
||||
"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": "Você tem certeza que deseja remover este site da organização?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Gerir sites",
|
||||
"siteDescription": "Criar e gerenciar sites para ativar a conectividade a redes privadas",
|
||||
"sitesBannerTitle": "Conectar a Qualquer Rede",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Gerenciar Recursos Públicos",
|
||||
"proxyResourceDescription": "Criar e gerenciar recursos que são acessíveis publicamente por meio de um navegador da web",
|
||||
"publicResourcesBannerTitle": "Acesso Público Baseado em Web",
|
||||
"publicResourcesBannerDescription": "Os recursos públicos são proxies HTTPS acessíveis a qualquer pessoa na internet através de um navegador web. Ao contrário dos recursos privados, eles não exigem software do lado do cliente e podem incluir políticas de acesso conscientes de identidade e contexto.",
|
||||
"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": "Gerenciar recursos privados",
|
||||
"clientResourceDescription": "Criar e gerenciar recursos que só são acessíveis por meio de um cliente conectado",
|
||||
"privateResourcesBannerTitle": "Acesso Privado com Confiança Zero",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Tipo de Ação",
|
||||
"alertingNotifyUsers": "Utilizadores",
|
||||
"alertingNotifyRoles": "Papéis",
|
||||
"alertingNotifyEmails": "Endereços de e-mail",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Adicione o e-mail e pressione Enter",
|
||||
"alertingWebhookMethod": "Método HTTP",
|
||||
"alertingWebhookSecret": "Segredo de assinatura (opcional)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Usuário só pode executar os comandos especificados com sudo.",
|
||||
"sshSudo": "Permitir sudo",
|
||||
"sshSudoCommands": "Comandos Sudo",
|
||||
"sshSudoCommandsDescription": "Lista de comandos que o usuário está autorizado a executar com sudo, separados por vírgulas, espaços ou novas linhas. Devem ser usados caminhos absolutos.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Criar Diretório Inicial",
|
||||
"sshUnixGroups": "Grupos Unix",
|
||||
"sshUnixGroupsDescription": "Grupos Unix para adicionar o usuário no host de destino, separados por vírgulas, espaços ou novas linhas.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Insira valores, ou solte um arquivo .txt ou .csv",
|
||||
"roleTextImportTitle": "Importar de Arquivo",
|
||||
"roleTextImportDescription": "Importando {fileName} para {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Esquema é obrigatório para recursos HTTP",
|
||||
"internalResourceHttpPortRequired": "Porta de destino é obrigatória para recursos HTTP",
|
||||
"siteConfiguration": "Configuração",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Cliente",
|
||||
"proxyProtocol": "Configurações de Protocolo Proxy",
|
||||
"proxyProtocolDescription": "Configurar o protocolo proxy para preservar endereços IP do cliente para serviços TCP.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Локальный",
|
||||
"edit": "Редактировать",
|
||||
"siteConfirmDelete": "Подтвердить удаление сайта",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Удалить сайт",
|
||||
"siteMessageRemove": "После удаления сайт больше не будет доступен. Все цели, связанные с сайтом, также будут удалены.",
|
||||
"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": "Вы уверены, что хотите удалить сайт из организации?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Управление сайтами",
|
||||
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
|
||||
"sitesBannerTitle": "Подключить любую сеть",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Управление публичными ресурсами",
|
||||
"proxyResourceDescription": "Создание и управление ресурсами, которые доступны через веб-браузер",
|
||||
"publicResourcesBannerTitle": "Веб-доступ к публичным ресурсам",
|
||||
"publicResourcesBannerDescription": "Публичные ресурсы — это HTTPS-прокси, доступные для любого пользователя Интернета через веб-браузер. В отличие от частных ресурсов, они не требуют программного обеспечения на стороне клиента и могут включать в себя политики доступа, учитывающие идентичность и контекст.",
|
||||
"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": "Управление приватными ресурсами",
|
||||
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
|
||||
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Тип действия",
|
||||
"alertingNotifyUsers": "Пользователи",
|
||||
"alertingNotifyRoles": "Роли",
|
||||
"alertingNotifyEmails": "Email адреса",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Добавьте email и нажмите Enter",
|
||||
"alertingWebhookMethod": "HTTP метод",
|
||||
"alertingWebhookSecret": "Секрет подписания (необязательно)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Пользователь может запускать только указанные команды с помощью sudo.",
|
||||
"sshSudo": "Разрешить sudo",
|
||||
"sshSudoCommands": "Sudo Команды",
|
||||
"sshSudoCommandsDescription": "Список команд, которые пользователь может запускать с sudo, разделенный запятыми, пробелами или новыми строками. Должны использоваться абсолютные пути.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Создать домашний каталог",
|
||||
"sshUnixGroups": "Unix группы",
|
||||
"sshUnixGroupsDescription": "Группы Unix, к которым пользователь добавляется на целевом хосте, разделяются запятыми, пробелами или новыми строками.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Введите значения или перетащите файл .txt или .csv",
|
||||
"roleTextImportTitle": "Импорт из файла",
|
||||
"roleTextImportDescription": "Импортирую {fileName} в {fieldLabel}.",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Схема обязательна для HTTP ресурсов",
|
||||
"internalResourceHttpPortRequired": "Порт назначения обязателен для HTTP ресурсов",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2967,6 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "Отсутствует организация или ID домена",
|
||||
"loadingDNSRecords": "Загрузка записей DNS...",
|
||||
"olmUpdateAvailableInfo": "Доступна обновленная версия Олма. Пожалуйста, обновитесь до последней версии.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки протокола прокси",
|
||||
"proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP.",
|
||||
|
||||
+13
-5
@@ -66,9 +66,15 @@
|
||||
"local": "Yerel",
|
||||
"edit": "Düzenle",
|
||||
"siteConfirmDelete": "Site Silmeyi Onayla",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Siteyi 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.",
|
||||
"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": "Siteyi organizasyondan kaldırmak istediğinizden emin misiniz?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"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",
|
||||
@@ -204,7 +210,7 @@
|
||||
"proxyResourceTitle": "Herkese Açık Kaynakları Yönet",
|
||||
"proxyResourceDescription": "Bir web tarayıcısı aracılığıyla kamuya açık kaynaklar oluşturun ve yönetin",
|
||||
"publicResourcesBannerTitle": "Web tabanlı Açık Erişim",
|
||||
"publicResourcesBannerDescription": "Genel kaynaklar, web tarayıcısı aracılığıyla internette herkesin erişebileceği HTTPS veya TCP/UDP proxy'leridir. Özel kaynakların aksine istemci tarafı yazılım gerektirmezler ve kimlik ve bağlam farkındalığı erişim politikalarını içerebilirler.",
|
||||
"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": "Özel Kaynakları Yönet",
|
||||
"clientResourceDescription": "Sadece bağlı bir istemci aracılığıyla erişilebilen kaynakları oluşturun ve yönetin",
|
||||
"privateResourcesBannerTitle": "Sıfır Güven Özel Erişim",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "Eylem türü",
|
||||
"alertingNotifyUsers": "Kullanıcılar",
|
||||
"alertingNotifyRoles": "Roller",
|
||||
"alertingNotifyEmails": "E-posta adresleri",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "E-posta ekleyin ve Enter tuşuna basın",
|
||||
"alertingWebhookMethod": "HTTP yöntemi",
|
||||
"alertingWebhookSecret": "İmza sırrı (isteğe bağlı)",
|
||||
@@ -2171,10 +2177,10 @@
|
||||
"sshSudoModeCommandsDescription": "Kullanıcı sadece belirtilen komutları sudo ile çalıştırabilir.",
|
||||
"sshSudo": "Sudo'ya izin ver",
|
||||
"sshSudoCommands": "Sudo Komutları",
|
||||
"sshSudoCommandsDescription": "Kullanıcının 'sudo' ile çalıştırmasına izin verilen komutlar listesi noktalı virgülle, boşluk veya yeni satırla ayrılmalıdır. Mutlak yollar kullanılmalıdır.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Ev Dizini Oluştur",
|
||||
"sshUnixGroups": "Unix Grupları",
|
||||
"sshUnixGroupsDescription": "Hedef ana bilgisayardaki kullanıcıya eklemek için Unix grupları, noktalı virgülle, boşluk veya yeni satırla ayrılmalıdır.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "Değerleri girin veya bir .txt veya .csv dosyası bırakın",
|
||||
"roleTextImportTitle": "Dosyadan İçe Aktar",
|
||||
"roleTextImportDescription": "{fileName} dosyası {fieldLabel} alanına içe aktarılıyor.",
|
||||
@@ -2338,6 +2344,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": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP kaynakları için şema gereklidir",
|
||||
"internalResourceHttpPortRequired": "HTTP kaynakları için hedef bağlantı noktası gereklidir",
|
||||
"siteConfiguration": "Yapılandırma",
|
||||
@@ -2967,6 +2974,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": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "İstemci",
|
||||
"proxyProtocol": "Proxy Protokol Ayarları",
|
||||
"proxyProtocolDescription": "TCP hizmetleri için istemci IP adreslerini korumak amacıyla Proxy Protokolünü yapılandırın.",
|
||||
|
||||
+70
-62
@@ -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": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "删除节点",
|
||||
"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": "您确定要从组织中删除该节点吗?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "管理站点",
|
||||
"siteDescription": "创建和管理站点,启用与私人网络的连接",
|
||||
"sitesBannerTitle": "连接任何网络",
|
||||
"sitesBannerDescription": "站点是连接到远程网络的链接,允许Pangolin为用户提供资源访问,无论是公共还是私人。可以在任何可以运行二进制文件或容器的地方安装站点网络连接器(Newt)以建立连接。",
|
||||
"sitesBannerDescription": "站点是到远程网络的连接,使 Pangolin 能够向任何位置的用户提提供公共或私有的资源访问。你可以在任何能够运行二进制文件或容器的地方安装站点网络连接器(Newt),以建立连接。",
|
||||
"sitesBannerButtonText": "安装站点",
|
||||
"approvalsBannerTitle": "批准或拒绝设备访问",
|
||||
"approvalsBannerDescription": "审核、批准或拒绝用户的设备访问请求。 当需要设备批准时,用户必须先获得管理员批准,然后他们的设备才能连接到您的组织资源。",
|
||||
@@ -134,7 +140,7 @@
|
||||
"siteResourcesHowToAccess": "如何访问",
|
||||
"siteResourcesTargetsOnSite": "此站点上的目标",
|
||||
"siteSetting": "{siteName} 设置",
|
||||
"siteNewtTunnel": "新站点 (推荐)",
|
||||
"siteNewtTunnel": "新节点 (推荐)",
|
||||
"siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。",
|
||||
"siteWg": "基本 WireGuard",
|
||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||
@@ -143,23 +149,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 +210,11 @@
|
||||
"proxyResourceTitle": "管理公共资源",
|
||||
"proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源",
|
||||
"publicResourcesBannerTitle": "基于 Web 的公共访问",
|
||||
"publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可以通过网络浏览器在互联网上的任何人访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
|
||||
"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": "管理私有资源",
|
||||
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
|
||||
"privateResourcesBannerTitle": "零信任的私人访问",
|
||||
"privateResourcesBannerDescription": "私人资源使用零信任安全性,确保只允许明确授予的用户和机器访问资源。可以连接用户设备或机器客户端,通过安全的虚拟专用网络访问这些资源。",
|
||||
"privateResourcesBannerTitle": "零信任私有访问",
|
||||
"privateResourcesBannerDescription": "私有资源采用零信任安全机制,确保只有获得明确授权的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
|
||||
"resourcesSearch": "搜索资源...",
|
||||
"resourceAdd": "添加资源",
|
||||
"resourceErrorDelte": "删除资源时出错",
|
||||
@@ -327,7 +333,7 @@
|
||||
"passToAuth": "传递至认证",
|
||||
"orgSettingsDescription": "配置组织设置",
|
||||
"orgGeneralSettings": "组织设置",
|
||||
"orgGeneralSettingsDescription": "管理机构的详细信息和配置",
|
||||
"orgGeneralSettingsDescription": "管理组织的详细信息和配置",
|
||||
"saveGeneralSettings": "保存常规设置",
|
||||
"saveSettings": "保存设置",
|
||||
"orgDangerZone": "危险区域",
|
||||
@@ -381,7 +387,7 @@
|
||||
"accessApprovalsDescription": "查看和管理待审批的组织访问权限",
|
||||
"description": "描述",
|
||||
"inviteTitle": "打开邀请",
|
||||
"inviteDescription": "管理其他用户加入机构的邀请",
|
||||
"inviteDescription": "管理其他用户加入组织的邀请",
|
||||
"inviteSearch": "搜索邀请...",
|
||||
"minutes": "分钟",
|
||||
"hours": "小时",
|
||||
@@ -425,24 +431,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 +468,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 +889,11 @@
|
||||
"resourcesErrorUpdateDescription": "更新资源时出错",
|
||||
"access": "访问权限",
|
||||
"accessControl": "访问控制",
|
||||
"shareLink": "{resource} 可共享链接",
|
||||
"shareLink": "{resource} 的共享链接",
|
||||
"resourceSelect": "选择资源",
|
||||
"shareLinks": "可共享链接",
|
||||
"shareLinks": "共享链接",
|
||||
"share": "分享链接",
|
||||
"shareDescription2": "创建资源的可共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
|
||||
"shareDescription2": "创建资源的共享链接。链接提供了对您资源的临时或无限制访问。 当您创建链接时,您可以配置链接的到期时间。",
|
||||
"shareEasyCreate": "轻松创建和分享",
|
||||
"shareConfigurableExpirationDuration": "可配置的过期时间",
|
||||
"shareSecureAndRevocable": "安全和可撤销的",
|
||||
@@ -1059,7 +1065,7 @@
|
||||
"network": "网络",
|
||||
"manage": "管理",
|
||||
"sitesNotFound": "未找到站点。",
|
||||
"pangolinServerAdmin": "服务器管理员 - Pangolin",
|
||||
"pangolinServerAdmin": "服务器管理 - Pangolin",
|
||||
"licenseTierProfessional": "专业许可证",
|
||||
"licenseTierEnterprise": "企业许可证",
|
||||
"licenseTierPersonal": "个人许可证",
|
||||
@@ -1366,7 +1372,7 @@
|
||||
"supportKeyBuy": "购买支持者密钥",
|
||||
"logoutError": "注销错误",
|
||||
"signingAs": "登录为",
|
||||
"serverAdmin": "服务器管理员",
|
||||
"serverAdmin": "服务器管理",
|
||||
"managedSelfhosted": "托管自托管",
|
||||
"otpEnable": "启用双因子认证",
|
||||
"otpDisable": "禁用双因子认证",
|
||||
@@ -1536,8 +1542,8 @@
|
||||
"sidebarSites": "站点",
|
||||
"sidebarApprovals": "审批请求",
|
||||
"sidebarResources": "资源",
|
||||
"sidebarProxyResources": "公开的",
|
||||
"sidebarClientResources": "非公开的",
|
||||
"sidebarProxyResources": "公开资源",
|
||||
"sidebarClientResources": "私有资源",
|
||||
"sidebarPolicies": "共享策略",
|
||||
"sidebarResourcePolicies": "公共资源",
|
||||
"sidebarAccessControl": "访问控制",
|
||||
@@ -1547,17 +1553,17 @@
|
||||
"sidebarAdmin": "管理员",
|
||||
"sidebarInvitations": "邀请",
|
||||
"sidebarRoles": "角色",
|
||||
"sidebarShareableLinks": "可共享链接",
|
||||
"sidebarShareableLinks": "共享链接",
|
||||
"sidebarApiKeys": "API密钥",
|
||||
"sidebarProvisioning": "置备中",
|
||||
"sidebarProvisioning": "预配",
|
||||
"sidebarSettings": "设置",
|
||||
"sidebarAllUsers": "所有用户",
|
||||
"sidebarIdentityProviders": "身份提供商",
|
||||
"sidebarLicense": "证书",
|
||||
"sidebarClients": "客户端",
|
||||
"sidebarUserDevices": "用户设备",
|
||||
"sidebarMachineClients": "机",
|
||||
"sidebarDomains": "域",
|
||||
"sidebarMachineClients": "机器身份",
|
||||
"sidebarDomains": "域名",
|
||||
"sidebarGeneral": "管理",
|
||||
"sidebarLogAndAnalytics": "日志与分析",
|
||||
"sidebarBluePrints": "蓝图",
|
||||
@@ -1638,7 +1644,7 @@
|
||||
"alertingActionType": "操作类型",
|
||||
"alertingNotifyUsers": "用户",
|
||||
"alertingNotifyRoles": "角色",
|
||||
"alertingNotifyEmails": "电子邮件地址",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "添加电子邮件并按回车键",
|
||||
"alertingWebhookMethod": "HTTP 方法",
|
||||
"alertingWebhookSecret": "签名密钥(可选)",
|
||||
@@ -1689,8 +1695,8 @@
|
||||
"alertingTabHealthChecks": "健康检查",
|
||||
"alertingRulesBannerTitle": "获取通知",
|
||||
"alertingRulesBannerDescription": "每条规则都连接要监视的对象(站点、健康检查或资源),触发时间(例如离线或不健康),以及如何通过电子邮件、Webhooks 或集成将通知发送给团队。使用此列表创建、启用和管理这些规则。",
|
||||
"alertingHealthChecksBannerTitle": "监视健康和资源",
|
||||
"alertingHealthChecksBannerDescription": "健康检查是您一次定义的 HTTP 或 TCP 监控。然后可以将它们用作告警规则中的来源,以便目标变得正常或不正常时得到通知。资源上的健康检查也会出现在此处。",
|
||||
"alertingHealthChecksBannerTitle": "资源与健康监控",
|
||||
"alertingHealthChecksBannerDescription": "通过 HTTP 或 TCP 检查目标状态,并在服务异常或恢复时发送通知。资源中配置的健康检查也会显示在这里。",
|
||||
"standaloneHcTableTitle": "健康检查",
|
||||
"standaloneHcSearchPlaceholder": "搜索健康检查…",
|
||||
"standaloneHcAddButton": "创建健康检查",
|
||||
@@ -1791,17 +1797,17 @@
|
||||
"theme": "主题",
|
||||
"subnetRequired": "子网是必填项",
|
||||
"initialSetupTitle": "初始服务器设置",
|
||||
"initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
|
||||
"initialSetupDescription": "创建初始的管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
|
||||
"createAdminAccount": "创建管理员帐户",
|
||||
"setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。",
|
||||
"setupErrorCreateAdmin": "创建管理员账户时发生错误。",
|
||||
"certificateStatus": "证书",
|
||||
"certificateStatusAutoRefreshHint": "状态自动刷新。",
|
||||
"loading": "加载中",
|
||||
"loadingEllipsis": "加载中……",
|
||||
"loadingAnalytics": "加载分析",
|
||||
"restart": "重启",
|
||||
"domains": "域",
|
||||
"domainsDescription": "创建和管理组织中可用的域",
|
||||
"domains": "域名",
|
||||
"domainsDescription": "创建和管理组织中可用的域名",
|
||||
"domainsSearch": "搜索域...",
|
||||
"domainAdd": "添加域",
|
||||
"domainAddDescription": "注册一个新域名到组织",
|
||||
@@ -2165,16 +2171,16 @@
|
||||
"sshSudoMode": "Sudo 访问",
|
||||
"sshSudoModeNone": "无",
|
||||
"sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
|
||||
"sshSudoModeFull": "全苏多",
|
||||
"sshSudoModeFull": "完整 Sudo 权限",
|
||||
"sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
|
||||
"sshSudoModeCommands": "命令",
|
||||
"sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
|
||||
"sshSudo": "允许Sudo",
|
||||
"sshSudoCommands": "Sudo 命令",
|
||||
"sshSudoCommandsDescription": "用户可以使用 sudo 运行的命令列表,以逗号、空格或新行分隔。必须使用绝对路径。",
|
||||
"sshSudoCommands": "可用 Sudo 命令",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "创建主目录",
|
||||
"sshUnixGroups": "Unix 组",
|
||||
"sshUnixGroupsDescription": "在目标主机上将用户添加到的 Unix 组,以逗号、空格或新行分隔。",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"roleTextFieldPlaceholder": "输入值,或放入 .txt 或 .csv 文件",
|
||||
"roleTextImportTitle": "从文件导入",
|
||||
"roleTextImportDescription": "将 {fileName} 导入到 {fieldLabel}。",
|
||||
@@ -2183,7 +2189,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 +2241,8 @@
|
||||
"resourceEditDomain": "编辑域名",
|
||||
"siteName": "站点名称",
|
||||
"proxyPort": "端口",
|
||||
"resourcesTableProxyResources": "公开的",
|
||||
"resourcesTableClientResources": "非公开的",
|
||||
"resourcesTableProxyResources": "",
|
||||
"resourcesTableClientResources": "私有资源",
|
||||
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
|
||||
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
||||
"resourcesTableDestination": "目标",
|
||||
@@ -2338,6 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 资源需要方案",
|
||||
"internalResourceHttpPortRequired": "HTTP 资源需要目的端口",
|
||||
"siteConfiguration": "配置",
|
||||
@@ -2925,7 +2932,7 @@
|
||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||
"logRetentionAccessLabel": "访问日志保留",
|
||||
"logRetentionAccessDescription": "保留访问日志的时间",
|
||||
"logRetentionActionLabel": "动作日志保留",
|
||||
"logRetentionActionLabel": "审计日志保留",
|
||||
"logRetentionActionDescription": "保留操作日志的时间",
|
||||
"logRetentionConnectionLabel": "连接日志保留",
|
||||
"logRetentionConnectionDescription": "保留连接日志的时间",
|
||||
@@ -2938,11 +2945,11 @@
|
||||
"logRetentionForever": "永远的",
|
||||
"logRetentionEndOfFollowingYear": "下一年结束",
|
||||
"actionLogsDescription": "查看此机构执行的操作历史",
|
||||
"accessLogsDescription": "查看此机构资源的访问认证请求",
|
||||
"accessLogsDescription": "查看此组织资源的访问认证请求",
|
||||
"connectionLogs": "连接日志",
|
||||
"connectionLogsDescription": "查看此机构隧道的连接日志",
|
||||
"sidebarLogsConnection": "连接日志",
|
||||
"sidebarLogsStreaming": "流流",
|
||||
"sidebarLogsStreaming": "事件流",
|
||||
"sourceAddress": "源地址",
|
||||
"destinationAddress": "目的地址",
|
||||
"duration": "期限",
|
||||
@@ -2967,6 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "缺少机构或域 ID",
|
||||
"loadingDNSRecords": "正在载入DNS记录...",
|
||||
"olmUpdateAvailableInfo": "有最新版本的 Olm 可用。请更新到最新版本以获取最佳体验。",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "客户端:",
|
||||
"proxyProtocol": "代理协议设置",
|
||||
"proxyProtocolDescription": "配置代理协议以保留TCP服务的客户端 IP 地址。",
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
primaryKey,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { InferSelectModel, sql } from "drizzle-orm";
|
||||
import {
|
||||
domains,
|
||||
orgs,
|
||||
@@ -207,17 +207,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")
|
||||
|
||||
+391
-281
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { InferSelectModel, sql } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
@@ -82,107 +82,130 @@ 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
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
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`)
|
||||
]
|
||||
);
|
||||
|
||||
export const labels = pgTable("labels", {
|
||||
labelId: serial("labelId").primaryKey(),
|
||||
@@ -267,71 +290,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(),
|
||||
@@ -406,43 +442,74 @@ export const networks = pgTable("networks", {
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const siteNetworks = pgTable("siteNetworks", {
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
networkId: integer("networkId")
|
||||
.notNull()
|
||||
.references(() => networks.networkId, { 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 clientSiteResources = pgTable("clientSiteResources", {
|
||||
clientId: integer("clientId")
|
||||
.notNull()
|
||||
.references(() => clients.clientId, { 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" })
|
||||
});
|
||||
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" })
|
||||
});
|
||||
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 +534,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 +647,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")
|
||||
@@ -1004,40 +1095,44 @@ 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)]
|
||||
);
|
||||
|
||||
export const clientSitesAssociationsCache = pgTable(
|
||||
"clientSitesAssociationsCache",
|
||||
@@ -1049,7 +1144,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 +1157,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 +1177,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(),
|
||||
|
||||
+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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { FeatureId, 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
newts,
|
||||
blueprints,
|
||||
Blueprint,
|
||||
Site,
|
||||
siteResources,
|
||||
roleSiteResources,
|
||||
userSiteResources,
|
||||
@@ -48,42 +47,38 @@ 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 +131,38 @@ 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
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.roleId));
|
||||
|
||||
const existingUserIds = await trx
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
userSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.userId));
|
||||
|
||||
const existingClientIds = await trx
|
||||
.select()
|
||||
.from(clientSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((row) => row.clientId));
|
||||
|
||||
// delete the existing site resource
|
||||
await trx
|
||||
.delete(siteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
siteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
result.oldSiteResource,
|
||||
trx
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((site) => ({
|
||||
// only need to run this on the old sites because the new sites are added above
|
||||
siteId: site.siteId,
|
||||
orgId: result.newSiteResource.orgId
|
||||
}))
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error handling messaging for updated site resource ${result.newSiteResource.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
} 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;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
db,
|
||||
olms,
|
||||
orgs,
|
||||
primaryDb,
|
||||
roleClients,
|
||||
roles,
|
||||
Transaction,
|
||||
@@ -23,10 +24,44 @@ 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
|
||||
queuedClients: ClientRow[]
|
||||
): void {
|
||||
if (queuedClients.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueClientsById = new Map<number, ClientRow>();
|
||||
for (const client of queuedClients) {
|
||||
uniqueClientsById.set(client.clientId, client);
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
for (const client of uniqueClientsById.values()) {
|
||||
try {
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed rebuilding associations for client ${client.clientId} (user ${userId}): ${String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
const queuedAssociationRebuilds: ClientRow[] = [];
|
||||
|
||||
const execute = async (transaction: Transaction | typeof db) => {
|
||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||
const adminRoleCache = new Map<
|
||||
@@ -189,7 +224,12 @@ export async function calculateUserClientsForOrgs(
|
||||
|
||||
if (userOlms.length === 0) {
|
||||
// No OLMs for this user, but we should still clean up any orphaned clients
|
||||
await cleanupOrphanedClients(userId, transaction);
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
[],
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,10 +422,7 @@ export async function calculateUserClientsForOrgs(
|
||||
.returning();
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(
|
||||
newClient,
|
||||
transaction
|
||||
);
|
||||
queuedAssociationRebuilds.push(newClient);
|
||||
|
||||
// Grant admin role access to the client
|
||||
await transaction.insert(roleClients).values({
|
||||
@@ -414,24 +451,22 @@ export async function calculateUserClientsForOrgs(
|
||||
}
|
||||
|
||||
// Clean up clients in orgs the user is no longer in
|
||||
await cleanupOrphanedClients(userId, transaction, userOrgIds);
|
||||
await cleanupOrphanedClients(
|
||||
userId,
|
||||
transaction,
|
||||
userOrgIds,
|
||||
queuedAssociationRebuilds
|
||||
);
|
||||
};
|
||||
|
||||
if (trx) {
|
||||
// Use provided transaction
|
||||
await execute(trx);
|
||||
} else {
|
||||
// Create new transaction
|
||||
await db.transaction(async (transaction) => {
|
||||
await execute(transaction);
|
||||
});
|
||||
}
|
||||
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 +496,9 @@ async function cleanupOrphanedClients(
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Rebuild associations for each deleted client to clean up related data
|
||||
// Queue deleted clients for post-transaction association cleanup.
|
||||
for (const deletedClient of deletedClients) {
|
||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
||||
queuedAssociationRebuilds.push(deletedClient);
|
||||
|
||||
if (deletedClient.olmId) {
|
||||
await sendTerminateClient(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
exitNodes,
|
||||
newts,
|
||||
olms,
|
||||
primaryDb,
|
||||
roleSiteResources,
|
||||
Site,
|
||||
SiteResource,
|
||||
@@ -20,10 +21,10 @@ import {
|
||||
} from "@server/db";
|
||||
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
||||
|
||||
import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers";
|
||||
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
||||
import {
|
||||
initPeerAddHandshake,
|
||||
deletePeer as olmDeletePeer
|
||||
initPeerAddHandshakeBatch,
|
||||
deletePeersBatch as olmDeletePeersBatch
|
||||
} from "@server/routers/olm/peers";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
import logger from "@server/logger";
|
||||
@@ -34,12 +35,13 @@ import {
|
||||
parseEndpoint
|
||||
} from "@server/lib/ip";
|
||||
import {
|
||||
addPeerData,
|
||||
addTargets as addSubnetProxyTargets,
|
||||
removePeerData,
|
||||
removeTargets as removeSubnetProxyTargets
|
||||
addPeerDataBatch,
|
||||
addTargetsBatch as addSubnetProxyTargetsBatch,
|
||||
removePeerDataBatch,
|
||||
removeTargetsBatch as removeSubnetProxyTargetsBatch
|
||||
} from "@server/routers/client/targets";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
||||
|
||||
// TTL for rebuild-association locks. These functions can fan out into many
|
||||
// peer/proxy updates, so give them a generous window.
|
||||
@@ -158,20 +160,35 @@ export async function getClientSiteResourceAccess(
|
||||
}
|
||||
|
||||
export async function rebuildClientAssociationsFromSiteResource(
|
||||
siteResource: SiteResource,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<{
|
||||
mergedAllClients: {
|
||||
clientId: number;
|
||||
pubKey: string | null;
|
||||
subnet: string | null;
|
||||
}[];
|
||||
}> {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource, trx),
|
||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||
);
|
||||
siteResource: SiteResource
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||
() =>
|
||||
rebuildClientAssociationsFromSiteResourceImpl(
|
||||
siteResource,
|
||||
trx
|
||||
),
|
||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (
|
||||
typeof err?.message === "string" &&
|
||||
err.message.startsWith("Failed to acquire lock")
|
||||
) {
|
||||
logger.warn(
|
||||
`rebuildClientAssociations: could not acquire lock for site resource ${siteResource.siteResourceId}, queuing for deferred processing`
|
||||
);
|
||||
await rebuildQueue.enqueue({
|
||||
type: "site-resource",
|
||||
id: siteResource.siteResourceId
|
||||
});
|
||||
return { mergedAllClients: [] };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
@@ -536,6 +553,28 @@ async function handleMessagesForSiteClients(
|
||||
const newtJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
const exitNodeJobs: Promise<any>[] = [];
|
||||
const newtPeerDeletes: {
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
newtId: string;
|
||||
}[] = [];
|
||||
const olmPeerDeletes: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
olmId: string;
|
||||
}[] = [];
|
||||
const olmPeerAddHandshakes: {
|
||||
clientId: number;
|
||||
peer: {
|
||||
siteId: number;
|
||||
exitNode: {
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
};
|
||||
};
|
||||
olmId: string;
|
||||
}[] = [];
|
||||
|
||||
// Combine all clients that need processing (those being added or removed)
|
||||
const clientsToProcess = new Map<
|
||||
@@ -584,6 +623,21 @@ async function handleMessagesForSiteClients(
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-fetch all olm IDs for the clients we need to process
|
||||
const clientIdsToProcess = Array.from(clientsToProcess.keys());
|
||||
const olmRows =
|
||||
clientIdsToProcess.length > 0
|
||||
? await trx
|
||||
.select({ olmId: olms.olmId, clientId: olms.clientId })
|
||||
.from(olms)
|
||||
.where(inArray(olms.clientId, clientIdsToProcess))
|
||||
: [];
|
||||
const olmByClientId = new Map<number, string>(
|
||||
olmRows
|
||||
.filter((r) => r.clientId !== null)
|
||||
.map((r) => [r.clientId as number, r.olmId])
|
||||
);
|
||||
|
||||
for (const client of clientsToProcess.values()) {
|
||||
// UPDATE THE NEWT
|
||||
if (!client.subnet || !client.pubKey) {
|
||||
@@ -600,14 +654,8 @@ async function handleMessagesForSiteClients(
|
||||
continue;
|
||||
}
|
||||
|
||||
const [olm] = await trx
|
||||
.select({
|
||||
olmId: olms.olmId
|
||||
})
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, client.clientId))
|
||||
.limit(1);
|
||||
if (!olm) {
|
||||
const olmId = olmByClientId.get(client.clientId);
|
||||
if (!olmId) {
|
||||
logger.warn(
|
||||
`Olm not found for client ${client.clientId} so cannot add/delete peers`
|
||||
);
|
||||
@@ -615,15 +663,17 @@ async function handleMessagesForSiteClients(
|
||||
}
|
||||
|
||||
if (isDelete) {
|
||||
newtJobs.push(newtDeletePeer(siteId, client.pubKey, newt.newtId));
|
||||
olmJobs.push(
|
||||
olmDeletePeer(
|
||||
client.clientId,
|
||||
siteId,
|
||||
site.publicKey,
|
||||
olm.olmId
|
||||
)
|
||||
);
|
||||
newtPeerDeletes.push({
|
||||
siteId,
|
||||
publicKey: client.pubKey,
|
||||
newtId: newt.newtId
|
||||
});
|
||||
olmPeerDeletes.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
publicKey: site.publicKey,
|
||||
olmId
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
@@ -635,23 +685,34 @@ async function handleMessagesForSiteClients(
|
||||
continue;
|
||||
}
|
||||
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clientId,
|
||||
{
|
||||
olmPeerAddHandshakes.push({
|
||||
clientId: client.clientId,
|
||||
peer: {
|
||||
siteId,
|
||||
exitNode: {
|
||||
publicKey: exitNode.publicKey,
|
||||
endpoint: exitNode.endpoint
|
||||
}
|
||||
},
|
||||
olm.olmId
|
||||
);
|
||||
olmId
|
||||
});
|
||||
}
|
||||
|
||||
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
|
||||
}
|
||||
|
||||
if (newtPeerDeletes.length > 0) {
|
||||
newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
|
||||
}
|
||||
|
||||
if (olmPeerDeletes.length > 0) {
|
||||
olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
|
||||
}
|
||||
|
||||
if (olmPeerAddHandshakes.length > 0) {
|
||||
olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
|
||||
}
|
||||
|
||||
Promise.all(exitNodeJobs).catch((error) => {
|
||||
logger.error(
|
||||
`rebuildClientAssociations: Error updating client site destinations for site ${site.siteId}:`,
|
||||
@@ -812,6 +873,20 @@ async function handleSubnetProxyTargetUpdates(
|
||||
): Promise<void> {
|
||||
const proxyJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
const targetsToAddBatch: {
|
||||
newtId: string;
|
||||
targets: NonNullable<
|
||||
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||
>;
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
const targetsToRemoveBatch: {
|
||||
newtId: string;
|
||||
targets: NonNullable<
|
||||
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||
>;
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
|
||||
for (const siteData of sitesList) {
|
||||
const siteId = siteData.siteId;
|
||||
@@ -843,25 +918,25 @@ async function handleSubnetProxyTargetUpdates(
|
||||
);
|
||||
|
||||
if (targetsToAdd) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToAdd,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
targetsToAddBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets: targetsToAdd,
|
||||
version: newt.version
|
||||
});
|
||||
}
|
||||
|
||||
for (const client of addedClients) {
|
||||
olmJobs.push(
|
||||
addPeerData(
|
||||
client.clientId,
|
||||
olmJobs.push(
|
||||
addPeerDataBatch(
|
||||
addedClients.map((client) => ({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
generateRemoteSubnets([siteResource]),
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
}
|
||||
remoteSubnets: generateRemoteSubnets([
|
||||
siteResource
|
||||
]),
|
||||
aliases: generateAliasConfig([siteResource])
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,15 +955,20 @@ async function handleSubnetProxyTargetUpdates(
|
||||
);
|
||||
|
||||
if (targetsToRemove) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToRemove,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
targetsToRemoveBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets: targetsToRemove,
|
||||
version: newt.version
|
||||
});
|
||||
}
|
||||
|
||||
const peerDataRemovals: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: ReturnType<typeof generateAliasConfig>;
|
||||
}[] = [];
|
||||
|
||||
for (const client of removedClients) {
|
||||
if (!siteResource.destination) {
|
||||
continue;
|
||||
@@ -936,31 +1016,58 @@ async function handleSubnetProxyTargetUpdates(
|
||||
? []
|
||||
: generateRemoteSubnets([siteResource]);
|
||||
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
siteId,
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
peerDataRemovals.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: remoteSubnetsToRemove,
|
||||
aliases: generateAliasConfig([siteResource])
|
||||
});
|
||||
}
|
||||
|
||||
if (peerDataRemovals.length > 0) {
|
||||
olmJobs.push(removePeerDataBatch(peerDataRemovals));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(proxyJobs);
|
||||
if (targetsToAddBatch.length > 0) {
|
||||
proxyJobs.push(addSubnetProxyTargetsBatch(targetsToAddBatch));
|
||||
}
|
||||
|
||||
if (targetsToRemoveBatch.length > 0) {
|
||||
proxyJobs.push(removeSubnetProxyTargetsBatch(targetsToRemoveBatch));
|
||||
}
|
||||
|
||||
await Promise.all([...proxyJobs, ...olmJobs]);
|
||||
}
|
||||
|
||||
export async function rebuildClientAssociationsFromClient(
|
||||
client: Client,
|
||||
trx: Transaction | typeof db = db
|
||||
client: Client
|
||||
): Promise<void> {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:client:${client.clientId}`,
|
||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||
);
|
||||
const trx = primaryDb;
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:client:${client.clientId}`,
|
||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (
|
||||
typeof err?.message === "string" &&
|
||||
err.message.startsWith("Failed to acquire lock")
|
||||
) {
|
||||
logger.warn(
|
||||
`rebuildClientAssociations: could not acquire lock for client ${client.clientId}, queuing for deferred processing`
|
||||
);
|
||||
await rebuildQueue.enqueue({
|
||||
type: "client",
|
||||
id: client.clientId
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildClientAssociationsFromClientImpl(
|
||||
@@ -1237,6 +1344,28 @@ async function handleMessagesForClientSites(
|
||||
const newtJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
const exitNodeJobs: Promise<any>[] = [];
|
||||
const newtPeerDeletes: {
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
newtId: string;
|
||||
}[] = [];
|
||||
const olmPeerDeletes: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
olmId: string;
|
||||
}[] = [];
|
||||
const olmPeerAddHandshakes: {
|
||||
clientId: number;
|
||||
peer: {
|
||||
siteId: number;
|
||||
exitNode: {
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
};
|
||||
};
|
||||
olmId: string;
|
||||
}[] = [];
|
||||
|
||||
const totalSitesOnClient = await trx
|
||||
.select({ count: count(clientSitesAssociationsCache.siteId) })
|
||||
@@ -1268,19 +1397,19 @@ async function handleMessagesForClientSites(
|
||||
|
||||
if (isRemove) {
|
||||
// Remove peer from newt
|
||||
newtJobs.push(
|
||||
newtDeletePeer(site.siteId, client.pubKey, newt.newtId)
|
||||
);
|
||||
newtPeerDeletes.push({
|
||||
siteId: site.siteId,
|
||||
publicKey: client.pubKey,
|
||||
newtId: newt.newtId
|
||||
});
|
||||
try {
|
||||
// Remove peer from olm
|
||||
olmJobs.push(
|
||||
olmDeletePeer(
|
||||
client.clientId,
|
||||
site.siteId,
|
||||
site.publicKey,
|
||||
olmId
|
||||
)
|
||||
);
|
||||
olmPeerDeletes.push({
|
||||
clientId: client.clientId,
|
||||
siteId: site.siteId,
|
||||
publicKey: site.publicKey,
|
||||
olmId
|
||||
});
|
||||
} catch (error) {
|
||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||
if (
|
||||
@@ -1312,10 +1441,9 @@ async function handleMessagesForClientSites(
|
||||
continue;
|
||||
}
|
||||
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clientId,
|
||||
{
|
||||
olmPeerAddHandshakes.push({
|
||||
clientId: client.clientId,
|
||||
peer: {
|
||||
siteId: site.siteId,
|
||||
exitNode: {
|
||||
publicKey: exitNode.publicKey,
|
||||
@@ -1323,7 +1451,7 @@ async function handleMessagesForClientSites(
|
||||
}
|
||||
},
|
||||
olmId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Update exit node destinations
|
||||
@@ -1339,6 +1467,18 @@ async function handleMessagesForClientSites(
|
||||
);
|
||||
}
|
||||
|
||||
if (newtPeerDeletes.length > 0) {
|
||||
newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
|
||||
}
|
||||
|
||||
if (olmPeerDeletes.length > 0) {
|
||||
olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
|
||||
}
|
||||
|
||||
if (olmPeerAddHandshakes.length > 0) {
|
||||
olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
|
||||
}
|
||||
|
||||
Promise.all(exitNodeJobs).catch((error) => {
|
||||
logger.error(
|
||||
`rebuildClientAssociations: Error updating client site destinations for client ${client.clientId}:`,
|
||||
@@ -1437,6 +1577,20 @@ async function handleMessagesForClientResources(
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetsToAddBatch: {
|
||||
newtId: string;
|
||||
targets: NonNullable<
|
||||
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||
>;
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
const peerDataAdds: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: ReturnType<typeof generateAliasConfig>;
|
||||
}[] = [];
|
||||
|
||||
for (const resource of resources) {
|
||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
@@ -1447,25 +1601,21 @@ async function handleMessagesForClientResources(
|
||||
]);
|
||||
|
||||
if (targets) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targets,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
targetsToAddBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets,
|
||||
version: newt.version
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Add peer data to olm
|
||||
olmJobs.push(
|
||||
addPeerData(
|
||||
client.clientId,
|
||||
siteId,
|
||||
generateRemoteSubnets([resource]),
|
||||
generateAliasConfig([resource])
|
||||
)
|
||||
);
|
||||
peerDataAdds.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: generateRemoteSubnets([resource]),
|
||||
aliases: generateAliasConfig([resource])
|
||||
});
|
||||
} catch (error) {
|
||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||
if (
|
||||
@@ -1480,6 +1630,14 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetsToAddBatch.length > 0) {
|
||||
proxyJobs.push(addSubnetProxyTargetsBatch(targetsToAddBatch));
|
||||
}
|
||||
|
||||
if (peerDataAdds.length > 0) {
|
||||
olmJobs.push(addPeerDataBatch(peerDataAdds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1546,6 +1704,20 @@ async function handleMessagesForClientResources(
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetsToRemoveBatch: {
|
||||
newtId: string;
|
||||
targets: NonNullable<
|
||||
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||
>;
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
const peerDataRemovals: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: ReturnType<typeof generateAliasConfig>;
|
||||
}[] = [];
|
||||
|
||||
for (const resource of resources) {
|
||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
@@ -1556,13 +1728,11 @@ async function handleMessagesForClientResources(
|
||||
]);
|
||||
|
||||
if (targets) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targets,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
targetsToRemoveBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets,
|
||||
version: newt.version
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1613,14 +1783,12 @@ async function handleMessagesForClientResources(
|
||||
: generateRemoteSubnets([resource]);
|
||||
|
||||
// Remove peer data from olm
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
siteId,
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([resource])
|
||||
)
|
||||
);
|
||||
peerDataRemovals.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: remoteSubnetsToRemove,
|
||||
aliases: generateAliasConfig([resource])
|
||||
});
|
||||
} catch (error) {
|
||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||
if (
|
||||
@@ -1635,6 +1803,16 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetsToRemoveBatch.length > 0) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargetsBatch(targetsToRemoveBatch)
|
||||
);
|
||||
}
|
||||
|
||||
if (peerDataRemovals.length > 0) {
|
||||
olmJobs.push(removePeerDataBatch(peerDataRemovals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1884,11 +2062,20 @@ export async function cleanupSiteAssociations(
|
||||
|
||||
// 7. Fire all removal messages in parallel.
|
||||
const jobs: Promise<any>[] = [];
|
||||
const olmPeerDeletes: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
}[] = [];
|
||||
|
||||
for (const client of allClients) {
|
||||
// Tell each olm to drop the site's WireGuard peer.
|
||||
if (site.publicKey) {
|
||||
jobs.push(olmDeletePeer(client.clientId, siteId, site.publicKey));
|
||||
olmPeerDeletes.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
publicKey: site.publicKey
|
||||
});
|
||||
}
|
||||
|
||||
// Recompute and push updated relay destinations (now excluding this site).
|
||||
@@ -1897,6 +2084,10 @@ export async function cleanupSiteAssociations(
|
||||
}
|
||||
}
|
||||
|
||||
if (olmPeerDeletes.length > 0) {
|
||||
jobs.push(olmDeletePeersBatch(olmPeerDeletes));
|
||||
}
|
||||
|
||||
await Promise.all(jobs).catch((error) => {
|
||||
logger.error(
|
||||
`cleanupSiteAssociations: error sending cleanup messages for siteId=${siteId}:`,
|
||||
@@ -1906,3 +2097,44 @@ export async function cleanupSiteAssociations(
|
||||
|
||||
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background rebuild queue processor. This should be called once
|
||||
* during server startup. Only one server instance at a time will actively
|
||||
* consume the queue (enforced via a distributed Redis lock); all other
|
||||
* instances will poll and wait until the lock becomes available.
|
||||
*/
|
||||
export function startRebuildQueueProcessor(): void {
|
||||
rebuildQueue.startProcessing({
|
||||
onSiteResource: async (siteResourceId: number) => {
|
||||
const [siteResource] = await primaryDb
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(eq(siteResources.siteResourceId, siteResourceId));
|
||||
|
||||
if (!siteResource) {
|
||||
logger.warn(
|
||||
`Rebuild queue: site resource ${siteResourceId} not found, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource);
|
||||
},
|
||||
onClient: async (clientId: number) => {
|
||||
const [client] = await primaryDb
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
if (!client) {
|
||||
logger.warn(
|
||||
`Rebuild queue: client ${clientId} not found, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
|
||||
class NoopRebuildQueue implements RebuildQueueManager {
|
||||
async enqueue(_job: RebuildJob): Promise<void> {}
|
||||
startProcessing(_handlers: RebuildJobHandlers): void {}
|
||||
}
|
||||
|
||||
export const rebuildQueue: RebuildQueueManager = new NoopRebuildQueue();
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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 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();
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -277,6 +277,8 @@ hybridRouter.get(
|
||||
);
|
||||
}
|
||||
|
||||
const pangolinUIUrl = config.getRawConfig().app.dashboard_url; // points to the dashboard to serve from there
|
||||
|
||||
try {
|
||||
const traefikConfig = await getTraefikConfig(
|
||||
remoteExitNode.exitNodeId,
|
||||
@@ -284,8 +286,8 @@ hybridRouter.get(
|
||||
true, // But don't allow domain namespace resources
|
||||
false, // Dont include login pages,
|
||||
true, // allow raw resources
|
||||
false, // dont generate maintenance page
|
||||
false // dont generate browser gateway targets
|
||||
pangolinUIUrl, // dont generate maintenance page
|
||||
pangolinUIUrl // generate browser gateway targets
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function unassociateOrgIdp(
|
||||
});
|
||||
|
||||
for (const userId of userIdsToRemove) {
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}`
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||
import cache from "#private/lib/cache";
|
||||
import { regionalCache as cache } from "#private/lib/cache";
|
||||
import semver from "semver";
|
||||
|
||||
let stalePangolinNodeVersion: string | null = null;
|
||||
|
||||
@@ -163,13 +163,11 @@ export async function addUserRole(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -170,13 +170,11 @@ export async function removeUserRole(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -150,13 +150,11 @@ export async function setUserOrgRoles(
|
||||
});
|
||||
|
||||
for (const orgClient of orgClientsToRebuild) {
|
||||
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(orgClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
|
||||
+257
-47
@@ -38,6 +38,7 @@ import { messageHandlers } from "@server/routers/ws/messageHandlers";
|
||||
import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers";
|
||||
import {
|
||||
AuthenticatedWebSocket,
|
||||
BatchSendMessage,
|
||||
ClientType,
|
||||
WSMessage,
|
||||
TokenPayload,
|
||||
@@ -187,6 +188,8 @@ const wss: WebSocketServer = new WebSocketServer({ noServer: true });
|
||||
// Generate unique node ID for this instance
|
||||
const NODE_ID = uuidv4();
|
||||
const REDIS_CHANNEL = "websocket_messages";
|
||||
const REDIS_DIRECT_BATCH_SIZE = 250;
|
||||
const REDIS_DIRECT_FLUSH_INTERVAL_MS = 10;
|
||||
|
||||
// Client tracking map (local to this node)
|
||||
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
||||
@@ -197,6 +200,15 @@ const clientConfigVersions: Map<string, number> = new Map();
|
||||
// Recovery tracking
|
||||
let isRedisRecoveryInProgress = false;
|
||||
|
||||
interface RedisDirectBatchEntry {
|
||||
targetClientId: string;
|
||||
message: WSMessage;
|
||||
resolve: () => void;
|
||||
}
|
||||
|
||||
let pendingRedisDirectMessages: RedisDirectBatchEntry[] = [];
|
||||
let redisDirectFlushTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
// Helper to get map key
|
||||
const getClientMapKey = (clientId: string) => clientId;
|
||||
|
||||
@@ -207,6 +219,78 @@ const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
|
||||
const getConfigVersionKey = (clientId: string) =>
|
||||
`ws:configVersion:${clientId}`;
|
||||
|
||||
const clearRedisDirectFlushTimer = (): void => {
|
||||
if (redisDirectFlushTimer) {
|
||||
clearTimeout(redisDirectFlushTimer);
|
||||
redisDirectFlushTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const publishDirectBatch = async (
|
||||
entries: RedisDirectBatchEntry[]
|
||||
): Promise<void> => {
|
||||
const redisMessage: RedisMessage = {
|
||||
type: "direct-batch",
|
||||
messages: entries.map((entry) => ({
|
||||
targetClientId: entry.targetClientId,
|
||||
message: entry.message
|
||||
})),
|
||||
fromNodeId: NODE_ID
|
||||
};
|
||||
|
||||
await redisManager.publish(REDIS_CHANNEL, JSON.stringify(redisMessage));
|
||||
};
|
||||
|
||||
const flushPendingRedisDirectMessages = async (): Promise<void> => {
|
||||
clearRedisDirectFlushTimer();
|
||||
|
||||
if (pendingRedisDirectMessages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = pendingRedisDirectMessages;
|
||||
pendingRedisDirectMessages = [];
|
||||
|
||||
if (!redisManager.isRedisEnabled()) {
|
||||
entries.forEach((entry) => entry.resolve());
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i += REDIS_DIRECT_BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
|
||||
try {
|
||||
await publishDirectBatch(batch);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to send batched direct messages via Redis, messages may be lost:",
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
batch.forEach((entry) => entry.resolve());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueRedisDirectMessage = async (
|
||||
targetClientId: string,
|
||||
message: WSMessage
|
||||
): Promise<void> => {
|
||||
await new Promise<void>((resolve) => {
|
||||
pendingRedisDirectMessages.push({ targetClientId, message, resolve });
|
||||
|
||||
if (pendingRedisDirectMessages.length >= REDIS_DIRECT_BATCH_SIZE) {
|
||||
void flushPendingRedisDirectMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!redisDirectFlushTimer) {
|
||||
redisDirectFlushTimer = setTimeout(() => {
|
||||
void flushPendingRedisDirectMessages();
|
||||
}, REDIS_DIRECT_FLUSH_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize Redis subscription for cross-node messaging
|
||||
const initializeRedisSubscription = async (): Promise<void> => {
|
||||
if (!redisManager.isRedisEnabled()) return;
|
||||
@@ -227,7 +311,16 @@ const initializeRedisSubscription = async (): Promise<void> => {
|
||||
// Send to specific client on this node
|
||||
await sendToClientLocal(
|
||||
redisMessage.targetClientId,
|
||||
redisMessage.message
|
||||
redisMessage.message,
|
||||
{},
|
||||
redisMessage.message.configVersion
|
||||
);
|
||||
} else if (
|
||||
redisMessage.type === "direct-batch" &&
|
||||
redisMessage.messages
|
||||
) {
|
||||
await sendRedisDirectBatchToLocalClients(
|
||||
redisMessage.messages
|
||||
);
|
||||
} else if (redisMessage.type === "broadcast") {
|
||||
// Broadcast to all clients on this node except excluded
|
||||
@@ -503,7 +596,8 @@ const incrementClientConfigVersion = async (
|
||||
const sendToClientLocal = async (
|
||||
clientId: string,
|
||||
message: WSMessage,
|
||||
options: SendMessageOptions = {}
|
||||
options: SendMessageOptions = {},
|
||||
preResolvedConfigVersion?: number
|
||||
): Promise<boolean> => {
|
||||
const mapKey = getClientMapKey(clientId);
|
||||
const clients = connectedClients.get(mapKey);
|
||||
@@ -512,7 +606,8 @@ const sendToClientLocal = async (
|
||||
}
|
||||
|
||||
// Handle config version
|
||||
const configVersion = await getClientConfigVersion(clientId);
|
||||
const configVersion =
|
||||
preResolvedConfigVersion ?? (await getClientConfigVersion(clientId));
|
||||
|
||||
// Add config version to message
|
||||
const messageWithVersion = {
|
||||
@@ -545,43 +640,71 @@ const sendToClientLocal = async (
|
||||
return true;
|
||||
};
|
||||
|
||||
const sendRedisDirectBatchToLocalClients = async (
|
||||
entries: { targetClientId: string; message: WSMessage }[]
|
||||
): Promise<void> => {
|
||||
const jobs = entries.map((entry) =>
|
||||
sendToClientLocal(
|
||||
entry.targetClientId,
|
||||
entry.message,
|
||||
{},
|
||||
entry.message.configVersion
|
||||
)
|
||||
);
|
||||
await Promise.all(jobs);
|
||||
};
|
||||
|
||||
const broadcastToAllExceptLocal = async (
|
||||
message: WSMessage,
|
||||
excludeClientId?: string,
|
||||
options: SendMessageOptions = {}
|
||||
): Promise<void> => {
|
||||
for (const [mapKey, clients] of connectedClients.entries()) {
|
||||
const [type, id] = mapKey.split(":");
|
||||
const clientId = mapKey; // mapKey is the clientId
|
||||
if (!(excludeClientId && clientId === excludeClientId)) {
|
||||
// Handle config version per client
|
||||
let configVersion = await getClientConfigVersion(clientId);
|
||||
if (options.incrementConfigVersion) {
|
||||
configVersion = await incrementClientConfigVersion(clientId);
|
||||
}
|
||||
const sendPlans = await Promise.all(
|
||||
Array.from(connectedClients.entries()).map(
|
||||
async ([mapKey, clients]) => {
|
||||
const clientId = mapKey; // mapKey is the clientId
|
||||
if (excludeClientId && clientId === excludeClientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add config version to message
|
||||
const messageWithVersion = {
|
||||
...message,
|
||||
configVersion
|
||||
};
|
||||
let configVersion = await getClientConfigVersion(clientId);
|
||||
if (options.incrementConfigVersion) {
|
||||
configVersion =
|
||||
await incrementClientConfigVersion(clientId);
|
||||
}
|
||||
|
||||
if (options.compress) {
|
||||
const compressed = zlib.gzipSync(
|
||||
Buffer.from(JSON.stringify(messageWithVersion), "utf8")
|
||||
);
|
||||
clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(compressed);
|
||||
return {
|
||||
clients,
|
||||
messageWithVersion: {
|
||||
...message,
|
||||
configVersion
|
||||
}
|
||||
});
|
||||
} else {
|
||||
clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(messageWithVersion));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
for (const plan of sendPlans) {
|
||||
if (!plan) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.compress) {
|
||||
const compressed = zlib.gzipSync(
|
||||
Buffer.from(JSON.stringify(plan.messageWithVersion), "utf8")
|
||||
);
|
||||
plan.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(compressed);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const messageString = JSON.stringify(plan.messageWithVersion);
|
||||
plan.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(messageString);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -602,28 +725,23 @@ const sendToClient = async (
|
||||
);
|
||||
|
||||
// Try to send locally first
|
||||
const localSent = await sendToClientLocal(clientId, message, options);
|
||||
const localSent = await sendToClientLocal(
|
||||
clientId,
|
||||
message,
|
||||
options,
|
||||
configVersion
|
||||
);
|
||||
|
||||
// Only send via Redis if the client is not connected locally and Redis is enabled
|
||||
if (!localSent && redisManager.isRedisEnabled()) {
|
||||
try {
|
||||
const redisMessage: RedisMessage = {
|
||||
type: "direct",
|
||||
targetClientId: clientId,
|
||||
message: {
|
||||
...message,
|
||||
configVersion
|
||||
},
|
||||
fromNodeId: NODE_ID
|
||||
};
|
||||
|
||||
await redisManager.publish(
|
||||
REDIS_CHANNEL,
|
||||
JSON.stringify(redisMessage)
|
||||
);
|
||||
await enqueueRedisDirectMessage(clientId, {
|
||||
...message,
|
||||
configVersion
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to send message via Redis, message may be lost:",
|
||||
"Failed to queue batched direct message for Redis delivery, message may be lost:",
|
||||
error
|
||||
);
|
||||
// Continue execution - local delivery already attempted
|
||||
@@ -638,6 +756,95 @@ const sendToClient = async (
|
||||
return localSent;
|
||||
};
|
||||
|
||||
const sendToClientsBatch = async (
|
||||
entries: BatchSendMessage[]
|
||||
): Promise<void> => {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteEntries: { targetClientId: string; message: WSMessage }[] = [];
|
||||
const clientsWithIncrement = new Set(
|
||||
entries
|
||||
.filter((entry) => !!entry.options?.incrementConfigVersion)
|
||||
.map((entry) => entry.clientId)
|
||||
);
|
||||
const nonIncrementOnlyClientIds = Array.from(
|
||||
new Set(
|
||||
entries
|
||||
.map((entry) => entry.clientId)
|
||||
.filter((clientId) => !clientsWithIncrement.has(clientId))
|
||||
)
|
||||
);
|
||||
const stableConfigVersionByClient = new Map<string, number | undefined>(
|
||||
await Promise.all(
|
||||
nonIncrementOnlyClientIds.map(
|
||||
async (clientId) =>
|
||||
[clientId, await getClientConfigVersion(clientId)] as const
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
for (const entry of entries) {
|
||||
const options = entry.options || {};
|
||||
const { clientId, message } = entry;
|
||||
|
||||
const configVersion = options.incrementConfigVersion
|
||||
? await incrementClientConfigVersion(clientId)
|
||||
: stableConfigVersionByClient.get(clientId);
|
||||
|
||||
logger.debug(
|
||||
`sendToClientsBatch: Message type ${message.type} queued for clientId ${clientId} (new configVersion: ${configVersion})`
|
||||
);
|
||||
|
||||
const localSent = await sendToClientLocal(
|
||||
clientId,
|
||||
message,
|
||||
options,
|
||||
configVersion
|
||||
);
|
||||
|
||||
if (!localSent && redisManager.isRedisEnabled()) {
|
||||
remoteEntries.push({
|
||||
targetClientId: clientId,
|
||||
message: {
|
||||
...message,
|
||||
configVersion
|
||||
}
|
||||
});
|
||||
} else if (!localSent && !redisManager.isRedisEnabled()) {
|
||||
logger.debug(
|
||||
`Could not deliver batch message to ${clientId} - not connected locally and Redis unavailable`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!redisManager.isRedisEnabled() || remoteEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < remoteEntries.length; i += REDIS_DIRECT_BATCH_SIZE) {
|
||||
const messages = remoteEntries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
|
||||
try {
|
||||
const redisMessage: RedisMessage = {
|
||||
type: "direct-batch",
|
||||
messages,
|
||||
fromNodeId: NODE_ID
|
||||
};
|
||||
|
||||
await redisManager.publish(
|
||||
REDIS_CHANNEL,
|
||||
JSON.stringify(redisMessage)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to send explicit direct batch via Redis, messages may be lost:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const broadcastToAllExcept = async (
|
||||
message: WSMessage,
|
||||
excludeClientId?: string,
|
||||
@@ -1109,6 +1316,8 @@ const disconnectClient = async (clientId: string): Promise<boolean> => {
|
||||
// Cleanup function for graceful shutdown
|
||||
const cleanup = async (): Promise<void> => {
|
||||
try {
|
||||
await flushPendingRedisDirectMessages();
|
||||
|
||||
// Close all WebSocket connections
|
||||
connectedClients.forEach((clients) => {
|
||||
clients.forEach((client) => {
|
||||
@@ -1139,6 +1348,7 @@ export {
|
||||
router,
|
||||
handleWSUpgrade,
|
||||
sendToClient,
|
||||
sendToClientsBatch,
|
||||
broadcastToAllExcept,
|
||||
connectedClients,
|
||||
hasActiveConnections,
|
||||
|
||||
@@ -30,7 +30,7 @@ const listAccessTokensParamsSchema = z
|
||||
error: "Either resourceId or orgId must be provided, but not both"
|
||||
});
|
||||
|
||||
const listAccessTokensSchema = z.object({
|
||||
const listAccessTokensSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -15,7 +15,7 @@ const paramsSchema = z.object({
|
||||
apiKeyId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -9,7 +9,7 @@ import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -20,7 +20,7 @@ import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
|
||||
export const queryAccessAuditLogsQuery = z.object({
|
||||
export const queryAccessAuditLogsQuery = z.strictObject({
|
||||
// iso string just validate its a parseable date
|
||||
timeStart: z
|
||||
.string()
|
||||
|
||||
@@ -224,7 +224,7 @@ export async function deleteMyAccount(
|
||||
}
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { assertEquals } from "@test/assert";
|
||||
import { REGIONS } from "@server/db/regions";
|
||||
import { isPathAllowed } from "@server/lib/pathMatch";
|
||||
|
||||
function isIpInRegion(
|
||||
ipCountryCode: string | undefined,
|
||||
@@ -33,76 +34,6 @@ function isIpInRegion(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPathAllowed(pattern: string, path: string): boolean {
|
||||
// Normalize and split paths into segments
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
||||
if (patternIndex >= patternParts.length) {
|
||||
const result = pathIndex >= pathParts.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we've consumed all path parts but still have pattern parts
|
||||
if (pathIndex >= pathParts.length) {
|
||||
// The only way this can match is if all remaining pattern parts are wildcards
|
||||
const remainingPattern = patternParts.slice(patternIndex);
|
||||
const result = remainingPattern.every((p) => p === "*");
|
||||
return result;
|
||||
}
|
||||
|
||||
// For full segment wildcards, try consuming different numbers of path segments
|
||||
if (currentPatternPart === "*") {
|
||||
// Try consuming 0 segments (skip the wildcard)
|
||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try consuming current segment and recursively try rest
|
||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
||||
if (currentPatternPart.includes("*")) {
|
||||
// Convert the pattern segment to a regex pattern
|
||||
const regexPattern = currentPatternPart
|
||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
|
||||
if (regex.test(currentPathPart)) {
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// For regular segments, they must match exactly
|
||||
if (currentPatternPart !== currentPathPart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move to next segments in both pattern and path
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
}
|
||||
|
||||
const result = matchSegments(0, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log("Running path matching tests...");
|
||||
|
||||
@@ -308,6 +239,121 @@ function runTests() {
|
||||
console.log("All path matching tests passed!");
|
||||
}
|
||||
|
||||
function runSpecialCharacterTests() {
|
||||
console.log("\nRunning special character tests...");
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
isPathAllowed("(api*", "anything");
|
||||
isPathAllowed("a(b*", "a(bc");
|
||||
isPathAllowed("c[d*", "c[de");
|
||||
isPathAllowed("x{2}*", "x{2}y");
|
||||
isPathAllowed("a|b*", "a|bc");
|
||||
isPathAllowed("back\\slash*", "back\\slashed");
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
console.error(
|
||||
"Patterns accepted by isValidUrlGlobPattern crashed the matcher:",
|
||||
e instanceof Error ? e.message : e
|
||||
);
|
||||
}
|
||||
assertEquals(
|
||||
threw,
|
||||
false,
|
||||
"Patterns with regex metacharacters must not throw"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("(api*", "(api-v1"),
|
||||
true,
|
||||
"Parenthesis should be treated as a literal character"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("(api*", "xapi-v1"),
|
||||
false,
|
||||
"Parenthesis should not match other characters"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("a(b)*", "a(b)c"),
|
||||
true,
|
||||
"Parentheses pair should be treated as literal characters"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("*.png", "image.png"),
|
||||
true,
|
||||
"Dot should match a literal dot"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("*.png", "imageXpng"),
|
||||
false,
|
||||
"Dot should not act as a regex wildcard"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("v1.0*", "v1.0.1"),
|
||||
true,
|
||||
"Version-like literal should match itself"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("v1.0*", "v1x0-beta"),
|
||||
false,
|
||||
"Version-like literal should not match arbitrary characters"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("a+b*", "a+bc"),
|
||||
true,
|
||||
"Plus should be treated as a literal character"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("a+b*", "aaabc"),
|
||||
false,
|
||||
"Plus should not act as a regex quantifier"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("$ref*", "$refs"),
|
||||
true,
|
||||
"Dollar sign should be treated as a literal character"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("price$*", "price$100"),
|
||||
true,
|
||||
"Dollar sign mid-pattern should be treated as a literal character"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("^start*", "^started"),
|
||||
true,
|
||||
"Caret should be treated as a literal character"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("a|b*", "a|bc"),
|
||||
true,
|
||||
"Pipe should be treated as a literal character"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("a|b*", "a"),
|
||||
false,
|
||||
"Pipe should not act as regex alternation"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("file?*", "fileX"),
|
||||
true,
|
||||
"Question mark should still act as a single-character wildcard"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
isPathAllowed("api/*", "api/" + "x/".repeat(50)),
|
||||
true,
|
||||
"Deeply nested paths should still match"
|
||||
);
|
||||
|
||||
console.log("All special character tests passed!");
|
||||
}
|
||||
|
||||
function runRegionTests() {
|
||||
console.log("\nRunning isIpInRegion tests...");
|
||||
|
||||
@@ -367,6 +413,7 @@ function runRegionTests() {
|
||||
// Run all tests
|
||||
try {
|
||||
runTests();
|
||||
runSpecialCharacterTests();
|
||||
runRegionTests();
|
||||
console.log("\n✅ All tests passed!");
|
||||
} catch (error) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||
import { isPathAllowed } from "@server/lib/pathMatch";
|
||||
import { response } from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -1090,143 +1091,7 @@ async function checkRules(
|
||||
return;
|
||||
}
|
||||
|
||||
export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(`\nMatching path "${path}" against pattern "${pattern}"`);
|
||||
|
||||
// Normalize and split paths into segments
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
|
||||
logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
|
||||
logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
|
||||
|
||||
// Maximum recursion depth to prevent stack overflow and memory issues
|
||||
const MAX_RECURSION_DEPTH = 100;
|
||||
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(
|
||||
patternIndex: number,
|
||||
pathIndex: number,
|
||||
depth: number = 0
|
||||
): boolean {
|
||||
// Check recursion depth limit
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
logger.warn(
|
||||
`Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const indent = " ".repeat(depth); // Indent based on recursion depth
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
logger.debug(
|
||||
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]`
|
||||
);
|
||||
|
||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
||||
if (patternIndex >= patternParts.length) {
|
||||
const result = pathIndex >= pathParts.length;
|
||||
logger.debug(
|
||||
`${indent}Reached end of pattern, remaining path: ${pathParts.slice(pathIndex).join("/")} -> ${result}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we've consumed all path parts but still have pattern parts
|
||||
if (pathIndex >= pathParts.length) {
|
||||
// The only way this can match is if all remaining pattern parts are wildcards
|
||||
const remainingPattern = patternParts.slice(patternIndex);
|
||||
const result = remainingPattern.every((p) => p === "*");
|
||||
logger.debug(
|
||||
`${indent}Reached end of path, remaining pattern: ${remainingPattern.join("/")} -> ${result}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// For full segment wildcards, try consuming different numbers of path segments
|
||||
if (currentPatternPart === "*") {
|
||||
logger.debug(
|
||||
`${indent}Found wildcard at pattern index ${patternIndex}`
|
||||
);
|
||||
|
||||
// Try consuming 0 segments (skip the wildcard)
|
||||
logger.debug(
|
||||
`${indent}Trying to skip wildcard (consume 0 segments)`
|
||||
);
|
||||
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by skipping wildcard`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try consuming current segment and recursively try rest
|
||||
logger.debug(
|
||||
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
||||
);
|
||||
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
|
||||
logger.debug(
|
||||
`${indent}Successfully matched by consuming segment for wildcard`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug(`${indent}Failed to match wildcard`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
||||
if (currentPatternPart.includes("*")) {
|
||||
logger.debug(
|
||||
`${indent}Found in-segment wildcard in "${currentPatternPart}"`
|
||||
);
|
||||
|
||||
// Convert the pattern segment to a regex pattern
|
||||
const regexPattern = currentPatternPart
|
||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
|
||||
if (regex.test(currentPathPart)) {
|
||||
logger.debug(
|
||||
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
||||
);
|
||||
return matchSegments(
|
||||
patternIndex + 1,
|
||||
pathIndex + 1,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${indent}Segment with wildcard mismatch: "${currentPatternPart}" doesn't match "${currentPathPart}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For regular segments, they must match exactly
|
||||
if (currentPatternPart !== currentPathPart) {
|
||||
logger.debug(
|
||||
`${indent}Segment mismatch: "${currentPatternPart}" != "${currentPathPart}"`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
||||
);
|
||||
// Move to next segments in both pattern and path
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||
}
|
||||
|
||||
const result = matchSegments(0, 0, 0);
|
||||
logger.debug(`Final result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
export { isPathAllowed };
|
||||
|
||||
async function isIpInGeoIP(
|
||||
ipCountryCode: string | undefined,
|
||||
|
||||
@@ -280,13 +280,11 @@ export async function createClient(
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(newClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating client: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response<CreateClientResponse>(res, {
|
||||
|
||||
@@ -255,13 +255,11 @@ export async function createUserClient(
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating user client: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(newClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after creating user client: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return response<CreateClientAndOlmResponse>(res, {
|
||||
|
||||
@@ -109,13 +109,11 @@ export async function deleteClient(
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(deletedClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
if (olm) {
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
|
||||
@@ -41,7 +41,7 @@ const listClientsParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listClientsSchema = z.object({
|
||||
const listClientsSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -40,7 +40,7 @@ const listUserDevicesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserDevicesSchema = z.object({
|
||||
const listUserDevicesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
@@ -420,31 +420,6 @@ export async function listUserDevices(
|
||||
}
|
||||
);
|
||||
|
||||
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
||||
// // Try to get the latest version, but don't block if it fails
|
||||
// try {
|
||||
// const latestOlmVersion = await getLatestOlmVersion();
|
||||
|
||||
// if (latestOlmVersion) {
|
||||
// olmsWithUpdates.forEach((client) => {
|
||||
// try {
|
||||
// client.olmUpdateAvailable = semver.lt(
|
||||
// client.olmVersion ? client.olmVersion : "",
|
||||
// latestOlmVersion
|
||||
// );
|
||||
// } catch (error) {
|
||||
// client.olmUpdateAvailable = false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// // Log the error but don't let it block the response
|
||||
// logger.warn(
|
||||
// "Failed to check for OLM updates, continuing without update info:",
|
||||
// error
|
||||
// );
|
||||
// }
|
||||
|
||||
return response<ListUserDevicesResponse>(res, {
|
||||
data: {
|
||||
devices: olmsWithUpdates,
|
||||
|
||||
@@ -60,13 +60,17 @@ export async function rebuildClientAssociationsCacheRoute(
|
||||
);
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromClient(client);
|
||||
rebuildClientAssociationsFromClient(client).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${clientId}: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Client association cache rebuilt successfully",
|
||||
message: "Client association cache queued successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||
import { db, newts, olms } from "@server/db";
|
||||
import {
|
||||
Alias,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@server/lib/ip";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import logger from "@server/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import semver from "semver";
|
||||
|
||||
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
|
||||
@@ -59,6 +59,42 @@ export async function addTargets(
|
||||
);
|
||||
}
|
||||
|
||||
export async function addTargetsBatch(
|
||||
entries: {
|
||||
newtId: string;
|
||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = await Promise.all(
|
||||
entries.map(async (entry) => ({
|
||||
...entry,
|
||||
targets: await convertTargetsIfNecessary(
|
||||
entry.newtId,
|
||||
entry.targets
|
||||
)
|
||||
}))
|
||||
);
|
||||
|
||||
await sendToClientsBatch(
|
||||
resolved.map((entry) => ({
|
||||
clientId: entry.newtId,
|
||||
message: {
|
||||
type: `newt/wg/targets/add`,
|
||||
data: entry.targets
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(entry.version, "newt")
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeTargets(
|
||||
newtId: string,
|
||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
||||
@@ -76,6 +112,42 @@ export async function removeTargets(
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeTargetsBatch(
|
||||
entries: {
|
||||
newtId: string;
|
||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = await Promise.all(
|
||||
entries.map(async (entry) => ({
|
||||
...entry,
|
||||
targets: await convertTargetsIfNecessary(
|
||||
entry.newtId,
|
||||
entry.targets
|
||||
)
|
||||
}))
|
||||
);
|
||||
|
||||
await sendToClientsBatch(
|
||||
resolved.map((entry) => ({
|
||||
clientId: entry.newtId,
|
||||
message: {
|
||||
type: `newt/wg/targets/remove`,
|
||||
data: entry.targets
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(entry.version, "newt")
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateTargets(
|
||||
newtId: string,
|
||||
targets: {
|
||||
@@ -201,6 +273,171 @@ export async function removePeerData(
|
||||
});
|
||||
}
|
||||
|
||||
const resolveOlmTargets = async (
|
||||
entries: {
|
||||
clientId: number;
|
||||
olmId?: string;
|
||||
version?: string | null;
|
||||
}[]
|
||||
) => {
|
||||
const unresolvedClientIds = entries
|
||||
.filter((entry) => !entry.olmId)
|
||||
.map((entry) => entry.clientId);
|
||||
|
||||
const olmMap = new Map<number, { olmId: string; version: string | null }>();
|
||||
|
||||
if (unresolvedClientIds.length > 0) {
|
||||
const olmRows = await db
|
||||
.select({
|
||||
clientId: olms.clientId,
|
||||
olmId: olms.olmId,
|
||||
version: olms.version
|
||||
})
|
||||
.from(olms)
|
||||
.where(inArray(olms.clientId, unresolvedClientIds));
|
||||
|
||||
for (const row of olmRows) {
|
||||
if (row.clientId !== null) {
|
||||
olmMap.set(row.clientId, {
|
||||
olmId: row.olmId,
|
||||
version: row.version
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
.map((entry) => {
|
||||
if (entry.olmId) {
|
||||
return {
|
||||
clientId: entry.clientId,
|
||||
olmId: entry.olmId,
|
||||
version: entry.version
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = olmMap.get(entry.clientId);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: entry.clientId,
|
||||
olmId: resolved.olmId,
|
||||
version: entry.version ?? resolved.version
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null);
|
||||
};
|
||||
|
||||
export async function addPeerDataBatch(
|
||||
entries: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: Alias[];
|
||||
olmId?: string;
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedTargets = await resolveOlmTargets(entries);
|
||||
|
||||
if (resolvedTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloads = entries
|
||||
.map((entry) => {
|
||||
const resolved = resolvedTargets.find(
|
||||
(target) => target.clientId === entry.clientId
|
||||
);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: resolved.olmId,
|
||||
message: {
|
||||
type: `olm/wg/peer/data/add`,
|
||||
data: {
|
||||
siteId: entry.siteId,
|
||||
remoteSubnets: entry.remoteSubnets,
|
||||
aliases: entry.aliases
|
||||
}
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(resolved.version, "olm")
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null);
|
||||
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(payloads);
|
||||
}
|
||||
|
||||
export async function removePeerDataBatch(
|
||||
entries: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: Alias[];
|
||||
olmId?: string;
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedTargets = await resolveOlmTargets(entries);
|
||||
|
||||
if (resolvedTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloads = entries
|
||||
.map((entry) => {
|
||||
const resolved = resolvedTargets.find(
|
||||
(target) => target.clientId === entry.clientId
|
||||
);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: resolved.olmId,
|
||||
message: {
|
||||
type: `olm/wg/peer/data/remove`,
|
||||
data: {
|
||||
siteId: entry.siteId,
|
||||
remoteSubnets: entry.remoteSubnets,
|
||||
aliases: entry.aliases
|
||||
}
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(resolved.version, "olm")
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null);
|
||||
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(payloads);
|
||||
}
|
||||
|
||||
export async function updatePeerData(
|
||||
clientId: number,
|
||||
siteId: number,
|
||||
|
||||
@@ -635,7 +635,7 @@ export async function validateOidcCallback(
|
||||
}
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => {
|
||||
calculateUserClientsForOrgs(userId!).catch((err) => {
|
||||
logger.error(
|
||||
"Error calculating user clients after syncing orgs and roles for OIDC user",
|
||||
{ error: err }
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
verifyApiKey,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeyCanSetUserOrgRoles,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyTargetAccess,
|
||||
@@ -974,6 +973,13 @@ authenticated.get(
|
||||
idp.getIdp
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
||||
idp.deleteIdp
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { verifyPassword } from "@server/auth/password";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
// Stale-while-revalidate in-memory fallback for the releases API.
|
||||
|
||||
@@ -54,7 +54,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
||||
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
|
||||
}
|
||||
|
||||
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
|
||||
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 12) {
|
||||
logger.warn(
|
||||
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MessageHandler } from "@server/routers/ws";
|
||||
import logger from "@server/logger";
|
||||
import { Newt } from "@server/db";
|
||||
import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import cache from "#dynamic/lib/cache"; // not using regional here because we dont know where the site is
|
||||
|
||||
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db, Site } from "@server/db";
|
||||
import { newts, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function addPeer(
|
||||
@@ -36,10 +36,14 @@ export async function addPeer(
|
||||
newtId = newt.newtId;
|
||||
}
|
||||
|
||||
await sendToClient(newtId, {
|
||||
type: "newt/wg/peer/add",
|
||||
data: peer
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
await sendToClient(
|
||||
newtId,
|
||||
{
|
||||
type: "newt/wg/peer/add",
|
||||
data: peer
|
||||
},
|
||||
{ incrementConfigVersion: true }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
|
||||
@@ -76,12 +80,16 @@ export async function deletePeer(
|
||||
newtId = newt.newtId;
|
||||
}
|
||||
|
||||
await sendToClient(newtId, {
|
||||
type: "newt/wg/peer/remove",
|
||||
data: {
|
||||
publicKey
|
||||
}
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
await sendToClient(
|
||||
newtId,
|
||||
{
|
||||
type: "newt/wg/peer/remove",
|
||||
data: {
|
||||
publicKey
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: true }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
|
||||
@@ -90,6 +98,35 @@ export async function deletePeer(
|
||||
return site;
|
||||
}
|
||||
|
||||
export async function deletePeersBatch(
|
||||
peers: {
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
newtId: string;
|
||||
}[]
|
||||
) {
|
||||
if (peers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(
|
||||
peers.map((peer) => ({
|
||||
clientId: peer.newtId,
|
||||
message: {
|
||||
type: "newt/wg/peer/remove",
|
||||
data: {
|
||||
publicKey: peer.publicKey
|
||||
}
|
||||
},
|
||||
options: { incrementConfigVersion: true }
|
||||
}))
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending batched newt peer removals:`, error);
|
||||
});
|
||||
|
||||
logger.info(`Deleted ${peers.length} peer(s) from newts (batch)`);
|
||||
}
|
||||
|
||||
export async function updatePeer(
|
||||
siteId: number,
|
||||
publicKey: string,
|
||||
@@ -122,13 +159,17 @@ export async function updatePeer(
|
||||
newtId = newt.newtId;
|
||||
}
|
||||
|
||||
await sendToClient(newtId, {
|
||||
type: "newt/wg/peer/update",
|
||||
data: {
|
||||
publicKey,
|
||||
...peer
|
||||
}
|
||||
}, { incrementConfigVersion: true }).catch((error) => {
|
||||
await sendToClient(
|
||||
newtId,
|
||||
{
|
||||
type: "newt/wg/peer/update",
|
||||
data: {
|
||||
publicKey,
|
||||
...peer
|
||||
}
|
||||
},
|
||||
{ incrementConfigVersion: true }
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending message:`, error);
|
||||
});
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function createUserOlm(
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||
calculateUserClientsForOrgs(userId).catch((e) => {
|
||||
console.error(
|
||||
"Error calculating user clients after creating olm:",
|
||||
e
|
||||
|
||||
@@ -86,13 +86,11 @@ export async function deleteUserOlm(
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
}
|
||||
);
|
||||
rebuildClientAssociationsFromClient(deletedClient).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||
);
|
||||
});
|
||||
sendTerminateClient(
|
||||
deletedClient.clientId,
|
||||
OlmErrorCodes.TERMINATED_DELETED,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils";
|
||||
import { build } from "@server/build";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import cache from "#dynamic/lib/cache"; // not using regional here because we need this in the register message handler before we know where the client is
|
||||
|
||||
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
|
||||
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
|
||||
@@ -348,7 +348,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
||||
// the olm will pump the register so we can keep checking
|
||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
||||
if (now - (client.lastHolePunch || 0) > 12 && sitesCount > 0) {
|
||||
logger.warn(
|
||||
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
||||
{ orgId: client.orgId, clientId: client.clientId }
|
||||
|
||||
@@ -11,7 +11,7 @@ import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const querySchema = z.object({
|
||||
const querySchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
+149
-2
@@ -1,9 +1,9 @@
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||
import { clientSitesAssociationsCache, db, olms } from "@server/db";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { Alias } from "yaml";
|
||||
|
||||
export async function addPeer(
|
||||
@@ -205,3 +205,150 @@ export async function initPeerAddHandshake(
|
||||
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function deletePeersBatch(
|
||||
peers: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
olmId?: string;
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (peers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unresolvedClientIds = peers
|
||||
.filter((peer) => !peer.olmId)
|
||||
.map((peer) => peer.clientId);
|
||||
|
||||
const olmByClientId = new Map<
|
||||
number,
|
||||
{ olmId: string; version: string | null }
|
||||
>();
|
||||
|
||||
if (unresolvedClientIds.length > 0) {
|
||||
const olmRows = await db
|
||||
.select({
|
||||
clientId: olms.clientId,
|
||||
olmId: olms.olmId,
|
||||
version: olms.version
|
||||
})
|
||||
.from(olms)
|
||||
.where(inArray(olms.clientId, unresolvedClientIds));
|
||||
|
||||
for (const row of olmRows) {
|
||||
if (row.clientId !== null) {
|
||||
olmByClientId.set(row.clientId, {
|
||||
olmId: row.olmId,
|
||||
version: row.version
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const batchPayloads = peers
|
||||
.map((peer) => {
|
||||
const resolved = peer.olmId
|
||||
? { olmId: peer.olmId, version: peer.version ?? null }
|
||||
: olmByClientId.get(peer.clientId);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: resolved.olmId,
|
||||
message: {
|
||||
type: "olm/wg/peer/remove",
|
||||
data: {
|
||||
publicKey: peer.publicKey,
|
||||
siteId: peer.siteId
|
||||
}
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(
|
||||
peer.version ?? resolved.version,
|
||||
"olm"
|
||||
)
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((payload) => payload !== null);
|
||||
|
||||
if (batchPayloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(batchPayloads).catch((error) => {
|
||||
logger.warn(`Error sending batched olm peer removals:`, error);
|
||||
});
|
||||
|
||||
logger.info(`Deleted ${batchPayloads.length} peer(s) from olms (batch)`);
|
||||
}
|
||||
|
||||
export async function initPeerAddHandshakeBatch(
|
||||
handshakes: {
|
||||
clientId: number;
|
||||
peer: {
|
||||
siteId: number;
|
||||
exitNode: {
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
};
|
||||
};
|
||||
olmId: string;
|
||||
chainId?: string;
|
||||
}[]
|
||||
) {
|
||||
if (handshakes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(
|
||||
handshakes.map((item) => ({
|
||||
clientId: item.olmId,
|
||||
message: {
|
||||
type: "olm/wg/peer/holepunch/site/add",
|
||||
data: {
|
||||
siteId: item.peer.siteId,
|
||||
exitNode: {
|
||||
publicKey: item.peer.exitNode.publicKey,
|
||||
relayPort:
|
||||
config.getRawConfig().gerbil.clients_start_port,
|
||||
endpoint: item.peer.exitNode.endpoint
|
||||
},
|
||||
chainId: item.chainId
|
||||
}
|
||||
},
|
||||
options: { incrementConfigVersion: true }
|
||||
}))
|
||||
).catch((error) => {
|
||||
logger.warn(`Error sending batched olm handshakes:`, error);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
handshakes.map((item) =>
|
||||
db
|
||||
.update(clientSitesAssociationsCache)
|
||||
.set({ isJitMode: false })
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSitesAssociationsCache.clientId,
|
||||
item.clientId
|
||||
),
|
||||
eq(
|
||||
clientSitesAssociationsCache.siteId,
|
||||
item.peer.siteId
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Initiated ${handshakes.length} peer add handshake(s) to olms (batch)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, primaryDb } from "@server/db";
|
||||
import { and, count, eq } from "drizzle-orm";
|
||||
import {
|
||||
domains,
|
||||
@@ -233,6 +233,7 @@ export async function createOrg(
|
||||
let error = "";
|
||||
let org: Org | null = null;
|
||||
let numOrgs: number | null = null;
|
||||
let ownerUserId: string | null = null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const allDomains = await trx
|
||||
@@ -326,7 +327,6 @@ export async function createOrg(
|
||||
);
|
||||
}
|
||||
|
||||
let ownerUserId: string | null = null;
|
||||
if (req.user) {
|
||||
await trx.insert(userOrgs).values({
|
||||
userId: req.user!.userId,
|
||||
@@ -382,8 +382,6 @@ export async function createOrg(
|
||||
}))
|
||||
);
|
||||
|
||||
await calculateUserClientsForOrgs(ownerUserId, trx);
|
||||
|
||||
if (billingOrgIdForNewOrg) {
|
||||
const [numOrgsResult] = await trx
|
||||
.select({ count: count() })
|
||||
@@ -396,6 +394,14 @@ export async function createOrg(
|
||||
}
|
||||
});
|
||||
|
||||
if (ownerUserId) {
|
||||
calculateUserClientsForOrgs(ownerUserId).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to calculate user clients after creating org ${orgId} for user ${ownerUserId}: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { fromZodError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
const listOrgsSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -14,7 +14,7 @@ const listOrgsParamsSchema = z.object({
|
||||
userId: z.string()
|
||||
});
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
const listOrgsSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
const getResourceSchema = z.strictObject({
|
||||
resourceId: z
|
||||
@@ -151,13 +152,7 @@ export async function getResource(
|
||||
const policy = await queryInlinePolicy(
|
||||
resource.defaultResourcePolicyId!
|
||||
);
|
||||
returnData = {
|
||||
...returnData,
|
||||
sso: policy?.sso || null,
|
||||
emailWhitelistEnabled: policy?.emailWhitelistEnabled || null,
|
||||
applyRules: policy?.applyRules || null,
|
||||
skipToIdpId: policy?.idpId || null
|
||||
};
|
||||
returnData = applyInlinePolicyFields(returnData, policy);
|
||||
}
|
||||
|
||||
return response<GetResourceResponse>(res, {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { assertEquals } from "../../../test/assert";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
function runTests() {
|
||||
const resource = {
|
||||
resourceId: 1,
|
||||
name: "dashboard",
|
||||
sso: null,
|
||||
emailWhitelistEnabled: null,
|
||||
applyRules: null,
|
||||
skipToIdpId: null
|
||||
} as any;
|
||||
|
||||
const enabledPolicy = {
|
||||
sso: true,
|
||||
emailWhitelistEnabled: true,
|
||||
applyRules: true,
|
||||
idpId: 42
|
||||
};
|
||||
|
||||
const enabledResult = applyInlinePolicyFields(resource, enabledPolicy);
|
||||
assertEquals(enabledResult.sso, true, "sso should mirror policy true");
|
||||
assertEquals(
|
||||
enabledResult.emailWhitelistEnabled,
|
||||
true,
|
||||
"email whitelist should mirror policy true"
|
||||
);
|
||||
assertEquals(
|
||||
enabledResult.applyRules,
|
||||
true,
|
||||
"applyRules should mirror policy true"
|
||||
);
|
||||
assertEquals(
|
||||
enabledResult.skipToIdpId,
|
||||
42,
|
||||
"skipToIdpId should use policy idpId"
|
||||
);
|
||||
|
||||
const disabledPolicy = {
|
||||
sso: false,
|
||||
emailWhitelistEnabled: false,
|
||||
applyRules: false,
|
||||
idpId: null
|
||||
};
|
||||
|
||||
const disabledResult = applyInlinePolicyFields(resource, disabledPolicy);
|
||||
assertEquals(disabledResult.sso, false, "sso false must not become null");
|
||||
assertEquals(
|
||||
disabledResult.emailWhitelistEnabled,
|
||||
false,
|
||||
"email whitelist false must not become null"
|
||||
);
|
||||
assertEquals(
|
||||
disabledResult.applyRules,
|
||||
false,
|
||||
"applyRules false must not become null"
|
||||
);
|
||||
assertEquals(
|
||||
disabledResult.skipToIdpId,
|
||||
null,
|
||||
"missing idp should stay null"
|
||||
);
|
||||
|
||||
const missingPolicyResult = applyInlinePolicyFields(resource, null);
|
||||
assertEquals(
|
||||
missingPolicyResult.sso,
|
||||
null,
|
||||
"missing policy should return nullable resource fields"
|
||||
);
|
||||
|
||||
console.log("PASS: inline policy fields mirror policy values");
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Resource, ResourcePolicy } from "@server/db";
|
||||
|
||||
type InlinePolicyFields = Pick<
|
||||
ResourcePolicy,
|
||||
"sso" | "emailWhitelistEnabled" | "applyRules" | "idpId"
|
||||
>;
|
||||
|
||||
export function applyInlinePolicyFields<T extends Resource>(
|
||||
resource: T,
|
||||
policy: InlinePolicyFields | null | undefined
|
||||
): T {
|
||||
return {
|
||||
...resource,
|
||||
sso: policy?.sso ?? null,
|
||||
emailWhitelistEnabled: policy?.emailWhitelistEnabled ?? null,
|
||||
applyRules: policy?.applyRules ?? null,
|
||||
skipToIdpId: policy?.idpId ?? null
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const listResourceRulesParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const listResourceRulesSchema = z.object({
|
||||
const listResourceRulesSchema = z.strictObject({
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -48,7 +48,7 @@ const listResourcesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listResourcesSchema = z.object({
|
||||
const listResourcesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -15,8 +15,7 @@ import logger from "@server/logger";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { localCache } from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
|
||||
|
||||
@@ -33,7 +32,7 @@ const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserResourceAliasesQuerySchema = z.object({
|
||||
const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
@@ -153,7 +152,7 @@ export async function listUserResourceAliases(
|
||||
pageSize
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
localCache.get(cacheKey);
|
||||
await cache.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
@@ -211,7 +210,11 @@ export async function listUserResourceAliases(
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
await cache.set(
|
||||
cacheKey,
|
||||
data,
|
||||
USER_RESOURCE_ALIASES_CACHE_TTL_SEC
|
||||
);
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
success: true,
|
||||
@@ -256,7 +259,7 @@ export async function listUserResourceAliases(
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
await cache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
|
||||
@@ -47,6 +47,7 @@ import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { applyInlinePolicyFields } from "./inlinePolicyFields";
|
||||
|
||||
const updateResourceParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
@@ -682,6 +683,12 @@ async function updateHttpResource(
|
||||
.where(eq(resourcePolicies.resourcePolicyId, policyId));
|
||||
}
|
||||
|
||||
const [inlinePolicy] = await db
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(eq(resourcePolicies.resourcePolicyId, policyId))
|
||||
.limit(1);
|
||||
|
||||
const updatedResource = await db
|
||||
.update(resources)
|
||||
.set({ ...resourceOnlyData, headers })
|
||||
@@ -698,7 +705,7 @@ async function updateHttpResource(
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedResource[0],
|
||||
data: applyInlinePolicyFields(updatedResource[0], inlinePolicy),
|
||||
success: true,
|
||||
error: false,
|
||||
message: "HTTP resource updated successfully",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resourceRules, resources } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { resourcePolicyRules, resourceRules, resources } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -22,13 +22,20 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const resourceRuleMatchSchema = z.enum([
|
||||
"CIDR",
|
||||
"IP",
|
||||
"PATH",
|
||||
"COUNTRY",
|
||||
"ASN",
|
||||
"REGION"
|
||||
]);
|
||||
|
||||
// Define Zod schema for request body validation
|
||||
const updateResourceRuleSchema = z
|
||||
.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z
|
||||
.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"])
|
||||
.optional(),
|
||||
match: resourceRuleMatchSchema.optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
@@ -123,37 +130,102 @@ export async function updateResourceRule(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Cannot create rule for non-http resource"
|
||||
"Cannot update rule for non-http resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
const isInlinePolicy =
|
||||
resource.resourcePolicyId === null &&
|
||||
resource.defaultResourcePolicyId !== null;
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
let existingMatch:
|
||||
| "CIDR"
|
||||
| "IP"
|
||||
| "PATH"
|
||||
| "COUNTRY"
|
||||
| "ASN"
|
||||
| "REGION";
|
||||
|
||||
if (isInlinePolicy) {
|
||||
const policyId = resource.defaultResourcePolicyId!;
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourcePolicyRules)
|
||||
.where(eq(resourcePolicyRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRule.resourcePolicyId !== policyId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedExistingMatch = resourceRuleMatchSchema.safeParse(
|
||||
existingRule.match
|
||||
);
|
||||
if (!parsedExistingMatch.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Resource rule has invalid match type"
|
||||
)
|
||||
);
|
||||
}
|
||||
existingMatch = parsedExistingMatch.data;
|
||||
} else {
|
||||
// Verify that the rule exists and belongs to the specified resource
|
||||
const [existingRule] = await db
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRule) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource rule with ID ${ruleId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existingRule.resourceId !== resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedExistingMatch = resourceRuleMatchSchema.safeParse(
|
||||
existingRule.match
|
||||
);
|
||||
if (!parsedExistingMatch.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Resource rule has invalid match type"
|
||||
)
|
||||
);
|
||||
}
|
||||
existingMatch = parsedExistingMatch.data;
|
||||
}
|
||||
|
||||
if (existingRule.resourceId !== resourceId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`Resource rule ${ruleId} does not belong to resource ${resourceId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const match = updateData.match || existingRule.match;
|
||||
const match = updateData.match || existingMatch;
|
||||
const { value } = updateData;
|
||||
|
||||
if (value !== undefined) {
|
||||
@@ -197,11 +269,30 @@ export async function updateResourceRule(
|
||||
}
|
||||
|
||||
// Update the rule
|
||||
const [updatedRule] = await db
|
||||
.update(resourceRules)
|
||||
.set(updateData)
|
||||
.where(eq(resourceRules.ruleId, ruleId))
|
||||
.returning();
|
||||
const [updatedRule] = isInlinePolicy
|
||||
? await db
|
||||
.update(resourcePolicyRules)
|
||||
.set(updateData)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicyRules.ruleId, ruleId),
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resource.defaultResourcePolicyId!
|
||||
)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
: await db
|
||||
.update(resourceRules)
|
||||
.set(updateData)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceRules.ruleId, ruleId),
|
||||
eq(resourceRules.resourceId, resourceId)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: updatedRule,
|
||||
|
||||
@@ -15,7 +15,7 @@ const listRolesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listRolesSchema = z.object({
|
||||
const listRolesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
|
||||
@@ -3,7 +3,6 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { response as sendResponse } from "@server/lib/response";
|
||||
import config from "@server/lib/config";
|
||||
import { build } from "@server/build";
|
||||
import { APP_VERSION } from "@server/lib/consts";
|
||||
import license from "#dynamic/license/license";
|
||||
@@ -22,9 +21,6 @@ export async function getServerInfo(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const supporterData = config.getSupporterData();
|
||||
const supporterStatusValid = supporterData?.valid || false;
|
||||
|
||||
let enterpriseLicenseValid = false;
|
||||
let enterpriseLicenseType: string | null = null;
|
||||
|
||||
@@ -41,7 +37,7 @@ export async function getServerInfo(
|
||||
return sendResponse<GetServerInfoResponse>(res, {
|
||||
data: {
|
||||
version: APP_VERSION,
|
||||
supporterStatusValid,
|
||||
supporterStatusValid: true,
|
||||
build,
|
||||
enterpriseLicenseValid,
|
||||
enterpriseLicenseType
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {
|
||||
db,
|
||||
exitNodes,
|
||||
labels,
|
||||
newts,
|
||||
orgs,
|
||||
remoteExitNodes,
|
||||
roleSites,
|
||||
siteLabels,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
targets,
|
||||
sites,
|
||||
targets,
|
||||
userSites,
|
||||
labels,
|
||||
siteLabels,
|
||||
type Label
|
||||
} from "@server/db";
|
||||
import cache from "#dynamic/lib/cache";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -23,108 +24,15 @@ import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import semver from "semver";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
// Stale-while-revalidate: keeps the last successfully fetched version so that
|
||||
// a transient network failure / timeout does not flip every site back to
|
||||
// newtUpdateAvailable: false.
|
||||
let staleNewtVersion: string | null = null;
|
||||
|
||||
async function getLatestNewtVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = await cache.get<string>(
|
||||
"cache:latestNewtVersion"
|
||||
);
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/newt/tags",
|
||||
{
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Newt repository");
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
// Remove release-candidates, then sort descending by semver so that
|
||||
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
|
||||
// from the GitHub API do not cause an older tag to be selected.
|
||||
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
|
||||
tags.sort((a: any, b: any) => {
|
||||
const va = semver.coerce(a.name);
|
||||
const vb = semver.coerce(b.name);
|
||||
if (!va && !vb) return 0;
|
||||
if (!va) return 1;
|
||||
if (!vb) return -1;
|
||||
return semver.rcompare(va, vb);
|
||||
});
|
||||
|
||||
// Deduplicate: keep only the first (highest) entry per normalised version
|
||||
const seen = new Set<string>();
|
||||
tags = tags.filter((tag: any) => {
|
||||
const normalised = semver.coerce(tag.name)?.version;
|
||||
if (!normalised || seen.has(normalised)) return false;
|
||||
seen.add(normalised);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (tags.length === 0) {
|
||||
logger.warn("No valid semver tags found for Newt repository");
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
staleNewtVersion = latestVersion;
|
||||
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
logger.warn(
|
||||
"Request to fetch latest Newt version timed out (1.5s)"
|
||||
);
|
||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
logger.warn(
|
||||
"Connection timeout while fetching latest Newt version"
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Error fetching latest Newt version:",
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return staleNewtVersion;
|
||||
}
|
||||
}
|
||||
|
||||
const listSitesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listSitesSchema = z.object({
|
||||
const listSitesSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
@@ -449,9 +357,6 @@ export async function listSites(
|
||||
|
||||
const totalCount = Number(countRows[0]?.count ?? 0);
|
||||
|
||||
// Get latest version asynchronously without blocking the response
|
||||
const latestNewtVersionPromise = getLatestNewtVersion();
|
||||
|
||||
const siteIds = rows.map((site) => site.siteId);
|
||||
|
||||
let labelsForSites: Array<{
|
||||
@@ -494,36 +399,6 @@ export async function listSites(
|
||||
return { ...siteWithUpdate, labels: labelsForSite };
|
||||
});
|
||||
|
||||
// Try to get the latest version, but don't block if it fails
|
||||
try {
|
||||
const latestNewtVersion = await latestNewtVersionPromise;
|
||||
|
||||
if (latestNewtVersion) {
|
||||
sitesWithUpdates.forEach((site) => {
|
||||
if (
|
||||
site.type === "newt" &&
|
||||
site.newtVersion &&
|
||||
latestNewtVersion
|
||||
) {
|
||||
try {
|
||||
site.newtUpdateAvailable = semver.lt(
|
||||
site.newtVersion,
|
||||
latestNewtVersion
|
||||
);
|
||||
} catch (error) {
|
||||
site.newtUpdateAvailable = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't let it block the response
|
||||
logger.warn(
|
||||
"Failed to check for Newt updates, continuing without update info:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
const sitesPayload = sitesWithUpdates.map((site) =>
|
||||
site.type === "local" ? { ...site, online: undefined } : site
|
||||
);
|
||||
|
||||
@@ -148,13 +148,15 @@ export async function addClientToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(clientSiteResources).values({
|
||||
clientId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(clientSiteResources).values({
|
||||
clientId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -155,13 +155,15 @@ export async function addRoleToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(roleSiteResources).values({
|
||||
roleId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(roleSiteResources).values({
|
||||
roleId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -124,13 +124,15 @@ export async function addUserToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.insert(userSiteResources).values({
|
||||
userId,
|
||||
siteResourceId
|
||||
});
|
||||
await db.insert(userSiteResources).values({
|
||||
userId,
|
||||
siteResourceId
|
||||
});
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(siteResource, trx);
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -235,7 +235,7 @@ export async function batchAddClientToSiteResources(
|
||||
}
|
||||
});
|
||||
|
||||
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => {
|
||||
rebuildClientAssociationsFromClient(client).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
|
||||
);
|
||||
|
||||
@@ -625,15 +625,14 @@ export async function createSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
newSiteResource!,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
rebuildClientAssociationsFromSiteResource(newSiteResource!).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: newSiteResource,
|
||||
|
||||
@@ -88,15 +88,14 @@ export async function deleteSiteResource(
|
||||
// own transaction so it always executes on the primary — avoiding any
|
||||
// replica-lag issues while still allowing the HTTP response to return
|
||||
// early.
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
removedSiteResource,
|
||||
primaryDb
|
||||
).catch((err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
});
|
||||
rebuildClientAssociationsFromSiteResource(removedSiteResource).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Deleted site resource ${siteResourceId}`);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user