mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-14 14:54:49 +02:00
Compare commits
49 Commits
1.19.3
..
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 |
Vendored
+1
-4
@@ -18,8 +18,5 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"cSpell.words": [
|
||||
"nessicary"
|
||||
]
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Локална",
|
||||
"edit": "Редактиране",
|
||||
"siteConfirmDelete": "Потвърждение на изтриване на сайта",
|
||||
"siteConfirmDeleteAndResources": "Потвърдете изтриването на сайта и ресурсите",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Изтриване на сайта",
|
||||
"siteDeleteAndResources": "Изтриване на сайта и ресурсите",
|
||||
"siteMessageRemove": "След премахване, сайтът вече няма да бъде достъпен. Всички цели, свързани със сайта, също ще бъдат премахнати.",
|
||||
"siteMessageRemoveAndResources": "Това ще изтрие окончателно всички публични и частни ресурси, свързани с този сайт, дори ако ресурсът е асоцииран и с други сайтове.",
|
||||
"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": "Наистина ли желаете да изтриете този сайт и всички свързани ресурси?",
|
||||
"sitesTableDeleteSite": "Изтриване на сайта",
|
||||
"sitesTableDeleteSiteAndResources": "Изтриване на сайта и ресурсите",
|
||||
"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": "Свържете се с мрежа.",
|
||||
@@ -210,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": "Достъп до частни ресурси с нулево доверие.",
|
||||
@@ -1644,7 +1644,7 @@
|
||||
"alertingActionType": "Тип на действието",
|
||||
"alertingNotifyUsers": "Потребители",
|
||||
"alertingNotifyRoles": "Роли",
|
||||
"alertingNotifyEmails": "Имейл адреси",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Добавете имейл и натиснете Enter",
|
||||
"alertingWebhookMethod": "HTTP метод",
|
||||
"alertingWebhookSecret": "Секрет за подписване (по избор)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR диапазонът на ресурса в мрежата на сайта.",
|
||||
"createInternalResourceDialogAlias": "Псевдоним",
|
||||
"createInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
|
||||
"internalResourceAliasLocalWarning": "Синоними с окончание .local могат да причинят проблеми с резолюцията поради mDNS в някои мрежи.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Методът е задължителен за HTTP ресурси",
|
||||
"internalResourceHttpPortRequired": "Портът към целта е задължителен за HTTP ресурси",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2974,7 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "Липсва идентификатор на организация или домейн",
|
||||
"loadingDNSRecords": "Зареждане на DNS записи...",
|
||||
"olmUpdateAvailableInfo": "Налична е актуализирана версия на Olm. Моля, актуализирайте до най-новата версия за най-добро преживяване.",
|
||||
"updateAvailableInfo": "На разположение е обновена версия. Моля, обновете до най-новата версия за най-добър опит.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки на прокси протокол",
|
||||
"proxyProtocolDescription": "Конфигурирайте Proxy Protocol, за да запазите IP адресите на клиентите за TCP услуги.",
|
||||
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Místní",
|
||||
"edit": "Upravit",
|
||||
"siteConfirmDelete": "Potvrdit odstranění lokality",
|
||||
"siteConfirmDeleteAndResources": "Potvrdit odstranění lokality a zdrojů",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Odstranění lokality",
|
||||
"siteDeleteAndResources": "Odstranit lokalitu a zdroje",
|
||||
"siteMessageRemove": "Po odstranění webu již nebude přístupný. Všechny cíle spojené s webem budou také odstraněny.",
|
||||
"siteMessageRemoveAndResources": "Toto trvale odstraní všechny veřejné a soukromé zdroje spojené s touto lokalitou, i když je zdroj také přiřazen k jiným lokalitám.",
|
||||
"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": "Opravdu chcete odstranit tuto lokalitu a všechny přidružené zdroje?",
|
||||
"sitesTableDeleteSite": "Odstranění lokality",
|
||||
"sitesTableDeleteSiteAndResources": "Odstranit lokalitu a zdroje",
|
||||
"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íť",
|
||||
@@ -210,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",
|
||||
@@ -1644,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é)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
|
||||
"internalResourceAliasLocalWarning": "Aliasy končící na .local mohou způsobit problémy s vyřešením díky mDNS v některých sítích.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Je k dispozici aktualizovaná verze. Aktualizujte prosím na nejnovější verzi pro nejlepší zážitek.",
|
||||
"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.",
|
||||
|
||||
-3608
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Lokal",
|
||||
"edit": "Bearbeiten",
|
||||
"siteConfirmDelete": "Löschen des Standorts bestätigen",
|
||||
"siteConfirmDeleteAndResources": "Löschen von Standort und Ressourcen bestätigen",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Standort löschen",
|
||||
"siteDeleteAndResources": "Standort und Ressourcen löschen",
|
||||
"siteMessageRemove": "Sobald der Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle mit dem Standort verbundenen Ziele werden ebenfalls entfernt.",
|
||||
"siteMessageRemoveAndResources": "Dies wird dauerhaft alle öffentlichen und privaten Ressourcen, die mit diesem Standort verknüpft sind, löschen, selbst wenn eine Ressource auch mit anderen Standorten verbunden ist.",
|
||||
"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": "Sind Sie sicher, dass Sie diesen Standort und alle zugehörigen Ressourcen löschen möchten?",
|
||||
"sitesTableDeleteSite": "Standort löschen",
|
||||
"sitesTableDeleteSiteAndResources": "Standort und Ressourcen löschen",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +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": "Aliasse, die auf .local enden, können aufgrund von mDNS in einigen Netzwerken zu Auflösungsproblemen führen.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Eine aktualisierte Version ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
|
||||
"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.",
|
||||
|
||||
+5
-12
@@ -66,15 +66,9 @@
|
||||
"local": "Local",
|
||||
"edit": "Edit",
|
||||
"siteConfirmDelete": "Confirm Delete Site",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Delete Site",
|
||||
"siteDeleteAndResources": "Delete Site and Resources",
|
||||
"siteMessageRemove": "Once removed the site will no longer be accessible. Targets associated with this site will be removed, but resources will remain.",
|
||||
"siteMessageRemoveAndResources": "This will permanently delete all public and private resources linked to this site, even if a resource is also associated with other sites.",
|
||||
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||
"siteQuestionRemoveAndResources": "Are you sure you want to delete this site and all associated resources?",
|
||||
"sitesTableDeleteSite": "Delete Site",
|
||||
"sitesTableDeleteSiteAndResources": "Delete Site and Resources",
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
@@ -210,7 +204,7 @@
|
||||
"proxyResourceTitle": "Manage Public Resources",
|
||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||
"publicResourcesBannerTitle": "Web-based Public Access",
|
||||
"publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet through a web browser and include identity and context-aware access policies. Unlike private resources, they do not require client-side software.",
|
||||
"publicResourcesBannerDescription": "Public resources are HTTPS proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
|
||||
"clientResourceTitle": "Manage Private Resources",
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
@@ -1644,7 +1638,7 @@
|
||||
"alertingActionType": "Action type",
|
||||
"alertingNotifyUsers": "Users",
|
||||
"alertingNotifyRoles": "Roles",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingNotifyEmails": "Email addresses",
|
||||
"alertingEmailPlaceholder": "Add email and press Enter",
|
||||
"alertingWebhookMethod": "HTTP method",
|
||||
"alertingWebhookSecret": "Signing secret (optional)",
|
||||
@@ -2177,10 +2171,10 @@
|
||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||
"sshSudo": "Allow sudo",
|
||||
"sshSudoCommands": "Sudo Commands",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, one per line. Absolute paths must be used.",
|
||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo, separated by commas, spaces, or new lines. Absolute paths must be used.",
|
||||
"sshCreateHomeDir": "Create Home Directory",
|
||||
"sshUnixGroups": "Unix Groups",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, one per line.",
|
||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host, separated by commas, spaces, or new lines.",
|
||||
"roleTextFieldPlaceholder": "Enter values, or drop a .txt or .csv file",
|
||||
"roleTextImportTitle": "Import from File",
|
||||
"roleTextImportDescription": "Importing {fileName} into {fieldLabel}.",
|
||||
@@ -2556,7 +2550,6 @@
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"subnet": "Subnet",
|
||||
"utilitySubnet": "Utility Subnet",
|
||||
"subnetDescription": "The subnet for this organization's network configuration.",
|
||||
"customDomain": "Custom Domain",
|
||||
"authPage": "Authentication Pages",
|
||||
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Local",
|
||||
"edit": "Editar",
|
||||
"siteConfirmDelete": "Confirmar Borrar Sitio",
|
||||
"siteConfirmDeleteAndResources": "Confirmar eliminación del sitio y recursos",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Eliminar sitio",
|
||||
"siteDeleteAndResources": "Eliminar sitio y recursos",
|
||||
"siteMessageRemove": "Una vez eliminado, el sitio ya no será accesible. Todos los objetivos asociados con el sitio también serán eliminados.",
|
||||
"siteMessageRemoveAndResources": "Esto eliminará permanentemente todos los recursos públicos y privados vinculados a este sitio, incluso si un recurso también está asociado con otros sitios.",
|
||||
"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": "¿Está seguro de que desea eliminar este sitio y todos los recursos asociados?",
|
||||
"sitesTableDeleteSite": "Eliminar sitio",
|
||||
"sitesTableDeleteSiteAndResources": "Eliminar sitio y recursos",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +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": "Los alias que terminan en .local pueden causar problemas de resolución debido a mDNS en algunas redes.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Hay una versión actualizada disponible. 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Locale",
|
||||
"edit": "Modifier",
|
||||
"siteConfirmDelete": "Confirmer la suppression du nœud",
|
||||
"siteConfirmDeleteAndResources": "Confirmer la suppression du site et des ressources",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Supprimer le nœud",
|
||||
"siteDeleteAndResources": "Supprimer le site et les ressources",
|
||||
"siteMessageRemove": "Une fois supprimé, le nœud ne sera plus accessible. Toutes les cibles associées au nœud seront également supprimées.",
|
||||
"siteMessageRemoveAndResources": "Cela supprimera définitivement toutes les ressources publiques et privées liées à ce site, même si une ressource est également associée à d'autres sites.",
|
||||
"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": "Êtes-vous sûr de vouloir supprimer ce site et toutes les ressources associées?",
|
||||
"sitesTableDeleteSite": "Supprimer le site",
|
||||
"sitesTableDeleteSiteAndResources": "Supprimer le site et les ressources",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +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": "Les alias se terminant par .local peuvent causer des problèmes de résolution dus au mDNS sur certains réseaux.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Une version mise à jour est disponible. Veuillez mettre à jour vers la dernière version pour une 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Locale",
|
||||
"edit": "Modifica",
|
||||
"siteConfirmDelete": "Conferma Eliminazione Sito",
|
||||
"siteConfirmDeleteAndResources": "Conferma Eliminazione Sito e Risorse",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Elimina Sito",
|
||||
"siteDeleteAndResources": "Elimina Sito e Risorse",
|
||||
"siteMessageRemove": "Una volta rimosso il sito non sarà più accessibile. Tutti gli oggetti associati al sito verranno rimossi.",
|
||||
"siteMessageRemoveAndResources": "Questo eliminerà permanentemente tutte le risorse pubbliche e private collegate a questo sito, anche se una risorsa è anche associata ad altri siti.",
|
||||
"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": "Sei sicuro di voler eliminare questo sito e tutte le risorse associate?",
|
||||
"sitesTableDeleteSite": "Elimina Sito",
|
||||
"sitesTableDeleteSiteAndResources": "Elimina Sito e Risorse",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "La gamma CIDR della risorsa sulla rete del sito.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.",
|
||||
"internalResourceAliasLocalWarning": "Gli alias che terminano in .local possono causare problemi di risoluzione a causa di mDNS su alcune reti.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "È disponibile una versione aggiornata. 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "로컬",
|
||||
"edit": "편집",
|
||||
"siteConfirmDelete": "사이트 삭제 확인",
|
||||
"siteConfirmDeleteAndResources": "사이트 및 리소스 삭제 확인",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "사이트 삭제",
|
||||
"siteDeleteAndResources": "사이트 및 리소스 삭제",
|
||||
"siteMessageRemove": "삭제되면 사이트에 더 이상 액세스할 수 없습니다. 사이트와 연결된 모든 대상도 삭제됩니다.",
|
||||
"siteMessageRemoveAndResources": "이 사이트와 연결된 모든 공용 및 개인 리소스는 다른 사이트에도 연결되어 있더라도 영구적으로 삭제됩니다.",
|
||||
"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": "이 사이트와 모든 관련 리소스를 삭제하시겠습니까?",
|
||||
"sitesTableDeleteSite": "사이트 삭제",
|
||||
"sitesTableDeleteSiteAndResources": "사이트 및 리소스 삭제",
|
||||
"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": "모든 네트워크 연결",
|
||||
@@ -210,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": "제로 트러스트 개인 접근",
|
||||
@@ -1644,7 +1644,7 @@
|
||||
"alertingActionType": "작업 유형",
|
||||
"alertingNotifyUsers": "사용자들",
|
||||
"alertingNotifyRoles": "역할들",
|
||||
"alertingNotifyEmails": "이메일 주소들",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "이메일 추가 후 Enter 키를 누르세요",
|
||||
"alertingWebhookMethod": "HTTP 메소드",
|
||||
"alertingWebhookSecret": "서명 비밀 (선택 사항)",
|
||||
@@ -2177,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}에 가져오는 중",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||
"createInternalResourceDialogAlias": "별칭",
|
||||
"createInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
|
||||
"internalResourceAliasLocalWarning": ".local로 끝나는 별칭은 일부 네트워크에서 mDNS로 인해 해결 문제가 발생할 수 있습니다.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 리소스에 스킴이 필요합니다",
|
||||
"internalResourceHttpPortRequired": "HTTP 리소스에 목적지 포트가 필요합니다",
|
||||
"siteConfiguration": "설정",
|
||||
@@ -2974,7 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
|
||||
"loadingDNSRecords": "DNS 레코드를 로드하는 중...",
|
||||
"olmUpdateAvailableInfo": "올름의 새 버전이 이용 가능합니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"updateAvailableInfo": "업데이트된 버전이 있습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "클라이언트",
|
||||
"proxyProtocol": "프록시 프로토콜 설정",
|
||||
"proxyProtocolDescription": "TCP 서비스에 대한 클라이언트 IP 주소를 유지하도록 프록시 프로토콜을 구성하세요.",
|
||||
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Lokal",
|
||||
"edit": "Rediger",
|
||||
"siteConfirmDelete": "Bekreft Sletting av Område",
|
||||
"siteConfirmDeleteAndResources": "Bekreft sletting av nettsted og ressurser",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Slett Område",
|
||||
"siteDeleteAndResources": "Slett nettsted og ressurser",
|
||||
"siteMessageRemove": "Når nettstedet er fjernet, vil det ikke lenger være tilgjengelig. Alle målene for nettstedet vil også bli fjernet.",
|
||||
"siteMessageRemoveAndResources": "Dette vil permanent slette alle offentlige og private ressurser tilknyttet dette nettstedet, selv om en ressurs også er tilknyttet andre nettsteder.",
|
||||
"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": "Er du sikker på at du vil slette dette nettstedet og alle tilknyttede ressurser?",
|
||||
"sitesTableDeleteSite": "Slett nettsted",
|
||||
"sitesTableDeleteSiteAndResources": "Slett nettsted og ressurser",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
|
||||
"internalResourceAliasLocalWarning": "Alias som slutter på .local kan forårsake oppløsningsproblemer på grunn av mDNS på enkelte nettverk.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "En oppdatert versjon er tilgjengelig. Vennligst oppdater til den nyeste versjonen for 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Lokaal",
|
||||
"edit": "Bewerken",
|
||||
"siteConfirmDelete": "Verwijderen van site bevestigen",
|
||||
"siteConfirmDeleteAndResources": "Bevestig Verwijderen van Site en Bronnen",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Site verwijderen",
|
||||
"siteDeleteAndResources": "Site en Bronnen verwijderen",
|
||||
"siteMessageRemove": "Eenmaal verwijderd zal de site niet langer toegankelijk zijn. Alle aan de site gekoppelde doelen zullen ook worden verwijderd.",
|
||||
"siteMessageRemoveAndResources": "Dit zal permanent alle publieke en private resources gekoppeld aan deze site verwijderen, zelfs als een resource ook aan andere sites is gekoppeld.",
|
||||
"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": "Weet u zeker dat u deze site en alle gekoppelde resources wilt verwijderen?",
|
||||
"sitesTableDeleteSite": "Site verwijderen",
|
||||
"sitesTableDeleteSiteAndResources": "Site en Bronnen verwijderen",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +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": "Aliassen die eindigen op .local kunnen resolutieproblemen veroorzaken vanwege mDNS op sommige netwerken.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Er is een bijgewerkte versie beschikbaar. Update 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Lokalny",
|
||||
"edit": "Edytuj",
|
||||
"siteConfirmDelete": "Potwierdź usunięcie witryny",
|
||||
"siteConfirmDeleteAndResources": "Potwierdź usunięcie witryny i zasobów",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Usuń witrynę",
|
||||
"siteDeleteAndResources": "Usuń witrynę i zasoby",
|
||||
"siteMessageRemove": "Po usunięciu witryna nie będzie już dostępna. Wszystkie cele związane z witryną zostaną również usunięte.",
|
||||
"siteMessageRemoveAndResources": "To spowoduje trwałe usunięcie wszystkich zasobów publicznych i prywatnych powiązanych z tą witryną, nawet jeśli zasób jest także powiązany z innymi witrynami.",
|
||||
"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": "Czy na pewno chcesz usunąć tę witrynę i wszystkie powiązane zasoby?",
|
||||
"sitesTableDeleteSite": "Usuń witrynę",
|
||||
"sitesTableDeleteSiteAndResources": "Usuń witrynę i zasoby",
|
||||
"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ć",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
|
||||
"internalResourceAliasLocalWarning": "Alias kończący się na .local może powodować problemy z rozpoznawaniem z powodu mDNS w niektórych sieciach.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Dostępna jest zaktualizowana wersja. Zaktualizuj do najnowszej wersji, aby uzyskać najlepsze wrażenia z użytkowania.",
|
||||
"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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Localização",
|
||||
"edit": "Alterar",
|
||||
"siteConfirmDelete": "Confirmar que pretende apagar o site",
|
||||
"siteConfirmDeleteAndResources": "Confirmar Exclusão do Site e Recursos",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Excluir site",
|
||||
"siteDeleteAndResources": "Excluir Site e Recursos",
|
||||
"siteMessageRemove": "Uma vez removido, o site não estará mais acessível. Todas as metas associadas ao site também serão removidas.",
|
||||
"siteMessageRemoveAndResources": "Isso excluirá permanentemente todos os recursos públicos e privados vinculados a este site, mesmo que um recurso também esteja associado a outros sites.",
|
||||
"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": "Tem certeza de que deseja excluir este site e todos os recursos associados?",
|
||||
"sitesTableDeleteSite": "Excluir Site",
|
||||
"sitesTableDeleteSiteAndResources": "Excluir Site e Recursos",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +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": "Os aliases terminando em .local podem causar problemas de resolução devido ao mDNS em algumas redes.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "Uma versão atualizada está disponível. Por favor, atualize para a versão mais recente para uma 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
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Локальный",
|
||||
"edit": "Редактировать",
|
||||
"siteConfirmDelete": "Подтвердить удаление сайта",
|
||||
"siteConfirmDeleteAndResources": "Подтвердите удаление сайта и ресурсов",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Удалить сайт",
|
||||
"siteDeleteAndResources": "Удалить сайт и ресурсы",
|
||||
"siteMessageRemove": "После удаления сайт больше не будет доступен. Все цели, связанные с сайтом, также будут удалены.",
|
||||
"siteMessageRemoveAndResources": "Это навсегда удалит все общественные и частные ресурсы, связанные с этим сайтом, даже если ресурс также связан с другими сайтами.",
|
||||
"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": "Вы уверены, что хотите удалить этот сайт и все связанные с ним ресурсы?",
|
||||
"sitesTableDeleteSite": "Удалить сайт",
|
||||
"sitesTableDeleteSiteAndResources": "Удалить сайт и ресурсы",
|
||||
"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": "Подключить любую сеть",
|
||||
@@ -210,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": "Частный доступ с нулевым доверием",
|
||||
@@ -1644,7 +1644,7 @@
|
||||
"alertingActionType": "Тип действия",
|
||||
"alertingNotifyUsers": "Пользователи",
|
||||
"alertingNotifyRoles": "Роли",
|
||||
"alertingNotifyEmails": "Email адреса",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "Добавьте email и нажмите Enter",
|
||||
"alertingWebhookMethod": "HTTP метод",
|
||||
"alertingWebhookSecret": "Секрет подписания (необязательно)",
|
||||
@@ -2177,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}.",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
|
||||
"internalResourceAliasLocalWarning": "Псевдонимы, оканчивающиеся на .local, могут вызывать проблемы с разрешением из-за mDNS в некоторых сетях.",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "Схема обязательна для HTTP ресурсов",
|
||||
"internalResourceHttpPortRequired": "Порт назначения обязателен для HTTP ресурсов",
|
||||
"siteConfiguration": "Конфигурация",
|
||||
@@ -2974,7 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "Отсутствует организация или ID домена",
|
||||
"loadingDNSRecords": "Загрузка записей DNS...",
|
||||
"olmUpdateAvailableInfo": "Доступна обновленная версия Олма. Пожалуйста, обновитесь до последней версии.",
|
||||
"updateAvailableInfo": "Доступна обновленная версия. Пожалуйста, обновитесь до последней версии для получения лучшего опыта.",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "Клиент",
|
||||
"proxyProtocol": "Настройки протокола прокси",
|
||||
"proxyProtocolDescription": "Настроить Прокси-протокол для сохранения IP-адресов клиента для служб TCP.",
|
||||
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "Yerel",
|
||||
"edit": "Düzenle",
|
||||
"siteConfirmDelete": "Site Silmeyi Onayla",
|
||||
"siteConfirmDeleteAndResources": "Site ve Kaynakları Silmeyi Onayla",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "Siteyi Sil",
|
||||
"siteDeleteAndResources": "Site ve Kaynakları Sil",
|
||||
"siteMessageRemove": "Kaldırıldıktan sonra site artık erişilebilir olmayacaktır. Siteyle ilişkilendirilmiş tüm hedefler de kaldırılacaktır.",
|
||||
"siteMessageRemoveAndResources": "Bu işlem, diğer sitelerle de ilişkilendirilmiş olsa bile, bu siteye bağlı tüm genel ve özel kaynakları kalıcı olarak silecektir.",
|
||||
"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": "Bu siteyi ve tüm ilişkili kaynakları silmek istediğinizden emin misiniz?",
|
||||
"sitesTableDeleteSite": "Siteyi Sil",
|
||||
"sitesTableDeleteSiteAndResources": "Site ve Kaynakları Sil",
|
||||
"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",
|
||||
@@ -210,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",
|
||||
@@ -1644,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ı)",
|
||||
@@ -2177,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.",
|
||||
@@ -2344,7 +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": "Bazı ağlarda mDNS nedeniyle .local ile biten takma adlar çözümleme sorunlarına neden olabilir.",
|
||||
"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",
|
||||
@@ -2974,7 +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": "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.",
|
||||
|
||||
+13
-13
@@ -66,15 +66,15 @@
|
||||
"local": "本地的",
|
||||
"edit": "编辑",
|
||||
"siteConfirmDelete": "确认删除节点",
|
||||
"siteConfirmDeleteAndResources": "确认删除站点及资源",
|
||||
"siteConfirmDeleteAndResources": "Confirm Delete Site and Resources",
|
||||
"siteDelete": "删除节点",
|
||||
"siteDeleteAndResources": "删除站点及资源",
|
||||
"siteMessageRemove": "一旦移除,节点将无法访问。与节点相关的所有目标也将被移除。",
|
||||
"siteMessageRemoveAndResources": "这将永久删除与该站点关联的所有公共和私人资源,即使资源也与其他站点相关联。",
|
||||
"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": "您确定要删除此站点及所有关联资源吗?",
|
||||
"sitesTableDeleteSite": "删除站点",
|
||||
"sitesTableDeleteSiteAndResources": "删除站点及资源",
|
||||
"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": "连接任何网络",
|
||||
@@ -210,7 +210,7 @@
|
||||
"proxyResourceTitle": "管理公共资源",
|
||||
"proxyResourceDescription": "创建和管理可通过 Web 浏览器公开访问的资源",
|
||||
"publicResourcesBannerTitle": "基于 Web 的公共访问",
|
||||
"publicResourcesBannerDescription": "公共资源是 HTTPS 代理,可供互联网上的任何人通过 Web 浏览器访问。与私人资源不同,它们不需要客户端软件,并且可以包含身份和上下文感知的访问策略。",
|
||||
"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": "零信任私有访问",
|
||||
@@ -1644,7 +1644,7 @@
|
||||
"alertingActionType": "操作类型",
|
||||
"alertingNotifyUsers": "用户",
|
||||
"alertingNotifyRoles": "角色",
|
||||
"alertingNotifyEmails": "电子邮件地址",
|
||||
"alertingNotifyEmails": "Email Addresses",
|
||||
"alertingEmailPlaceholder": "添加电子邮件并按回车键",
|
||||
"alertingWebhookMethod": "HTTP 方法",
|
||||
"alertingWebhookSecret": "签名密钥(可选)",
|
||||
@@ -2177,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}。",
|
||||
@@ -2344,7 +2344,7 @@
|
||||
"createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
|
||||
"internalResourceAliasLocalWarning": "以 .local 结尾的别名可能会因某些网络上的 mDNS 而导致解析问题。",
|
||||
"internalResourceAliasLocalWarning": "Aliases ending in .local can cause resolution issues due to mDNS on some networks.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 资源需要方案",
|
||||
"internalResourceHttpPortRequired": "HTTP 资源需要目的端口",
|
||||
"siteConfiguration": "配置",
|
||||
@@ -2974,7 +2974,7 @@
|
||||
"orgOrDomainIdMissing": "缺少机构或域 ID",
|
||||
"loadingDNSRecords": "正在载入DNS记录...",
|
||||
"olmUpdateAvailableInfo": "有最新版本的 Olm 可用。请更新到最新版本以获取最佳体验。",
|
||||
"updateAvailableInfo": "有新版本可用。请更新到最新版本以获得最佳体验。",
|
||||
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||
"client": "客户端:",
|
||||
"proxyProtocol": "代理协议设置",
|
||||
"proxyProtocolDescription": "配置代理协议以保留TCP服务的客户端 IP 地址。",
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
users
|
||||
} from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
import type { RandomReader } from "@oslojs/crypto/random";
|
||||
import { generateRandomString } from "@oslojs/crypto/random";
|
||||
@@ -136,45 +136,6 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function invalidateAllSessionsExceptCurrent(
|
||||
userId: string,
|
||||
currentSessionId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
const userSessions = await trx
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
|
||||
if (userSessions.length > 0) {
|
||||
await trx.delete(resourceSessions).where(
|
||||
inArray(
|
||||
resourceSessions.userSessionId,
|
||||
userSessions.map((s) => s.sessionId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(sessions)
|
||||
.where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to invalidate user sessions except current", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeSessionCookie(
|
||||
token: string,
|
||||
isSecure: boolean,
|
||||
|
||||
@@ -795,13 +795,10 @@ export const COUNTRIES = [
|
||||
name: "Serbia",
|
||||
code: "RS"
|
||||
},
|
||||
// Removed as this is a deprecated ISO country code, not supported anymore
|
||||
// Also the individual flags for Serbia & Montenegro are already included in the list
|
||||
// more details: https://en.wikipedia.org/wiki/ISO_3166-2:CS
|
||||
// {
|
||||
// name: "Serbia and Montenegro",
|
||||
// code: "CS"
|
||||
// },
|
||||
{
|
||||
name: "Serbia and Montenegro",
|
||||
code: "CS"
|
||||
},
|
||||
{
|
||||
name: "Seychelles",
|
||||
code: "SC"
|
||||
|
||||
@@ -29,11 +29,8 @@ import { updateResourcePolicies } from "./resourcePolicies";
|
||||
import { BlueprintSource } from "@server/routers/blueprints/types";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
import { generateName } from "@server/db/names";
|
||||
import {
|
||||
handleMessagingForUpdatedSiteResource,
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
waitForSiteResourceRebuildIdle
|
||||
} from "../rebuildClientAssociations";
|
||||
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
|
||||
|
||||
type ApplyBlueprintArgs = {
|
||||
orgId: string;
|
||||
@@ -141,25 +138,26 @@ export async function applyBlueprint({
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
result.newSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((s) => s.siteId),
|
||||
result.newSites.map((s) => s.siteId)
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
db,
|
||||
newts,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
targets,
|
||||
type Resource,
|
||||
type Target,
|
||||
type TargetHealthCheck,
|
||||
type Transaction
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { removeTargets } from "@server/routers/newt/targets";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
export type DeleteResourceResult = {
|
||||
deletedResource: Resource;
|
||||
targetsToBeRemoved: Target[];
|
||||
healthChecksToBeRemoved: TargetHealthCheck[];
|
||||
};
|
||||
|
||||
export async function performDeleteResources(
|
||||
resourceIds: number[],
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<DeleteResourceResult[]> {
|
||||
if (resourceIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetsToBeRemoved = await trx
|
||||
.select()
|
||||
.from(targets)
|
||||
.where(inArray(targets.resourceId, resourceIds));
|
||||
|
||||
const targetIds = targetsToBeRemoved.map((t) => t.targetId);
|
||||
const healthChecksToBeRemoved =
|
||||
targetIds.length > 0
|
||||
? await trx
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.where(inArray(targetHealthCheck.targetId, targetIds))
|
||||
: [];
|
||||
|
||||
const deletedResources = await trx
|
||||
.delete(resources)
|
||||
.where(inArray(resources.resourceId, resourceIds))
|
||||
.returning();
|
||||
|
||||
const policyIds = deletedResources
|
||||
.map((resource) => resource.defaultResourcePolicyId)
|
||||
.filter((id): id is number => id != null);
|
||||
|
||||
if (policyIds.length > 0) {
|
||||
await trx
|
||||
.delete(resourcePolicies)
|
||||
.where(inArray(resourcePolicies.resourcePolicyId, policyIds));
|
||||
}
|
||||
|
||||
if (deletedResources.length > 0) {
|
||||
logger.debug(`Deleted ${deletedResources.length} resources`);
|
||||
}
|
||||
|
||||
const targetsByResourceId = new Map<number, Target[]>();
|
||||
for (const target of targetsToBeRemoved) {
|
||||
const existing = targetsByResourceId.get(target.resourceId) ?? [];
|
||||
existing.push(target);
|
||||
targetsByResourceId.set(target.resourceId, existing);
|
||||
}
|
||||
|
||||
const targetIdToResourceId = new Map(
|
||||
targetsToBeRemoved.map((target) => [target.targetId, target.resourceId])
|
||||
);
|
||||
|
||||
const healthChecksByResourceId = new Map<number, TargetHealthCheck[]>();
|
||||
for (const healthCheck of healthChecksToBeRemoved) {
|
||||
const resourceId = targetIdToResourceId.get(healthCheck.targetId!);
|
||||
if (resourceId == null) {
|
||||
continue;
|
||||
}
|
||||
const existing = healthChecksByResourceId.get(resourceId) ?? [];
|
||||
existing.push(healthCheck);
|
||||
healthChecksByResourceId.set(resourceId, existing);
|
||||
}
|
||||
|
||||
return deletedResources.map((deletedResource) => ({
|
||||
deletedResource,
|
||||
targetsToBeRemoved:
|
||||
targetsByResourceId.get(deletedResource.resourceId) ?? [],
|
||||
healthChecksToBeRemoved:
|
||||
healthChecksByResourceId.get(deletedResource.resourceId) ?? []
|
||||
}));
|
||||
}
|
||||
|
||||
export async function performDeleteResource(
|
||||
resourceId: number,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<DeleteResourceResult | null> {
|
||||
const [result] = await performDeleteResources([resourceId], trx);
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
export async function runResourceDeleteSideEffects(
|
||||
result: DeleteResourceResult
|
||||
): Promise<void> {
|
||||
const { deletedResource, targetsToBeRemoved, healthChecksToBeRemoved } =
|
||||
result;
|
||||
|
||||
for (const target of targetsToBeRemoved) {
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, target.siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
throw createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${target.siteId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
if (site.pubKey && site.type === "newt") {
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
if (newt) {
|
||||
await removeTargets(
|
||||
newt.newtId,
|
||||
[],
|
||||
healthChecksToBeRemoved,
|
||||
deletedResource.mode === "udp" ? "udp" : "tcp",
|
||||
newt.version
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import {
|
||||
db,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
targets,
|
||||
type SiteResource,
|
||||
type Transaction
|
||||
} from "@server/db";
|
||||
import {
|
||||
performDeleteResources,
|
||||
runResourceDeleteSideEffects,
|
||||
type DeleteResourceResult
|
||||
} from "@server/lib/deleteResource";
|
||||
import {
|
||||
performDeleteSiteResources,
|
||||
runSiteResourceDeleteSideEffects
|
||||
} from "@server/lib/deleteSiteResource";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE = 250;
|
||||
|
||||
export type DeleteSiteAssociatedResourcesSideEffects = {
|
||||
resources: DeleteResourceResult[];
|
||||
siteResources: SiteResource[];
|
||||
};
|
||||
|
||||
export async function getResourceIdsForSite(
|
||||
siteId: number,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<number[]> {
|
||||
const rows = await trx
|
||||
.selectDistinct({ resourceId: targets.resourceId })
|
||||
.from(targets)
|
||||
.where(eq(targets.siteId, siteId));
|
||||
|
||||
return rows.map((row) => row.resourceId);
|
||||
}
|
||||
|
||||
export async function getSiteResourceIdsForSite(
|
||||
siteId: number,
|
||||
orgId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<number[]> {
|
||||
const rows = await trx
|
||||
.selectDistinct({ siteResourceId: siteResources.siteResourceId })
|
||||
.from(siteNetworks)
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(siteResources.networkId, siteNetworks.networkId)
|
||||
)
|
||||
.where(
|
||||
and(eq(siteNetworks.siteId, siteId), eq(siteResources.orgId, orgId))
|
||||
);
|
||||
|
||||
return rows.map((row) => row.siteResourceId);
|
||||
}
|
||||
|
||||
export async function getAssociatedResourceCountForSite(
|
||||
siteId: number,
|
||||
orgId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<number> {
|
||||
const [publicCountResult, privateCountResult] = await Promise.all([
|
||||
trx
|
||||
.select({
|
||||
count: sql<number>`count(distinct ${targets.resourceId})`
|
||||
})
|
||||
.from(targets)
|
||||
.where(eq(targets.siteId, siteId)),
|
||||
trx
|
||||
.select({
|
||||
count: sql<number>`count(distinct ${siteResources.siteResourceId})`
|
||||
})
|
||||
.from(siteNetworks)
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(siteResources.networkId, siteNetworks.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
return (
|
||||
Number(publicCountResult[0]?.count ?? 0) +
|
||||
Number(privateCountResult[0]?.count ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function exceedsSiteAssociatedResourceDeleteLimit(
|
||||
resourceCount: number
|
||||
): boolean {
|
||||
return resourceCount > MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE;
|
||||
}
|
||||
|
||||
export async function deleteAssociatedResourcesForSite(
|
||||
siteId: number,
|
||||
orgId: string,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<DeleteSiteAssociatedResourcesSideEffects> {
|
||||
const resourceIds = await getResourceIdsForSite(siteId, trx);
|
||||
const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
|
||||
|
||||
const [resources, siteResourcesDeleted] = await Promise.all([
|
||||
performDeleteResources(resourceIds, trx),
|
||||
performDeleteSiteResources(siteResourceIds, trx)
|
||||
]);
|
||||
|
||||
return { resources, siteResources: siteResourcesDeleted };
|
||||
}
|
||||
|
||||
export async function runDeleteSiteAssociatedResourcesSideEffects(
|
||||
sideEffects: DeleteSiteAssociatedResourcesSideEffects
|
||||
): Promise<void> {
|
||||
for (const result of sideEffects.resources) {
|
||||
await runResourceDeleteSideEffects(result);
|
||||
}
|
||||
|
||||
for (const removed of sideEffects.siteResources) {
|
||||
runSiteResourceDeleteSideEffects(removed);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { inArray } from "drizzle-orm";
|
||||
import {
|
||||
db,
|
||||
siteResources,
|
||||
type SiteResource,
|
||||
type Transaction
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
export async function performDeleteSiteResources(
|
||||
siteResourceIds: number[],
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<SiteResource[]> {
|
||||
if (siteResourceIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const removedSiteResources = await trx
|
||||
.delete(siteResources)
|
||||
.where(inArray(siteResources.siteResourceId, siteResourceIds))
|
||||
.returning();
|
||||
|
||||
if (removedSiteResources.length > 0) {
|
||||
logger.debug(`Deleted ${removedSiteResources.length} site resources`);
|
||||
}
|
||||
|
||||
return removedSiteResources;
|
||||
}
|
||||
|
||||
export async function performDeleteSiteResource(
|
||||
siteResourceId: number,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<SiteResource | null> {
|
||||
const [removedSiteResource] = await performDeleteSiteResources(
|
||||
[siteResourceId],
|
||||
trx
|
||||
);
|
||||
return removedSiteResource ?? null;
|
||||
}
|
||||
|
||||
export function runSiteResourceDeleteSideEffects(
|
||||
removedSiteResource: SiteResource
|
||||
): void {
|
||||
rebuildClientAssociationsFromSiteResource(removedSiteResource).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
+7
-117
@@ -1,24 +1,4 @@
|
||||
const instanceId = `local-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
||||
|
||||
type LocalLockRecord = {
|
||||
owner: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const localLocks = new Map<string, LocalLockRecord>();
|
||||
|
||||
export class LockManager {
|
||||
private clearExpiredLocalLock(lockKey: string): void {
|
||||
const current = localLocks.get(lockKey);
|
||||
if (current && current.expiresAt <= Date.now()) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private getLocalOwnerToken(): string {
|
||||
return `${instanceId}:`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a distributed lock using Redis SET with NX and PX options
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
@@ -27,57 +7,22 @@ export class LockManager {
|
||||
*/
|
||||
async acquireLock(
|
||||
lockKey: string,
|
||||
ttlMs: number = 30000,
|
||||
maxRetries: number = 3,
|
||||
retryDelayMs: number = 100
|
||||
ttlMs: number = 30000
|
||||
): Promise<boolean> {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
|
||||
const existing = localLocks.get(lockKey);
|
||||
if (!existing) {
|
||||
localLocks.set(lockKey, {
|
||||
owner: this.getLocalOwnerToken(),
|
||||
expiresAt: Date.now() + ttlMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existing.owner === this.getLocalOwnerToken()) {
|
||||
existing.expiresAt = Date.now() + ttlMs;
|
||||
localLocks.set(lockKey, existing);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = retryDelayMs * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock using Lua script to ensure atomicity
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
*/
|
||||
async releaseLock(lockKey: string): Promise<void> {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (existing && existing.owner === this.getLocalOwnerToken()) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
async releaseLock(lockKey: string): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Force release a lock regardless of owner (use with caution)
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
*/
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Check if a lock exists and get its info
|
||||
@@ -90,20 +35,7 @@ export class LockManager {
|
||||
ttl: number;
|
||||
owner?: string;
|
||||
}> {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (!existing) {
|
||||
return { exists: false, ownedByMe: false, ttl: 0 };
|
||||
}
|
||||
|
||||
const ttl = Math.max(0, existing.expiresAt - Date.now());
|
||||
return {
|
||||
exists: true,
|
||||
ownedByMe: existing.owner === this.getLocalOwnerToken(),
|
||||
ttl,
|
||||
owner: existing.owner.split(":")[0]
|
||||
};
|
||||
return { exists: true, ownedByMe: true, ttl: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,15 +45,6 @@ export class LockManager {
|
||||
* @returns Promise<boolean> - true if extended successfully
|
||||
*/
|
||||
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (!existing || existing.owner !== this.getLocalOwnerToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
existing.expiresAt = Date.now() + ttlMs;
|
||||
localLocks.set(lockKey, existing);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,26 +62,7 @@ export class LockManager {
|
||||
maxRetries: number = 5,
|
||||
baseDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const acquired = await this.acquireLock(
|
||||
lockKey,
|
||||
ttlMs,
|
||||
1,
|
||||
baseDelayMs
|
||||
);
|
||||
|
||||
if (acquired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
const delay =
|
||||
baseDelayMs * Math.pow(2, attempt) + Math.random() * 100;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,21 +99,7 @@ export class LockManager {
|
||||
activeLocksCount: number;
|
||||
locksOwnedByMe: number;
|
||||
}> {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of localLocks.entries()) {
|
||||
if (value.expiresAt <= now) {
|
||||
localLocks.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
let locksOwnedByMe = 0;
|
||||
for (const value of localLocks.values()) {
|
||||
if (value.owner === this.getLocalOwnerToken()) {
|
||||
locksOwnedByMe++;
|
||||
}
|
||||
}
|
||||
|
||||
return { activeLocksCount: localLocks.size, locksOwnedByMe };
|
||||
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,13 +35,10 @@ import {
|
||||
parseEndpoint
|
||||
} from "@server/lib/ip";
|
||||
import {
|
||||
addPeerData,
|
||||
addPeerDataBatch,
|
||||
addTargetsBatch as addSubnetProxyTargetsBatch,
|
||||
removePeerDataBatch,
|
||||
removeTargetsBatch as removeSubnetProxyTargetsBatch,
|
||||
updatePeerDataBatch,
|
||||
updateTargets
|
||||
removeTargetsBatch as removeSubnetProxyTargetsBatch
|
||||
} from "@server/routers/client/targets";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
||||
@@ -50,112 +47,6 @@ import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
||||
// peer/proxy updates, so give them a generous window.
|
||||
const REBUILD_ASSOCIATIONS_LOCK_TTL_MS = 120000;
|
||||
|
||||
const REBUILD_IDLE_POLL_INTERVAL_MS = 300;
|
||||
const REBUILD_IDLE_DEFAULT_TIMEOUT_MS = 130_000; // slightly longer than lock TTL
|
||||
const REBUILD_IDLE_HANDLER_TIMEOUT_MS = 5_000;
|
||||
|
||||
/**
|
||||
* Returns true if a rebuild for the given site resource is currently active
|
||||
* (holding the distributed lock) or is pending in the rebuild queue.
|
||||
*/
|
||||
export async function hasActiveSiteResourceRebuild(
|
||||
siteResourceId: number
|
||||
): Promise<boolean> {
|
||||
const lockKey = `rebuild-client-associations:site-resource:${siteResourceId}`;
|
||||
const lockInfo = await lockManager.getLockInfo(lockKey);
|
||||
if (lockInfo.exists) return true;
|
||||
return rebuildQueue.isQueued({ type: "site-resource", id: siteResourceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once there is no active or queued rebuild for the given site resource.
|
||||
* Logs a warning and resolves early if the timeout is reached.
|
||||
*/
|
||||
export async function waitForSiteResourceRebuildIdle(
|
||||
siteResourceId: number,
|
||||
timeoutMs = REBUILD_IDLE_DEFAULT_TIMEOUT_MS
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (!(await hasActiveSiteResourceRebuild(siteResourceId))) return;
|
||||
await new Promise<void>((r) =>
|
||||
setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
|
||||
);
|
||||
}
|
||||
logger.warn(
|
||||
`waitForSiteResourceRebuildIdle: timed out after ${timeoutMs}ms waiting for siteResourceId=${siteResourceId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once there are no active or queued rebuilds for any site resource
|
||||
* associated with the given site.
|
||||
*/
|
||||
export async function waitForSiteRebuildIdle(
|
||||
siteId: number,
|
||||
timeoutMs = REBUILD_IDLE_HANDLER_TIMEOUT_MS
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const resourceRows = await db
|
||||
.select({ siteResourceId: siteResources.siteResourceId })
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(eq(siteNetworks.siteId, siteId));
|
||||
let allIdle = true;
|
||||
for (const { siteResourceId } of resourceRows) {
|
||||
if (await hasActiveSiteResourceRebuild(siteResourceId)) {
|
||||
allIdle = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allIdle) return;
|
||||
await new Promise<void>((r) =>
|
||||
setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
|
||||
);
|
||||
}
|
||||
logger.warn(
|
||||
`waitForSiteRebuildIdle: timed out after ${timeoutMs}ms waiting for siteId=${siteId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once there are no active or queued rebuilds for any site resource
|
||||
* associated with the given client.
|
||||
*/
|
||||
export async function waitForClientRebuildIdle(
|
||||
clientId: number,
|
||||
timeoutMs = REBUILD_IDLE_HANDLER_TIMEOUT_MS
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const resourceRows = await db
|
||||
.select({
|
||||
siteResourceId:
|
||||
clientSiteResourcesAssociationsCache.siteResourceId
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.where(eq(clientSiteResourcesAssociationsCache.clientId, clientId));
|
||||
let allIdle = true;
|
||||
for (const { siteResourceId } of resourceRows) {
|
||||
if (await hasActiveSiteResourceRebuild(siteResourceId)) {
|
||||
allIdle = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allIdle) return;
|
||||
await new Promise<void>((r) =>
|
||||
setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
|
||||
);
|
||||
}
|
||||
logger.warn(
|
||||
`waitForClientRebuildIdle: timed out after ${timeoutMs}ms waiting for clientId=${clientId}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getClientSiteResourceAccess(
|
||||
siteResource: SiteResource,
|
||||
trx: Transaction | typeof db = db
|
||||
@@ -271,10 +162,15 @@ export async function getClientSiteResourceAccess(
|
||||
export async function rebuildClientAssociationsFromSiteResource(
|
||||
siteResource: SiteResource
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource),
|
||||
() =>
|
||||
rebuildClientAssociationsFromSiteResourceImpl(
|
||||
siteResource,
|
||||
trx
|
||||
),
|
||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||
);
|
||||
} catch (err: any) {
|
||||
@@ -296,10 +192,15 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
}
|
||||
|
||||
async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
siteResource: SiteResource
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
|
||||
siteResource: SiteResource,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<{
|
||||
mergedAllClients: {
|
||||
clientId: number;
|
||||
pubKey: string | null;
|
||||
subnet: string | null;
|
||||
}[];
|
||||
}> {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] START siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} orgId=${siteResource.orgId}`
|
||||
);
|
||||
@@ -313,62 +214,14 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
|
||||
/////////// process the client-siteResource associations ///////////
|
||||
|
||||
const existingClientSiteResources = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
);
|
||||
|
||||
const existingClientSiteResourceIds = existingClientSiteResources.map(
|
||||
(row) => row.clientId
|
||||
);
|
||||
|
||||
// get all of the clients associated with other site resources that share
|
||||
// any of the same sites as this site resource (via siteNetworks). We can't
|
||||
// simply filter by networkId since each site resource has its own network;
|
||||
// two site resources serving the same site typically belong to different
|
||||
// networks that both happen to include the site through siteNetworks.
|
||||
const sitesListSiteIds = sitesList.map((s) => s.siteId);
|
||||
|
||||
// We must also consider sites where these clients are currently cached,
|
||||
// otherwise removing a site from this resource can leave stale
|
||||
// client-site cache entries behind for the removed site.
|
||||
const cachedSiteRowsForResourceClients =
|
||||
existingClientSiteResourceIds.length > 0
|
||||
? await trx
|
||||
.select({ siteId: clientSitesAssociationsCache.siteId })
|
||||
.from(clientSitesAssociationsCache)
|
||||
.where(
|
||||
inArray(
|
||||
clientSitesAssociationsCache.clientId,
|
||||
existingClientSiteResourceIds
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const allCandidateSiteIds = Array.from(
|
||||
new Set([
|
||||
...sitesListSiteIds,
|
||||
...cachedSiteRowsForResourceClients.map((r) => r.siteId)
|
||||
])
|
||||
);
|
||||
|
||||
const sitesToProcess =
|
||||
allCandidateSiteIds.length > 0
|
||||
? await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(inArray(sites.siteId, allCandidateSiteIds))
|
||||
: [];
|
||||
const currentSiteIdSet = new Set(sitesListSiteIds);
|
||||
const allUpdatedClientsFromOtherResourcesOnThisSite =
|
||||
allCandidateSiteIds.length > 0
|
||||
sitesListSiteIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
@@ -388,7 +241,7 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(siteNetworks.siteId, allCandidateSiteIds),
|
||||
inArray(siteNetworks.siteId, sitesListSiteIds),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
@@ -407,6 +260,22 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
clientsFromOtherResourcesBySite.get(row.siteId)!.add(row.clientId);
|
||||
}
|
||||
|
||||
const existingClientSiteResources = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
);
|
||||
|
||||
const existingClientSiteResourceIds = existingClientSiteResources.map(
|
||||
(row) => row.clientId
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} existingResourceClientIds=[${existingClientSiteResourceIds.join(", ")}]`
|
||||
);
|
||||
@@ -489,10 +358,10 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
/////////// process the client-site associations ///////////
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} beginning client-site association loop over ${sitesToProcess.length} site(s) (current=${sitesList.length})`
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} beginning client-site association loop over ${sitesList.length} site(s)`
|
||||
);
|
||||
|
||||
for (const site of sitesToProcess) {
|
||||
for (const site of sitesList) {
|
||||
const siteId = site.siteId;
|
||||
|
||||
logger.debug(
|
||||
@@ -534,13 +403,7 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
|
||||
);
|
||||
|
||||
// Expected clients from this resource are site-scoped: if this site is
|
||||
// no longer attached to the resource, the expected set is empty.
|
||||
const expectedClientIdsForSite = currentSiteIdSet.has(siteId)
|
||||
? mergedAllClientIds
|
||||
: [];
|
||||
|
||||
const clientSitesToAdd = expectedClientIdsForSite.filter(
|
||||
const clientSitesToAdd = mergedAllClientIds.filter(
|
||||
(clientId) =>
|
||||
!existingClientSiteIds.includes(clientId) &&
|
||||
!otherResourceClientIds.has(clientId) // dont add if already connected via another site resource
|
||||
@@ -575,7 +438,7 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
// Now remove any client-site associations that should no longer exist
|
||||
const clientSitesToRemove = existingClientSiteIds.filter(
|
||||
(clientId) =>
|
||||
!expectedClientIdsForSite.includes(clientId) &&
|
||||
!mergedAllClientIds.includes(clientId) &&
|
||||
!otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
|
||||
);
|
||||
|
||||
@@ -622,6 +485,10 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||
clientSiteResourcesToRemove,
|
||||
trx
|
||||
);
|
||||
|
||||
return {
|
||||
mergedAllClients
|
||||
};
|
||||
}
|
||||
|
||||
async function handleMessagesForSiteClients(
|
||||
@@ -1175,466 +1042,6 @@ async function handleSubnetProxyTargetUpdates(
|
||||
await Promise.all([...proxyJobs, ...olmJobs]);
|
||||
}
|
||||
|
||||
export async function handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource: SiteResource | undefined,
|
||||
updatedSiteResource: SiteResource,
|
||||
existingSiteIds: number[],
|
||||
updatedSiteIds: number[]
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: START siteResourceId=${updatedSiteResource.siteResourceId} existingSiteIds=[${existingSiteIds.join(", ")}] updatedSiteIds=[${updatedSiteIds.join(", ")}]`
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
|
||||
existingSiteResource
|
||||
);
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: updatedSiteResource is: ",
|
||||
updatedSiteResource
|
||||
);
|
||||
|
||||
const allSiteIds = [...new Set([...existingSiteIds, ...updatedSiteIds])];
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: allSiteIds=[${allSiteIds.join(", ")}] count=${allSiteIds.length}`
|
||||
);
|
||||
|
||||
const newtsForSites =
|
||||
allSiteIds.length > 0
|
||||
? await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(inArray(newts.siteId, allSiteIds))
|
||||
: [];
|
||||
const newtBySiteId = new Map(
|
||||
newtsForSites.map((newt) => [newt.siteId, newt])
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: fetched newts for ${newtsForSites.length}/${allSiteIds.length} site(s)`
|
||||
);
|
||||
|
||||
// WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED
|
||||
const mergedAllClients = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.innerJoin(
|
||||
clients,
|
||||
eq(clientSiteResourcesAssociationsCache.clientId, clients.clientId)
|
||||
)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
updatedSiteResource.siteResourceId
|
||||
)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: resolved merged clients count=${mergedAllClients.length} clientIds=[${mergedAllClients.map((c) => c.clientId).join(", ")}]`
|
||||
);
|
||||
|
||||
const targets = await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: generated updated targets count=${targets ? targets.length : 0}`
|
||||
);
|
||||
|
||||
const oldDestinationStillInUseClientSitePairs = new Set<string>();
|
||||
if (
|
||||
existingSiteResource?.destination &&
|
||||
allSiteIds.length > 0 &&
|
||||
mergedAllClients.length > 0
|
||||
) {
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: checking old destination reuse destination=${existingSiteResource.destination} across siteCount=${allSiteIds.length} clientCount=${mergedAllClients.length}`
|
||||
);
|
||||
|
||||
// we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
|
||||
const oldDestinationStillInUseRows = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
mergedAllClients.map((c) => c.clientId)
|
||||
),
|
||||
inArray(siteNetworks.siteId, allSiteIds),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
existingSiteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
existingSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
for (const row of oldDestinationStillInUseRows) {
|
||||
oldDestinationStillInUseClientSitePairs.add(
|
||||
`${row.clientId}:${row.siteId}`
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: old destination still in use rows=${oldDestinationStillInUseRows.length} uniqueClientSitePairs=${oldDestinationStillInUseClientSitePairs.size}`
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: skipping old destination reuse check (missing existing destination or no sites/clients)"
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////// FROM HERE DOWN WE ARE DEALING WITH REMOVING SITES
|
||||
const removedSiteIds = existingSiteIds.filter(
|
||||
(id) => !updatedSiteIds.includes(id)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: removing sites removedSiteIds=[${removedSiteIds.join(", ")}] count=${removedSiteIds.length}`
|
||||
);
|
||||
|
||||
const targetsToRemoveBatch: {
|
||||
newtId: string;
|
||||
targets: any[];
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
const peerDataRemoves: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: ReturnType<typeof generateAliasConfig>;
|
||||
}[] = [];
|
||||
if (targets) {
|
||||
for (const siteId of removedSiteIds) {
|
||||
const newt = newtBySiteId.get(siteId);
|
||||
if (!newt) {
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: skipping remove for siteId=${siteId} because no newt found`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: preparing remove batches for siteId=${siteId} newtId=${newt.newtId}`
|
||||
);
|
||||
|
||||
targetsToRemoveBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets: targets,
|
||||
version: newt.version
|
||||
});
|
||||
for (const client of mergedAllClients) {
|
||||
// we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
|
||||
const oldDestinationStillInUseBySite =
|
||||
oldDestinationStillInUseClientSitePairs.has(
|
||||
`${client.clientId}:${siteId}`
|
||||
);
|
||||
|
||||
if (existingSiteResource) {
|
||||
peerDataRemoves.push({
|
||||
// this might happen twice after the rebuild function but that is okay
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: !oldDestinationStillInUseBySite
|
||||
? generateRemoteSubnets([existingSiteResource])
|
||||
: [],
|
||||
aliases: generateAliasConfig([existingSiteResource])
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: skipping removal batch generation because targets were empty"
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: remove batches prepared targetBatchCount=${targetsToRemoveBatch.length} peerDataCount=${peerDataRemoves.length}`
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: dispatching removeSubnetProxyTargetsBatch"
|
||||
);
|
||||
|
||||
removeSubnetProxyTargetsBatch(targetsToRemoveBatch);
|
||||
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: dispatching removePeerDataBatch"
|
||||
);
|
||||
|
||||
removePeerDataBatch(peerDataRemoves);
|
||||
|
||||
//////////////////////////// FROM HERE DOWN WE ARE DEALING WITH ADDING NEW SITES
|
||||
const addedSiteIds = updatedSiteIds.filter(
|
||||
(id) => !existingSiteIds.includes(id)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: adding sites addedSiteIds=[${addedSiteIds.join(", ")}] count=${addedSiteIds.length}`
|
||||
);
|
||||
|
||||
const targetsToAddBatch: {
|
||||
newtId: string;
|
||||
targets: any[];
|
||||
version: string | null;
|
||||
}[] = [];
|
||||
const peerDataAdds: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets: string[];
|
||||
aliases: ReturnType<typeof generateAliasConfig>;
|
||||
}[] = [];
|
||||
if (targets) {
|
||||
for (const siteId of addedSiteIds) {
|
||||
const newt = newtBySiteId.get(siteId);
|
||||
if (!newt) {
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: skipping add for siteId=${siteId} because no newt found`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: preparing add batches for siteId=${siteId} newtId=${newt.newtId}`
|
||||
);
|
||||
|
||||
targetsToAddBatch.push({
|
||||
newtId: newt.newtId,
|
||||
targets: targets,
|
||||
version: newt.version
|
||||
});
|
||||
for (const client of mergedAllClients) {
|
||||
peerDataAdds.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: generateRemoteSubnets([updatedSiteResource]),
|
||||
aliases: generateAliasConfig([updatedSiteResource])
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: skipping add batch generation because targets were empty"
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: add batches prepared targetBatchCount=${targetsToAddBatch.length} peerDataCount=${peerDataAdds.length}`
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: dispatching addSubnetProxyTargetsBatch"
|
||||
);
|
||||
|
||||
addSubnetProxyTargetsBatch(targetsToAddBatch);
|
||||
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: dispatching addPeerDataBatch"
|
||||
);
|
||||
|
||||
addPeerDataBatch(peerDataAdds);
|
||||
|
||||
//////////////////////////// FROM HERE DOWN WE ARE DEALING WITH UPDATING THE EXISTING SITES
|
||||
|
||||
const unchangedSiteIds = existingSiteIds.filter((id) =>
|
||||
updatedSiteIds.includes(id)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: unchangedSiteIds=[${unchangedSiteIds.join(", ")}] count=${unchangedSiteIds.length}`
|
||||
);
|
||||
|
||||
// after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
|
||||
const destinationChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.destination !== updatedSiteResource.destination;
|
||||
const destinationPortChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.destinationPort !==
|
||||
updatedSiteResource.destinationPort;
|
||||
const aliasChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.alias !== updatedSiteResource.alias;
|
||||
const fullDomainChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.fullDomain !== updatedSiteResource.fullDomain;
|
||||
const sslChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.ssl !== updatedSiteResource.ssl;
|
||||
const portRangesChanged =
|
||||
existingSiteResource &&
|
||||
(existingSiteResource.tcpPortRangeString !==
|
||||
updatedSiteResource.tcpPortRangeString ||
|
||||
existingSiteResource.udpPortRangeString !==
|
||||
updatedSiteResource.udpPortRangeString ||
|
||||
existingSiteResource.disableIcmp !==
|
||||
updatedSiteResource.disableIcmp);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)}`
|
||||
);
|
||||
|
||||
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
|
||||
|
||||
if (
|
||||
destinationChanged ||
|
||||
aliasChanged ||
|
||||
fullDomainChanged ||
|
||||
sslChanged ||
|
||||
portRangesChanged ||
|
||||
destinationPortChanged
|
||||
) {
|
||||
const shouldUpdateTargets =
|
||||
destinationChanged ||
|
||||
sslChanged ||
|
||||
portRangesChanged ||
|
||||
fullDomainChanged ||
|
||||
destinationPortChanged;
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: entering unchanged-site update path shouldUpdateTargets=${shouldUpdateTargets}`
|
||||
);
|
||||
|
||||
const oldTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
const newTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: target update payload sizes oldTargets=${oldTargets ? oldTargets.length : 0} newTargets=${newTargets ? newTargets.length : 0}`
|
||||
);
|
||||
|
||||
const peerDataUpdateBatch: Parameters<typeof updatePeerDataBatch>[0] =
|
||||
[];
|
||||
|
||||
for (const siteId of unchangedSiteIds) {
|
||||
const newt = newtBySiteId.get(siteId);
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: processing unchanged siteId=${siteId}`
|
||||
);
|
||||
|
||||
if (!newt) {
|
||||
logger.error(
|
||||
`handleMessagingForUpdatedSiteResource: missing newt for unchanged siteId=${siteId}`
|
||||
);
|
||||
throw new Error(
|
||||
"Newt not found for site during site resource update"
|
||||
);
|
||||
}
|
||||
|
||||
// Only update targets on newt if these items change
|
||||
if (shouldUpdateTargets) {
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: updating targets for siteId=${siteId} newtId=${newt.newtId}`
|
||||
);
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{
|
||||
oldTargets: oldTargets ? oldTargets : [],
|
||||
newTargets: newTargets ? newTargets : []
|
||||
},
|
||||
newt.version
|
||||
);
|
||||
}
|
||||
|
||||
for (const client of mergedAllClients) {
|
||||
// does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet
|
||||
if (!existingSiteResource.destination) {
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: skipping peerData update for clientId=${client.clientId} siteId=${siteId} because existing destination is empty`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// we need to do this because the client only knows about peers not resources so we need to make sure that we dont remove it if there is still a another resource
|
||||
const oldDestinationStillInUseBySite =
|
||||
oldDestinationStillInUseClientSitePairs.has(
|
||||
`${client.clientId}:${siteId}`
|
||||
);
|
||||
|
||||
// we also need to update the remote subnets on the olms for each client that has access to this site
|
||||
peerDataUpdateBatch.push({
|
||||
clientId: client.clientId,
|
||||
siteId,
|
||||
remoteSubnets: destinationChanged
|
||||
? {
|
||||
oldRemoteSubnets: !oldDestinationStillInUseBySite
|
||||
? generateRemoteSubnets([
|
||||
existingSiteResource
|
||||
])
|
||||
: [],
|
||||
newRemoteSubnets: generateRemoteSubnets([
|
||||
updatedSiteResource
|
||||
])
|
||||
}
|
||||
: undefined,
|
||||
aliases:
|
||||
aliasChanged || fullDomainChanged // the full domain is sent down as an alias
|
||||
? {
|
||||
oldAliases: generateAliasConfig([
|
||||
existingSiteResource
|
||||
]),
|
||||
newAliases: generateAliasConfig([
|
||||
updatedSiteResource
|
||||
])
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: dispatching updatePeerDataBatch count=${peerDataUpdateBatch.length}`
|
||||
);
|
||||
|
||||
updatePeerDataBatch(peerDataUpdateBatch);
|
||||
} else {
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: no unchanged-site update required because no relevant fields changed"
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function rebuildClientAssociationsFromClient(
|
||||
client: Client
|
||||
): Promise<void> {
|
||||
@@ -2369,7 +1776,7 @@ async function handleMessagesForClientResources(
|
||||
)
|
||||
);
|
||||
|
||||
// Only remove remote subnet if no other resource uses the same destination on the same site
|
||||
// Only remove remote subnet if no other resource uses the same destination
|
||||
const remoteSubnetsToRemove =
|
||||
destinationStillInUse.length > 0
|
||||
? []
|
||||
|
||||
@@ -13,15 +13,11 @@ export interface RebuildJobHandlers {
|
||||
export interface RebuildQueueManager {
|
||||
enqueue(job: RebuildJob): Promise<void>;
|
||||
startProcessing(handlers: RebuildJobHandlers): void;
|
||||
isQueued(job: RebuildJob): Promise<boolean>;
|
||||
}
|
||||
|
||||
class NoopRebuildQueue implements RebuildQueueManager {
|
||||
async enqueue(_job: RebuildJob): Promise<void> {}
|
||||
startProcessing(_handlers: RebuildJobHandlers): void {}
|
||||
async isQueued(_job: RebuildJob): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const rebuildQueue: RebuildQueueManager = new NoopRebuildQueue();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { db, logsDb, statusHistory } from "@server/db";
|
||||
import { and, eq, gte, lt, asc, desc } from "drizzle-orm";
|
||||
import { and, eq, gte, asc } from "drizzle-orm";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||
@@ -42,29 +42,7 @@ export async function getCachedStatusHistory(
|
||||
)
|
||||
.orderBy(asc(statusHistory.timestamp));
|
||||
|
||||
// Fetch the last known state before the window so that entities that
|
||||
// haven't changed status recently still show the correct status rather
|
||||
// than appearing as "no_data".
|
||||
const [lastKnownEvent] = await logsDb
|
||||
.select()
|
||||
.from(statusHistory)
|
||||
.where(
|
||||
and(
|
||||
eq(statusHistory.entityType, entityType),
|
||||
eq(statusHistory.entityId, entityId),
|
||||
lt(statusHistory.timestamp, startSec)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(statusHistory.timestamp))
|
||||
.limit(1);
|
||||
|
||||
const priorStatus = lastKnownEvent?.status ?? null;
|
||||
|
||||
const { buckets, totalDowntime } = computeBuckets(
|
||||
events,
|
||||
days,
|
||||
priorStatus
|
||||
);
|
||||
const { buckets, totalDowntime } = computeBuckets(events, days);
|
||||
const totalWindow = days * 86400;
|
||||
const overallUptime =
|
||||
totalWindow > 0
|
||||
@@ -132,8 +110,7 @@ export function computeBuckets(
|
||||
timestamp: number;
|
||||
id: number;
|
||||
}[],
|
||||
days: number,
|
||||
priorStatus: string | null = null
|
||||
days: number
|
||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -159,10 +136,7 @@ export function computeBuckets(
|
||||
.filter((e) => e.timestamp < dayStartSec)
|
||||
.at(-1);
|
||||
|
||||
// Fall back to the last known state before the entire query window
|
||||
// so that entities that haven't generated events recently still show
|
||||
// as their actual status rather than "no_data".
|
||||
const currentStatus = lastBeforeDay?.status ?? priorStatus ?? null;
|
||||
const currentStatus = lastBeforeDay?.status ?? null;
|
||||
|
||||
const windows: { start: number; end: number | null; status: string }[] =
|
||||
[];
|
||||
|
||||
@@ -119,7 +119,8 @@ export async function verifyAccessTokenAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ export async function verifyAdmin(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ export async function verifyApiKeyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@ export async function verifyClientAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -128,7 +129,10 @@ export async function verifyClientAccess(
|
||||
.where(
|
||||
and(
|
||||
eq(roleClients.clientId, client.clientId),
|
||||
inArray(roleClients.roleId, req.userOrgRoleIds!)
|
||||
inArray(
|
||||
roleClients.roleId,
|
||||
req.userOrgRoleIds!
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
@@ -88,7 +88,8 @@ export async function verifyDomainAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||
import { getFirstString } from "@server/lib/requestParams";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifyOrgAccess(
|
||||
req: Request,
|
||||
@@ -60,7 +59,8 @@ export async function verifyOrgAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ export async function verifyResourceAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ export async function verifyResourcePolicyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@ export async function verifyRoleAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ export async function verifySetResourceClients(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ export async function verifySetResourceUsers(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,8 @@ export async function verifySiteAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,8 @@ export async function verifySiteProvisioningKeyAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ export async function verifySiteResourceAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@ export async function verifyTargetAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ export async function verifyUserAccess(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (policyCheck.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(policyCheck.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -693,9 +693,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// logger.debug(
|
||||
// `acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
|
||||
// );
|
||||
logger.debug(
|
||||
`acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
|
||||
);
|
||||
for (const cert of resolverData.Certificates) {
|
||||
allCerts.push(cert);
|
||||
}
|
||||
|
||||
@@ -21,49 +21,6 @@ import {
|
||||
} from "@server/lib/checkOrgAccessPolicy";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
function formatMaxSessionLengthRequirement(
|
||||
maxSessionLengthHours: number
|
||||
): string {
|
||||
if (maxSessionLengthHours < 24) {
|
||||
return `This organization requires you to log in every ${maxSessionLengthHours} hours.`;
|
||||
}
|
||||
|
||||
const maxDays = Math.round(maxSessionLengthHours / 24);
|
||||
return `This organization requires you to log in every ${maxDays} days.`;
|
||||
}
|
||||
|
||||
function buildOrgAccessPolicyError(
|
||||
policies: CheckOrgAccessPolicyResult["policies"]
|
||||
): string | undefined {
|
||||
if (!policies) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
if (policies.requiredTwoFactor === false) {
|
||||
errors.push(
|
||||
"This organization requires two-factor authentication. Enable two-factor authentication on your account to continue."
|
||||
);
|
||||
}
|
||||
|
||||
if (policies.maxSessionLength?.compliant === false) {
|
||||
errors.push(
|
||||
`Your session has expired. ${formatMaxSessionLengthRequirement(
|
||||
policies.maxSessionLength.maxSessionLengthHours
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (policies.passwordAge?.compliant === false) {
|
||||
errors.push(
|
||||
`Your password has expired. This organization requires you to change your password every ${policies.passwordAge.maxPasswordAgeDays} days.`
|
||||
);
|
||||
}
|
||||
|
||||
return errors.length > 0 ? errors.join(" ") : undefined;
|
||||
}
|
||||
|
||||
export function enforceResourceSessionLength(
|
||||
resourceSession: ResourceSession,
|
||||
org: Org
|
||||
@@ -79,17 +36,13 @@ export function enforceResourceSessionLength(
|
||||
if (sessionAgeMs > maxSessionLengthMs) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Your resource session has expired. ${formatMaxSessionLengthRequirement(
|
||||
maxSessionLengthHours
|
||||
)}`
|
||||
error: `Resource session has expired due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Your resource session is invalid. ${formatMaxSessionLengthRequirement(
|
||||
maxSessionLengthHours
|
||||
)}`
|
||||
error: `Resource session is invalid due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -107,20 +60,14 @@ export async function checkOrgAccessPolicy(
|
||||
if (!orgId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Unable to verify organization access. Organization information is missing."
|
||||
error: "Organization ID is required"
|
||||
};
|
||||
}
|
||||
if (!userId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Unable to verify organization access. User information is missing."
|
||||
};
|
||||
return { allowed: false, error: "User ID is required" };
|
||||
}
|
||||
if (!sessionId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your session is invalid. Please log in again."
|
||||
};
|
||||
return { allowed: false, error: "Session ID is required" };
|
||||
}
|
||||
|
||||
if (build === "enterprise") {
|
||||
@@ -142,10 +89,7 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
props.org = orgQuery;
|
||||
if (!props.org) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "This organization could not be found."
|
||||
};
|
||||
return { allowed: false, error: "Organization not found" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +100,7 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(users.userId, userId));
|
||||
props.user = userQuery;
|
||||
if (!props.user) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your account could not be found."
|
||||
};
|
||||
return { allowed: false, error: "User not found" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,17 +111,14 @@ export async function checkOrgAccessPolicy(
|
||||
.where(eq(sessions.sessionId, sessionId));
|
||||
props.session = sessionQuery;
|
||||
if (!props.session) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your session has expired. Please log in again."
|
||||
};
|
||||
return { allowed: false, error: "Session not found" };
|
||||
}
|
||||
}
|
||||
|
||||
if (props.session.userId !== props.user.userId) {
|
||||
return {
|
||||
allowed: false,
|
||||
error: "Your session is invalid. Please log in again."
|
||||
error: "Session does not belong to the user"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -249,14 +187,8 @@ export async function checkOrgAccessPolicy(
|
||||
allowed = false;
|
||||
}
|
||||
|
||||
const policyError = buildOrgAccessPolicyError(policies);
|
||||
|
||||
return {
|
||||
allowed,
|
||||
policies,
|
||||
error: allowed
|
||||
? undefined
|
||||
: (policyError ??
|
||||
"You do not meet this organization's security requirements.")
|
||||
policies
|
||||
};
|
||||
}
|
||||
|
||||
+48
-105
@@ -11,31 +11,14 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { config } from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { redis } from "#private/lib/redis";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const instanceId = uuidv4();
|
||||
|
||||
type LocalLockRecord = {
|
||||
owner: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const localLocks = new Map<string, LocalLockRecord>();
|
||||
|
||||
export class LockManager {
|
||||
private clearExpiredLocalLock(lockKey: string): void {
|
||||
const current = localLocks.get(lockKey);
|
||||
if (current && current.expiresAt <= Date.now()) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
private getLocalOwnerToken(): string {
|
||||
return `${instanceId}:`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a distributed lock using Redis SET with NX and PX options
|
||||
* @param lockKey - Unique identifier for the lock
|
||||
@@ -49,34 +32,12 @@ export class LockManager {
|
||||
retryDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
|
||||
const existing = localLocks.get(lockKey);
|
||||
if (!existing) {
|
||||
localLocks.set(lockKey, {
|
||||
owner: this.getLocalOwnerToken(),
|
||||
expiresAt: Date.now() + ttlMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existing.owner === this.getLocalOwnerToken()) {
|
||||
existing.expiresAt = Date.now() + ttlMs;
|
||||
localLocks.set(lockKey, existing);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = retryDelayMs * Math.pow(2, attempt);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const lockValue = `${instanceId}:${Date.now()}`;
|
||||
const lockValue = `${
|
||||
instanceId
|
||||
}:${Date.now()}`;
|
||||
const redisKey = `lock:${lockKey}`;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
@@ -92,7 +53,11 @@ export class LockManager {
|
||||
);
|
||||
|
||||
if (result === "OK") {
|
||||
logger.debug(`Lock acquired: ${lockKey} by ${instanceId}`);
|
||||
logger.debug(
|
||||
`Lock acquired: ${lockKey} by ${
|
||||
instanceId
|
||||
}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,11 +65,17 @@ export class LockManager {
|
||||
const existingValue = await redis.get(redisKey);
|
||||
if (
|
||||
existingValue &&
|
||||
existingValue.startsWith(`${instanceId}:`)
|
||||
existingValue.startsWith(
|
||||
`${instanceId}:`
|
||||
)
|
||||
) {
|
||||
// Extend the lock TTL since it's the same worker
|
||||
await redis.pexpire(redisKey, ttlMs);
|
||||
logger.debug(`Lock extended: ${lockKey} by ${instanceId}`);
|
||||
logger.debug(
|
||||
`Lock extended: ${lockKey} by ${
|
||||
instanceId
|
||||
}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -117,10 +88,7 @@ export class LockManager {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`,
|
||||
error
|
||||
);
|
||||
logger.error(`Failed to acquire lock ${lockKey} (attempt ${attempt + 1}/${maxRetries}):`, error);
|
||||
// On error, still retry if we have attempts left
|
||||
if (attempt < maxRetries - 1) {
|
||||
const delay = retryDelayMs * Math.pow(2, attempt);
|
||||
@@ -141,11 +109,6 @@ export class LockManager {
|
||||
*/
|
||||
async releaseLock(lockKey: string): Promise<void> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
if (existing && existing.owner === this.getLocalOwnerToken()) {
|
||||
localLocks.delete(lockKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +136,11 @@ export class LockManager {
|
||||
)) as number;
|
||||
|
||||
if (result === 1) {
|
||||
logger.debug(`Lock released: ${lockKey} by ${instanceId}`);
|
||||
logger.debug(
|
||||
`Lock released: ${lockKey} by ${
|
||||
instanceId
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Lock not released - not owned by worker: ${lockKey} by ${
|
||||
@@ -192,7 +159,6 @@ export class LockManager {
|
||||
*/
|
||||
async forceReleaseLock(lockKey: string): Promise<void> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
localLocks.delete(lockKey);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,20 +186,7 @@ export class LockManager {
|
||||
owner?: string;
|
||||
}> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (!existing) {
|
||||
return { exists: false, ownedByMe: false, ttl: 0 };
|
||||
}
|
||||
|
||||
const ttl = Math.max(0, existing.expiresAt - Date.now());
|
||||
return {
|
||||
exists: true,
|
||||
ownedByMe: existing.owner === this.getLocalOwnerToken(),
|
||||
ttl,
|
||||
owner: existing.owner.split(":")[0]
|
||||
};
|
||||
return { exists: false, ownedByMe: true, ttl: 0 };
|
||||
}
|
||||
|
||||
const redisKey = `lock:${lockKey}`;
|
||||
@@ -245,7 +198,11 @@ export class LockManager {
|
||||
]);
|
||||
|
||||
const exists = value !== null;
|
||||
const ownedByMe = exists && value!.startsWith(`${instanceId}:`);
|
||||
const ownedByMe =
|
||||
exists &&
|
||||
value!.startsWith(
|
||||
`${instanceId}:`
|
||||
);
|
||||
const owner = exists ? value!.split(":")[0] : undefined;
|
||||
|
||||
return {
|
||||
@@ -268,15 +225,6 @@ export class LockManager {
|
||||
*/
|
||||
async extendLock(lockKey: string, ttlMs: number): Promise<boolean> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
this.clearExpiredLocalLock(lockKey);
|
||||
const existing = localLocks.get(lockKey);
|
||||
|
||||
if (!existing || existing.owner !== this.getLocalOwnerToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
existing.expiresAt = Date.now() + ttlMs;
|
||||
localLocks.set(lockKey, existing);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -307,7 +255,9 @@ export class LockManager {
|
||||
|
||||
if (result === 1) {
|
||||
logger.debug(
|
||||
`Lock extended: ${lockKey} by ${instanceId} for ${ttlMs}ms`
|
||||
`Lock extended: ${lockKey} by ${
|
||||
instanceId
|
||||
} for ${ttlMs}ms`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -332,13 +282,12 @@ export class LockManager {
|
||||
maxRetries: number = 5,
|
||||
baseDelayMs: number = 100
|
||||
): Promise<boolean> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const acquired = await this.acquireLock(
|
||||
lockKey,
|
||||
ttlMs,
|
||||
1,
|
||||
baseDelayMs
|
||||
);
|
||||
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||
|
||||
if (acquired) {
|
||||
return true;
|
||||
@@ -370,6 +319,10 @@ export class LockManager {
|
||||
fn: () => Promise<T>,
|
||||
ttlMs: number = 30000
|
||||
): Promise<T> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
return await fn();
|
||||
}
|
||||
|
||||
const acquired = await this.acquireLock(lockKey, ttlMs);
|
||||
|
||||
if (!acquired) {
|
||||
@@ -393,21 +346,7 @@ export class LockManager {
|
||||
locksOwnedByMe: number;
|
||||
}> {
|
||||
if (!redis || !redis.status || redis.status !== "ready") {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of localLocks.entries()) {
|
||||
if (value.expiresAt <= now) {
|
||||
localLocks.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
let locksOwnedByMe = 0;
|
||||
for (const value of localLocks.values()) {
|
||||
if (value.owner === this.getLocalOwnerToken()) {
|
||||
locksOwnedByMe++;
|
||||
}
|
||||
}
|
||||
|
||||
return { activeLocksCount: localLocks.size, locksOwnedByMe };
|
||||
return { activeLocksCount: 0, locksOwnedByMe: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -417,7 +356,11 @@ export class LockManager {
|
||||
if (keys.length > 0) {
|
||||
const values = await redis.mget(...keys);
|
||||
locksOwnedByMe = values.filter(
|
||||
(value) => value && value.startsWith(`${instanceId}:`)
|
||||
(value) =>
|
||||
value &&
|
||||
value.startsWith(
|
||||
`${instanceId}:`
|
||||
)
|
||||
).length;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,17 +46,6 @@ const POLL_INTERVAL_MS = 500;
|
||||
class RedisRebuildQueue {
|
||||
private processingStarted = false;
|
||||
|
||||
async isQueued(job: RebuildJob): Promise<boolean> {
|
||||
if (!redis || redis.status !== "ready") return false;
|
||||
const dedupeKey = `${job.type}:${job.id}`;
|
||||
try {
|
||||
const member = await redis.sismember(QUEUED_SET_KEY, dedupeKey);
|
||||
return member === 1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async enqueue(job: RebuildJob): Promise<void> {
|
||||
if (!redis || redis.status !== "ready") {
|
||||
logger.warn(
|
||||
|
||||
@@ -10,8 +10,9 @@ import { hashPassword, verifyPassword } from "@server/auth/password";
|
||||
import { verifyTotpCode } from "@server/auth/totp";
|
||||
import logger from "@server/logger";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
import { invalidateAllSessionsExceptCurrent } from "@server/auth/sessions/app";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { invalidateAllSessions } from "@server/auth/sessions/app";
|
||||
import { sessions, resourceSessions } from "@server/db";
|
||||
import { and, eq, ne, inArray } from "drizzle-orm";
|
||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { sendEmail } from "@server/emails";
|
||||
@@ -30,6 +31,48 @@ export type ChangePasswordResponse = {
|
||||
codeRequested?: boolean;
|
||||
};
|
||||
|
||||
async function invalidateAllSessionsExceptCurrent(
|
||||
userId: string,
|
||||
currentSessionId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
// Get all user sessions except the current one
|
||||
const userSessions = await trx
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
|
||||
// Delete resource sessions for the sessions we're invalidating
|
||||
if (userSessions.length > 0) {
|
||||
await trx.delete(resourceSessions).where(
|
||||
inArray(
|
||||
resourceSessions.userSessionId,
|
||||
userSessions.map((s) => s.sessionId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the user sessions (except current)
|
||||
await trx
|
||||
.delete(sessions)
|
||||
.where(
|
||||
and(
|
||||
eq(sessions.userId, userId),
|
||||
ne(sessions.sessionId, currentSessionId)
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to invalidate user sessions except current", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePassword(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -15,10 +15,6 @@ import TwoFactorAuthNotification from "@server/emails/templates/TwoFactorAuthNot
|
||||
import config from "@server/lib/config";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { generateBackupCodes } from "@server/lib/totp";
|
||||
import {
|
||||
invalidateAllSessions,
|
||||
invalidateAllSessionsExceptCurrent
|
||||
} from "@server/auth/sessions/app";
|
||||
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
|
||||
@@ -172,15 +168,6 @@ export async function verifyTotp(
|
||||
);
|
||||
}
|
||||
|
||||
if (existingSession) {
|
||||
await invalidateAllSessionsExceptCurrent(
|
||||
user.userId,
|
||||
existingSession.sessionId
|
||||
);
|
||||
} else {
|
||||
await invalidateAllSessions(user.userId);
|
||||
}
|
||||
|
||||
sendEmail(
|
||||
TwoFactorAuthNotification({
|
||||
email: user.email!,
|
||||
|
||||
@@ -438,70 +438,6 @@ export async function removePeerDataBatch(
|
||||
await sendToClientsBatch(payloads);
|
||||
}
|
||||
|
||||
export async function updatePeerDataBatch(
|
||||
entries: {
|
||||
clientId: number;
|
||||
siteId: number;
|
||||
remoteSubnets:
|
||||
| {
|
||||
oldRemoteSubnets: string[];
|
||||
newRemoteSubnets: string[];
|
||||
}
|
||||
| undefined;
|
||||
aliases:
|
||||
| {
|
||||
oldAliases: Alias[];
|
||||
newAliases: Alias[];
|
||||
}
|
||||
| undefined;
|
||||
olmId?: string;
|
||||
version?: string | null;
|
||||
}[]
|
||||
) {
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedTargets = await resolveOlmTargets(entries);
|
||||
|
||||
if (resolvedTargets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloads = entries
|
||||
.map((entry) => {
|
||||
const resolved = resolvedTargets.find(
|
||||
(target) => target.clientId === entry.clientId
|
||||
);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: resolved.olmId,
|
||||
message: {
|
||||
type: `olm/wg/peer/data/update`,
|
||||
data: {
|
||||
siteId: entry.siteId,
|
||||
...entry.remoteSubnets,
|
||||
...entry.aliases
|
||||
}
|
||||
},
|
||||
options: {
|
||||
incrementConfigVersion: true,
|
||||
compress: canCompress(resolved.version, "olm")
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry !== null);
|
||||
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendToClientsBatch(payloads);
|
||||
}
|
||||
|
||||
export async function updatePeerData(
|
||||
clientId: number,
|
||||
siteId: number,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
|
||||
import { convertTargetsIfNecessary } from "../client/targets";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
import { waitForSiteRebuildIdle } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
@@ -62,8 +61,6 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForSiteRebuildIdle(siteId);
|
||||
|
||||
// update the endpoint and the public key
|
||||
const [site] = await db
|
||||
.update(sites)
|
||||
|
||||
@@ -49,22 +49,20 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
||||
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
||||
);
|
||||
|
||||
// TODO: IMPLEMENT THE SYNC ON THE NEWT SIDE AND COMMENT THIS BACK IN
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, newt.siteId))
|
||||
.limit(1);
|
||||
|
||||
// const [site] = await db
|
||||
// .select()
|
||||
// .from(sites)
|
||||
// .where(eq(sites.siteId, newt.siteId))
|
||||
// .limit(1);
|
||||
if (!site) {
|
||||
logger.warn(
|
||||
`Newt ping message: site with ID ${newt.siteId} not found`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!site) {
|
||||
// logger.warn(
|
||||
// `Newt ping message: site with ID ${newt.siteId} not found`
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await sendNewtSyncMessage(newt, site);
|
||||
await sendNewtSyncMessage(newt, site);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { build } from "@server/build";
|
||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||
import config from "@server/lib/config";
|
||||
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
|
||||
import { waitForClientRebuildIdle } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
|
||||
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
|
||||
@@ -386,8 +385,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
}
|
||||
|
||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||
await waitForClientRebuildIdle(olm.clientId);
|
||||
|
||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||
client,
|
||||
publicKey,
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { db } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
db,
|
||||
newts,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
targets
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -7,11 +16,9 @@ import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
performDeleteResource,
|
||||
runResourceDeleteSideEffects
|
||||
} from "@server/lib/deleteResource";
|
||||
import { removeTargets } from "../newt/targets";
|
||||
|
||||
// Define Zod schema for request parameters validation
|
||||
const deleteResourceSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
@@ -60,13 +67,27 @@ export async function deleteResource(
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
let deleteResult = null;
|
||||
const targetsToBeRemoved = await db
|
||||
.select()
|
||||
.from(targets)
|
||||
.where(eq(targets.resourceId, resourceId));
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
deleteResult = await performDeleteResource(resourceId, trx);
|
||||
});
|
||||
const healthChecksToBeRemoved = await db
|
||||
.select()
|
||||
.from(targetHealthCheck)
|
||||
.where(
|
||||
inArray(
|
||||
targetHealthCheck.targetId,
|
||||
targetsToBeRemoved.map((t) => t.targetId)
|
||||
)
|
||||
);
|
||||
|
||||
if (!deleteResult) {
|
||||
const [deletedResource] = await db
|
||||
.delete(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.returning();
|
||||
|
||||
if (!deletedResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -75,7 +96,54 @@ export async function deleteResource(
|
||||
);
|
||||
}
|
||||
|
||||
await runResourceDeleteSideEffects(deleteResult);
|
||||
for (const target of targetsToBeRemoved) {
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, target.siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${target.siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (site.pubKey) {
|
||||
if (site.type == "newt") {
|
||||
// get the newt on the site by querying the newt table for siteId
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
await removeTargets(
|
||||
newt.newtId,
|
||||
// [target],
|
||||
[], // deleting the target from newt causes issues because we cant unbind the port. this needs to be fixed in newt before we can do this
|
||||
healthChecksToBeRemoved,
|
||||
deletedResource.mode === "udp" ? "udp" : "tcp",
|
||||
newt.version
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete default resource policy
|
||||
if (deletedResource.defaultResourcePolicyId) {
|
||||
await db
|
||||
.delete(resourcePolicies)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicies.resourcePolicyId,
|
||||
deletedResource.defaultResourcePolicyId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
@@ -86,9 +154,6 @@ export async function deleteResource(
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
|
||||
@@ -80,7 +80,8 @@ export async function getExchangeToken(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"" + (hasAccess.error || "Unknown error")
|
||||
"Failed organization access policy check: " +
|
||||
(hasAccess.error || "Unknown error")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,41 +14,18 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import {
|
||||
deleteAssociatedResourcesForSite,
|
||||
exceedsSiteAssociatedResourceDeleteLimit,
|
||||
getAssociatedResourceCountForSite,
|
||||
runDeleteSiteAssociatedResourcesSideEffects,
|
||||
MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE,
|
||||
type DeleteSiteAssociatedResourcesSideEffects
|
||||
} from "@server/lib/deleteSiteAssociatedResources";
|
||||
|
||||
const deleteSiteSchema = z.strictObject({
|
||||
siteId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const deleteSiteQuerySchema = z.strictObject({
|
||||
deleteResources: z
|
||||
.enum(["true", "false"])
|
||||
.transform((v) => v === "true")
|
||||
.optional()
|
||||
.catch(false)
|
||||
.openapi({
|
||||
type: "boolean",
|
||||
description:
|
||||
"When true, also deletes all public and private resources associated with this site"
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/site/{siteId}",
|
||||
description: "Delete a site and all its associated data.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: deleteSiteSchema,
|
||||
query: deleteSiteQuerySchema
|
||||
params: deleteSiteSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
@@ -84,18 +61,7 @@ export async function deleteSite(
|
||||
);
|
||||
}
|
||||
|
||||
const parsedQuery = deleteSiteQuerySchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId } = parsedParams.data;
|
||||
const { deleteResources } = parsedQuery.data;
|
||||
|
||||
const [site] = await db
|
||||
.select()
|
||||
@@ -112,67 +78,20 @@ export async function deleteSite(
|
||||
);
|
||||
}
|
||||
|
||||
if (deleteResources) {
|
||||
const canDeletePublic = await checkUserActionPermission(
|
||||
ActionsEnum.deleteResource,
|
||||
req
|
||||
);
|
||||
const canDeletePrivate = await checkUserActionPermission(
|
||||
ActionsEnum.deleteSiteResource,
|
||||
req
|
||||
);
|
||||
|
||||
if (!canDeletePublic || !canDeletePrivate) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission to delete associated resources"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const associatedResourceCount =
|
||||
await getAssociatedResourceCountForSite(siteId, site.orgId);
|
||||
|
||||
if (
|
||||
exceedsSiteAssociatedResourceDeleteLimit(
|
||||
associatedResourceCount
|
||||
)
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Cannot delete site and associated resources when the site has more than ${MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE} resources`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [deletedNewt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
let resourceSideEffects: DeleteSiteAssociatedResourcesSideEffects = {
|
||||
resources: [],
|
||||
siteResources: []
|
||||
};
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (deleteResources) {
|
||||
resourceSideEffects = await deleteAssociatedResourcesForSite(
|
||||
siteId,
|
||||
site.orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (site.type == "wireguard") {
|
||||
if (site.pubKey) {
|
||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||
}
|
||||
} else if (site.type == "newt") {
|
||||
// Clean up all client associations and send peer/proxy removal
|
||||
// messages in a single efficient pass before deleting the row.
|
||||
await cleanupSiteAssociations(site, trx);
|
||||
}
|
||||
|
||||
@@ -180,17 +99,13 @@ export async function deleteSite(
|
||||
await usageService.add(site.orgId, FeatureId.SITES, -1, trx);
|
||||
});
|
||||
|
||||
if (deleteResources) {
|
||||
await runDeleteSiteAssociatedResourcesSideEffects(
|
||||
resourceSideEffects
|
||||
);
|
||||
}
|
||||
|
||||
// Send termination message outside of transaction to prevent blocking
|
||||
if (deletedNewt) {
|
||||
const payload = {
|
||||
type: `newt/wg/terminate`,
|
||||
data: {}
|
||||
};
|
||||
// Don't await this to prevent blocking the response
|
||||
sendToClient(deletedNewt.newtId, payload).catch((error) => {
|
||||
logger.error(
|
||||
"Failed to send termination message to newt:",
|
||||
@@ -208,9 +123,6 @@ export async function deleteSite(
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||
|
||||
const getSiteSchema = z.strictObject({
|
||||
siteId: z
|
||||
@@ -48,7 +47,6 @@ type SiteQueryRow = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
export type GetSiteResponse = SiteQueryRow["sites"] & {
|
||||
newtId: string | null;
|
||||
newtVersion: string | null;
|
||||
countryCode: string | null;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
@@ -136,10 +134,7 @@ export async function getSite(
|
||||
const data: GetSiteResponse = {
|
||||
...site.sites,
|
||||
newtId: site.newt ? site.newt.newtId : null,
|
||||
newtVersion: site.newt?.version ?? null,
|
||||
countryCode: site.sites.endpoint
|
||||
? ((await getCountryCodeForIp(site.sites.endpoint)) ?? null)
|
||||
: null
|
||||
newtVersion: site.newt?.version ?? null
|
||||
};
|
||||
|
||||
return response<GetSiteResponse>(res, {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, siteResources } from "@server/db";
|
||||
import { db, newts, primaryDb, sites } from "@server/db";
|
||||
import { siteResources } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import {
|
||||
performDeleteSiteResource,
|
||||
runSiteResourceDeleteSideEffects
|
||||
} from "@server/lib/deleteSiteResource";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const deleteSiteResourceParamsSchema = z.strictObject({
|
||||
siteResourceId: z.coerce.number().int().positive()
|
||||
@@ -67,10 +65,11 @@ export async function deleteSiteResource(
|
||||
|
||||
const { siteResourceId } = parsedParams.data;
|
||||
|
||||
// Check if site resource exists
|
||||
const [existingSiteResource] = await db
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(eq(siteResources.siteResourceId, siteResourceId))
|
||||
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingSiteResource) {
|
||||
@@ -79,22 +78,26 @@ export async function deleteSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
let removedSiteResource = null;
|
||||
// Delete the site resource
|
||||
const [removedSiteResource] = await db
|
||||
.delete(siteResources)
|
||||
.where(eq(siteResources.siteResourceId, siteResourceId))
|
||||
.returning();
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
removedSiteResource = await performDeleteSiteResource(
|
||||
siteResourceId,
|
||||
trx
|
||||
);
|
||||
});
|
||||
// Run in the background after the response is sent. Wrapped in its
|
||||
// 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).catch(
|
||||
(err) => {
|
||||
logger.error(
|
||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (!removedSiteResource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Site resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
runSiteResourceDeleteSideEffects(removedSiteResource);
|
||||
logger.info(`Deleted site resource ${siteResourceId}`);
|
||||
|
||||
return response(res, {
|
||||
data: { message: "Site resource deleted successfully" },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
clientSiteResources,
|
||||
clientSiteResourcesAssociationsCache,
|
||||
db,
|
||||
newts,
|
||||
orgs,
|
||||
roles,
|
||||
roleSiteResources,
|
||||
@@ -8,7 +10,10 @@ import {
|
||||
SiteResource,
|
||||
siteResources,
|
||||
sites,
|
||||
userSiteResources
|
||||
networks,
|
||||
Transaction,
|
||||
userSiteResources,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -16,11 +21,17 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, and, ne, inArray } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isIpInCidr, portRangeStringSchema } from "@server/lib/ip";
|
||||
import { updatePeerData, updateTargets } from "@server/routers/client/targets";
|
||||
import {
|
||||
handleMessagingForUpdatedSiteResource,
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
waitForSiteResourceRebuildIdle
|
||||
generateAliasConfig,
|
||||
generateRemoteSubnets,
|
||||
generateSubnetProxyTargetV2,
|
||||
isIpInCidr,
|
||||
portRangeStringSchema
|
||||
} from "@server/lib/ip";
|
||||
import {
|
||||
getClientSiteResourceAccess,
|
||||
rebuildClientAssociationsFromSiteResource
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -379,7 +390,7 @@ export async function updateSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
const existingSiteNetworks = existingSiteResource.networkId
|
||||
const existingSiteIds = existingSiteResource.networkId
|
||||
? await db
|
||||
.select()
|
||||
.from(siteNetworks)
|
||||
@@ -387,7 +398,7 @@ export async function updateSiteResource(
|
||||
eq(siteNetworks.networkId, existingSiteResource.networkId)
|
||||
)
|
||||
: [];
|
||||
const existingSiteIds = existingSiteNetworks.map((sn) => sn.siteId);
|
||||
const existingSiteIdSet = new Set(existingSiteIds.map((s) => s.siteId));
|
||||
|
||||
let fullDomain: string | null = null;
|
||||
let finalSubdomain: string | null = null;
|
||||
@@ -453,7 +464,6 @@ export async function updateSiteResource(
|
||||
}
|
||||
|
||||
let updatedSiteResource: SiteResource | undefined;
|
||||
let updatedSiteIds: number[] = [];
|
||||
await db.transaction(async (trx) => {
|
||||
// Update the site resource
|
||||
const sshPamSet =
|
||||
@@ -524,7 +534,6 @@ export async function updateSiteResource(
|
||||
siteId: siteId,
|
||||
networkId: updatedSiteResource.networkId!
|
||||
});
|
||||
updatedSiteIds.push(siteId);
|
||||
}
|
||||
|
||||
await trx
|
||||
@@ -596,27 +605,27 @@ export async function updateSiteResource(
|
||||
throw new Error("No updated resource found after update");
|
||||
}
|
||||
|
||||
const finalUpdatedSiteResource = updatedSiteResource;
|
||||
|
||||
rebuildClientAssociationsFromSiteResource(finalUpdatedSiteResource)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
finalUpdatedSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource,
|
||||
finalUpdatedSiteResource,
|
||||
existingSiteIds,
|
||||
updatedSiteIds
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
rebuildClientAssociationsFromSiteResource(updatedSiteResource).catch(
|
||||
(e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${siteResourceId}. Error: ${e}`
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource,
|
||||
updatedSiteResource,
|
||||
Array.from(existingSiteIdSet).map((siteId: number) => ({
|
||||
// we already added to the new sites above in the rebuild function so we only need to update the ones that did not change
|
||||
siteId,
|
||||
orgId: existingSiteResource.orgId
|
||||
}))
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to handle messaging for updated site resource ${siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: updatedSiteResource,
|
||||
@@ -635,3 +644,211 @@ export async function updateSiteResource(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMessagingForUpdatedSiteResource(
|
||||
existingSiteResource: SiteResource | undefined,
|
||||
updatedSiteResource: SiteResource,
|
||||
sites: { siteId: number; orgId: string }[]
|
||||
) {
|
||||
const trx = primaryDb;
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
|
||||
existingSiteResource
|
||||
);
|
||||
logger.debug(
|
||||
"handleMessagingForUpdatedSiteResource: updatedSiteResource is: ",
|
||||
updatedSiteResource
|
||||
);
|
||||
|
||||
const { sitesList, mergedAllClients, mergedAllClientIds } =
|
||||
await getClientSiteResourceAccess(
|
||||
existingSiteResource || updatedSiteResource,
|
||||
trx
|
||||
);
|
||||
|
||||
const siteIds = sites.map((site) => site.siteId);
|
||||
|
||||
// after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed
|
||||
const destinationChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.destination !== updatedSiteResource.destination;
|
||||
const destinationPortChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.destinationPort !==
|
||||
updatedSiteResource.destinationPort;
|
||||
const aliasChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.alias !== updatedSiteResource.alias;
|
||||
const fullDomainChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.fullDomain !== updatedSiteResource.fullDomain;
|
||||
const sslChanged =
|
||||
existingSiteResource &&
|
||||
existingSiteResource.ssl !== updatedSiteResource.ssl;
|
||||
const portRangesChanged =
|
||||
existingSiteResource &&
|
||||
(existingSiteResource.tcpPortRangeString !==
|
||||
updatedSiteResource.tcpPortRangeString ||
|
||||
existingSiteResource.udpPortRangeString !==
|
||||
updatedSiteResource.udpPortRangeString ||
|
||||
existingSiteResource.disableIcmp !==
|
||||
updatedSiteResource.disableIcmp);
|
||||
|
||||
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
|
||||
|
||||
if (
|
||||
destinationChanged ||
|
||||
aliasChanged ||
|
||||
fullDomainChanged ||
|
||||
sslChanged ||
|
||||
portRangesChanged ||
|
||||
destinationPortChanged
|
||||
) {
|
||||
const newtsForSites =
|
||||
siteIds.length > 0
|
||||
? await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(inArray(newts.siteId, siteIds))
|
||||
: [];
|
||||
const newtBySiteId = new Map(
|
||||
newtsForSites.map((newt) => [newt.siteId, newt])
|
||||
);
|
||||
|
||||
const oldDestinationStillInUseClientSitePairs = new Set<string>();
|
||||
if (
|
||||
existingSiteResource?.destination &&
|
||||
siteIds.length > 0 &&
|
||||
mergedAllClientIds.length > 0
|
||||
) {
|
||||
const oldDestinationStillInUseRows = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
mergedAllClientIds
|
||||
),
|
||||
inArray(siteNetworks.siteId, siteIds),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
existingSiteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
existingSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
for (const row of oldDestinationStillInUseRows) {
|
||||
oldDestinationStillInUseClientSitePairs.add(
|
||||
`${row.clientId}:${row.siteId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldUpdateTargets =
|
||||
destinationChanged ||
|
||||
sslChanged ||
|
||||
portRangesChanged ||
|
||||
fullDomainChanged ||
|
||||
destinationPortChanged;
|
||||
const oldTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
const newTargets = shouldUpdateTargets
|
||||
? await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const site of sites) {
|
||||
const newt = newtBySiteId.get(site.siteId);
|
||||
|
||||
if (!newt) {
|
||||
throw new Error(
|
||||
"Newt not found for site during site resource update"
|
||||
);
|
||||
}
|
||||
|
||||
// Only update targets on newt if these items change
|
||||
if (shouldUpdateTargets) {
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{
|
||||
oldTargets: oldTargets ? oldTargets : [],
|
||||
newTargets: newTargets ? newTargets : []
|
||||
},
|
||||
newt.version
|
||||
);
|
||||
}
|
||||
|
||||
const olmJobs: Promise<void>[] = [];
|
||||
for (const client of mergedAllClients) {
|
||||
// does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet
|
||||
if (!existingSiteResource.destination) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldDestinationStillInUseByASite =
|
||||
oldDestinationStillInUseClientSitePairs.has(
|
||||
`${client.clientId}:${site.siteId}`
|
||||
);
|
||||
|
||||
// we also need to update the remote subnets on the olms for each client that has access to this site
|
||||
olmJobs.push(
|
||||
updatePeerData(
|
||||
// TODO: THIS SHOULD BE UPDATED TO WORK I A BATCH
|
||||
client.clientId,
|
||||
site.siteId,
|
||||
destinationChanged
|
||||
? {
|
||||
oldRemoteSubnets:
|
||||
!oldDestinationStillInUseByASite
|
||||
? generateRemoteSubnets([
|
||||
existingSiteResource
|
||||
])
|
||||
: [],
|
||||
newRemoteSubnets: generateRemoteSubnets([
|
||||
updatedSiteResource
|
||||
])
|
||||
}
|
||||
: undefined,
|
||||
aliasChanged || fullDomainChanged // the full domain is sent down as an alias
|
||||
? {
|
||||
oldAliases: generateAliasConfig([
|
||||
existingSiteResource
|
||||
]),
|
||||
newAliases: generateAliasConfig([
|
||||
updatedSiteResource
|
||||
])
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(olmJobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={4}>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("name")}</InfoSectionTitle>
|
||||
<InfoSectionContent>{org.org.name}</InfoSectionContent>
|
||||
@@ -34,14 +34,6 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
|
||||
{org.org.subnet || t("none")}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("utilitySubnet")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{org.org.utilitySubnet || t("none")}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ProductUpdate,
|
||||
productUpdatesQueries
|
||||
} from "@app/lib/queries";
|
||||
import { build } from "@server/build";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -40,42 +39,22 @@ export default function ProductUpdates({
|
||||
}) {
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const productUpdatesEnabled = env.app.notifications.product_updates;
|
||||
const versionCheckEnabled =
|
||||
env.app.notifications.new_releases && build !== "saas";
|
||||
|
||||
const data = useQueries({
|
||||
queries: [
|
||||
productUpdatesQueries.list(productUpdatesEnabled, env.app.version),
|
||||
productUpdatesQueries.list(
|
||||
env.app.notifications.product_updates,
|
||||
env.app.version
|
||||
),
|
||||
productUpdatesQueries.latestVersion(
|
||||
env.app.notifications.new_releases
|
||||
)
|
||||
],
|
||||
combine(result) {
|
||||
const [updatesQuery, versionQuery] = result;
|
||||
|
||||
const updatesSettled =
|
||||
!productUpdatesEnabled ||
|
||||
updatesQuery.isFetched ||
|
||||
updatesQuery.isError;
|
||||
const versionSettled =
|
||||
!versionCheckEnabled ||
|
||||
versionQuery.isFetched ||
|
||||
versionQuery.isError;
|
||||
|
||||
if (!updatesSettled || !versionSettled) return null;
|
||||
|
||||
const updates = updatesQuery.isError
|
||||
? []
|
||||
: Array.isArray(updatesQuery.data?.data)
|
||||
? updatesQuery.data.data
|
||||
: [];
|
||||
|
||||
const latestVersion = versionQuery.isError
|
||||
? undefined
|
||||
: versionQuery.data;
|
||||
|
||||
return { updates, latestVersion };
|
||||
if (result[0].isLoading || result[1].isLoading) return null;
|
||||
return {
|
||||
updates: result[0].data?.data ?? [],
|
||||
latestVersion: result[1].data
|
||||
};
|
||||
}
|
||||
});
|
||||
const t = useTranslations();
|
||||
@@ -97,30 +76,19 @@ export default function ProductUpdates({
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const versionResponse = data.latestVersion?.data;
|
||||
const latestVersion = versionResponse?.pangolin?.latestVersion;
|
||||
const latestVersion = data?.latestVersion?.data?.pangolin.latestVersion;
|
||||
const currentVersion = env.app.version;
|
||||
|
||||
let showNewVersionPopup = false;
|
||||
if (
|
||||
const showNewVersionPopup = Boolean(
|
||||
latestVersion &&
|
||||
valid(latestVersion) &&
|
||||
valid(currentVersion) &&
|
||||
ignoredVersionUpdate !== latestVersion
|
||||
) {
|
||||
try {
|
||||
showNewVersionPopup = gt(latestVersion, currentVersion);
|
||||
} catch {
|
||||
showNewVersionPopup = false;
|
||||
}
|
||||
}
|
||||
|
||||
const readUpdateIds = Array.isArray(productUpdatesRead)
|
||||
? productUpdatesRead
|
||||
: [];
|
||||
valid(latestVersion) &&
|
||||
valid(currentVersion) &&
|
||||
ignoredVersionUpdate !== latestVersion &&
|
||||
gt(latestVersion, currentVersion)
|
||||
);
|
||||
|
||||
const filteredUpdates = data.updates.filter(
|
||||
(update) => !readUpdateIds.includes(update.id)
|
||||
(update) => !productUpdatesRead.includes(update.id)
|
||||
);
|
||||
|
||||
if (filteredUpdates.length === 0 && !showNewVersionPopup) {
|
||||
@@ -165,14 +133,17 @@ export default function ProductUpdates({
|
||||
show={filteredUpdates.length > 0}
|
||||
onDimissAll={() =>
|
||||
setProductUpdatesRead([
|
||||
...readUpdateIds,
|
||||
...productUpdatesRead,
|
||||
...filteredUpdates.map(
|
||||
(update) => update.id
|
||||
)
|
||||
])
|
||||
}
|
||||
onDimiss={(id) =>
|
||||
setProductUpdatesRead([...readUpdateIds, id])
|
||||
setProductUpdatesRead([
|
||||
...productUpdatesRead,
|
||||
id
|
||||
])
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -180,9 +151,11 @@ export default function ProductUpdates({
|
||||
</div>
|
||||
|
||||
<NewVersionAvailable
|
||||
version={versionResponse}
|
||||
version={data.latestVersion?.data}
|
||||
onDimiss={() => {
|
||||
setIgnoredVersionUpdate(latestVersion ?? null);
|
||||
setIgnoredVersionUpdate(
|
||||
data.latestVersion?.data?.pangolin.latestVersion ?? null
|
||||
);
|
||||
}}
|
||||
show={showNewVersionPopup}
|
||||
/>
|
||||
@@ -373,10 +346,6 @@ function NewVersionAvailable({
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
if (!version?.pangolin?.latestVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition show={open}>
|
||||
{version && (
|
||||
|
||||
@@ -61,7 +61,7 @@ export function parseUnixGroups(value: string | undefined): string[] {
|
||||
if (!value?.trim()) return [];
|
||||
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.split(/[,\s\n]+/)
|
||||
.map((group) => group.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -69,10 +69,18 @@ export function parseUnixGroups(value: string | undefined): string[] {
|
||||
export function parseSudoCommands(value: string | undefined): string[] {
|
||||
if (!value?.trim()) return [];
|
||||
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.map((command) => command.trim())
|
||||
.filter(Boolean);
|
||||
const commands: string[] = [];
|
||||
for (const segment of value.split(/[,\n]+/)) {
|
||||
const trimmed = segment.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
for (const part of trimmed.split(/ (?=\/)/)) {
|
||||
const command = part.trim();
|
||||
if (command) commands.push(command);
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
function hasOnlyAbsoluteSudoCommands(value: string | undefined): boolean {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
|
||||
|
||||
type SiteInfoCardProps = {};
|
||||
|
||||
@@ -53,11 +52,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("publicIpEndpoint")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{formatPublicEndpoint(site.endpoint)}
|
||||
<span className="text-lg">
|
||||
{site.countryCode &&
|
||||
countryCodeToFlagEmoji(site.countryCode)}
|
||||
</span>
|
||||
{formatPublicEndpoint(site.endpoint)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
@@ -105,7 +104,6 @@ export default function SitesTable({
|
||||
} = useNavigationContext();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deleteWithResources, setDeleteWithResources] = useState(false);
|
||||
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
|
||||
const [resourcesDialogSite, setResourcesDialogSite] =
|
||||
useState<SiteRow | null>(null);
|
||||
@@ -159,12 +157,10 @@ export default function SitesTable({
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSite(siteId: number, withResources: boolean) {
|
||||
function deleteSite(siteId: number) {
|
||||
startTransition(async () => {
|
||||
await api
|
||||
.delete(`/site/${siteId}`, {
|
||||
params: { deleteResources: withResources }
|
||||
})
|
||||
.delete(`/site/${siteId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("siteErrorDelete"), e);
|
||||
toast({
|
||||
@@ -525,33 +521,16 @@ export default function SitesTable({
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedSite(siteRow);
|
||||
setDeleteWithResources(false);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("sitesTableDeleteSite")}
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{siteRow.resourceCount <= 250 && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedSite(siteRow);
|
||||
setDeleteWithResources(true);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t(
|
||||
"sitesTableDeleteSiteAndResources"
|
||||
)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
@@ -660,38 +639,19 @@ export default function SitesTable({
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedSite(null);
|
||||
setDeleteWithResources(false);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{deleteWithResources
|
||||
? t("siteQuestionRemoveAndResources")
|
||||
: t("siteQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{deleteWithResources
|
||||
? t("siteMessageRemoveAndResources")
|
||||
: t("siteMessageRemove")}
|
||||
</p>
|
||||
<p>{t("siteQuestionRemove")}</p>
|
||||
<p>{t("siteMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={
|
||||
deleteWithResources
|
||||
? t("siteConfirmDeleteAndResources")
|
||||
: t("siteConfirmDelete")
|
||||
}
|
||||
buttonText={t("siteConfirmDelete")}
|
||||
onConfirm={async () =>
|
||||
startTransition(() =>
|
||||
deleteSite(selectedSite!.id, deleteWithResources)
|
||||
)
|
||||
startTransition(() => deleteSite(selectedSite!.id))
|
||||
}
|
||||
string={selectedSite.name}
|
||||
title={
|
||||
deleteWithResources
|
||||
? t("siteDeleteAndResources")
|
||||
: t("siteDelete")
|
||||
}
|
||||
title={t("siteDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@ import {
|
||||
sortPolicyRulesForResourceOverlay,
|
||||
type PolicyAccessRule
|
||||
} from "./policy-access-rule-utils";
|
||||
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
|
||||
|
||||
export type PolicyAccessRulesTableProps = {
|
||||
rules: PolicyAccessRule[];
|
||||
@@ -491,17 +490,8 @@ export function PolicyAccessRulesTable({
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: () => <span className="p-3">{t("value")}</span>,
|
||||
cell: ({ row }) => {
|
||||
let selectedCountry: (typeof COUNTRIES)[number] | undefined;
|
||||
if (
|
||||
row.original.match === "COUNTRY" &&
|
||||
row.original.value
|
||||
) {
|
||||
selectedCountry = COUNTRIES.find(
|
||||
(c) => c.code === row.original.value
|
||||
);
|
||||
}
|
||||
return row.original.match === "COUNTRY" ? (
|
||||
cell: ({ row }) =>
|
||||
row.original.match === "COUNTRY" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@@ -512,22 +502,15 @@ export function PolicyAccessRulesTable({
|
||||
}
|
||||
className="w-full min-w-0 justify-between"
|
||||
>
|
||||
{selectedCountry ? (
|
||||
<>
|
||||
<span>
|
||||
{selectedCountry.code === "ALL"
|
||||
? "🌍"
|
||||
: countryCodeToFlagEmoji(
|
||||
selectedCountry.code
|
||||
)}
|
||||
|
||||
{selectedCountry.name} (
|
||||
{selectedCountry.code})
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
t("selectCountry")
|
||||
)}
|
||||
{row.original.value
|
||||
? COUNTRIES.find(
|
||||
(c) =>
|
||||
c.code === row.original.value
|
||||
)?.name +
|
||||
" (" +
|
||||
row.original.value +
|
||||
")"
|
||||
: t("selectCountry")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -557,13 +540,6 @@ export function PolicyAccessRulesTable({
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${row.original.value === country.code ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
<span>
|
||||
{country.code === "ALL"
|
||||
? "🌍"
|
||||
: countryCodeToFlagEmoji(
|
||||
country.code
|
||||
)}
|
||||
</span>
|
||||
{country.name} (
|
||||
{country.code})
|
||||
</CommandItem>
|
||||
@@ -791,8 +767,7 @@ export function PolicyAccessRulesTable({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
|
||||
Reference in New Issue
Block a user