mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-13 21:36:38 +00:00
Compare commits
105 Commits
revert-276
...
miloschwar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02dfeed3ce | ||
|
|
3436105bec | ||
|
|
d948d2ec33 | ||
|
|
4b3375ab8e | ||
|
|
6b8a3c8d77 | ||
|
|
ba9794c067 | ||
|
|
6ce165bfd5 | ||
|
|
eb4b2daaab | ||
|
|
8cbc8dec89 | ||
|
|
e89e60d50b | ||
|
|
c45308f234 | ||
|
|
40205c40c5 | ||
|
|
f3fe2dd33b | ||
|
|
8edcc45033 | ||
|
|
91471a4aca | ||
|
|
ae2c37a2f6 | ||
|
|
c8208f0a88 | ||
|
|
e11dfbd29c | ||
|
|
b375d20598 | ||
|
|
c4b82c69f8 | ||
|
|
c9a00420a0 | ||
|
|
36ef9cd442 | ||
|
|
5e08779ab0 | ||
|
|
16a0e1ce7b | ||
|
|
8b03484ade | ||
|
|
9da9974adf | ||
|
|
6f80cf3db2 | ||
|
|
76d8f44779 | ||
|
|
700c92efcb | ||
|
|
d17e0c9f50 | ||
|
|
f00b9794f5 | ||
|
|
daff59c93f | ||
|
|
aa8954366c | ||
|
|
87464d53bd | ||
|
|
e04f17c9aa | ||
|
|
b25e3499d8 | ||
|
|
2e6f74a6f8 | ||
|
|
8eee0ca5a5 | ||
|
|
c2ebc0a0ff | ||
|
|
03c905a7af | ||
|
|
035644eaf7 | ||
|
|
8ce45a1acd | ||
|
|
16e7233a3e | ||
|
|
a331dd3fb4 | ||
|
|
e3e2938b28 | ||
|
|
73e96b1b28 | ||
|
|
b8194295ec | ||
|
|
382a46dfff | ||
|
|
1f74e1b320 | ||
|
|
fee780cb81 | ||
|
|
5056cba85d | ||
|
|
dab38ff82c | ||
|
|
d83fa63af5 | ||
|
|
d5837ab718 | ||
|
|
f85cfc4c68 | ||
|
|
0b2aceafe0 | ||
|
|
059db34a53 | ||
|
|
bc1ea86b4e | ||
|
|
9f2ced1933 | ||
|
|
013cff9b6e | ||
|
|
aa19437031 | ||
|
|
e848ef848b | ||
|
|
bb6605337f | ||
|
|
8df8383468 | ||
|
|
a7e9de3ac4 | ||
|
|
8df41f514e | ||
|
|
c2bf50b121 | ||
|
|
4e7dcbd7b5 | ||
|
|
b7ccb92236 | ||
|
|
23a151dd45 | ||
|
|
122079ddb2 | ||
|
|
1d0b0ae6ec | ||
|
|
f1a0bc97e3 | ||
|
|
a57dfd1d12 | ||
|
|
c0a8304b91 | ||
|
|
ab7b968e28 | ||
|
|
f10b40c3b0 | ||
|
|
7878ac9c76 | ||
|
|
0752951842 | ||
|
|
06bb6636a1 | ||
|
|
2fdd332a31 | ||
|
|
98b1e9546a | ||
|
|
184aa65c6d | ||
|
|
70b3a432a4 | ||
|
|
fb4fc75bd8 | ||
|
|
0479ed9e7f | ||
|
|
8f3fbb94d2 | ||
|
|
e8c35bec1c | ||
|
|
728e7252eb | ||
|
|
1218507f7d | ||
|
|
a2dff0a35d | ||
|
|
f411180908 | ||
|
|
231a19b679 | ||
|
|
58a87a986a | ||
|
|
61a78ef352 | ||
|
|
e28e5ebb4e | ||
|
|
19cef8c453 | ||
|
|
1290d6cd5c | ||
|
|
ad301074db | ||
|
|
30a756d254 | ||
|
|
0fc1aa9191 | ||
|
|
ddf417f4ca | ||
|
|
d08be59055 | ||
|
|
322c136d1f | ||
|
|
e06f2f47b1 |
22
README.md
22
README.md
@@ -35,19 +35,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.pangolin.net/careers/join-us">
|
||||
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
|
||||
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -60,16 +54,16 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
||||
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing - no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. |
|
||||
| **Pangolin Cloud** | Fully managed service - no infrastructure required. |
|
||||
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue. |
|
||||
|
||||
## Key Features
|
||||
|
||||
| <img width=500 /> | <img width=500 /> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
| **Connect remote networks with sites**<br /><br />Pangolin's site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
| **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
|
||||
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
|
||||
|
||||
@@ -87,7 +81,7 @@ Download the Pangolin client for your platform:
|
||||
|
||||
### Sign up now
|
||||
|
||||
Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available.
|
||||
Create a free account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud.
|
||||
|
||||
### Check out the docs
|
||||
|
||||
@@ -102,7 +96,3 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License
|
||||
## Contributions
|
||||
|
||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||
|
||||
---
|
||||
|
||||
WireGuard® is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -86,6 +86,8 @@ entryPoints:
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
middlewares:
|
||||
- crowdsec@file
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Ключът за осигуряване е актуализиран",
|
||||
"provisioningKeysUpdatedDescription": "Вашите промени бяха запазени.",
|
||||
"provisioningKeysBannerTitle": "Ключове за осигуряване на сайта",
|
||||
"provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте с Newt конектора за автоматично създаване на сайтове при първото стартиране — няма нужда от създаване на отделни идентификационни данни за всеки сайт.",
|
||||
"provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте със съединителя Newt за автоматично създаване на сайтове при първоначално стартиране - не е необходимо да се създават отделни идентификационни данни за всеки сайт.",
|
||||
"provisioningKeysBannerButtonText": "Научете повече",
|
||||
"pendingSitesBannerTitle": "Чакащи сайтове",
|
||||
"pendingSitesBannerDescription": "Сайтовете, които се свързват чрез ключ за осигуряване, се появяват тук за преглед. Одобрете всеки сайт, преди да стане активен и да получи достъп до вашите ресурси.",
|
||||
"pendingSitesBannerDescription": "Сайтовете, които се свързват с ключ за осигуряване, ще се появят тук за преглед.",
|
||||
"pendingSitesBannerButtonText": "Научете повече",
|
||||
"apiKeysSettings": "Настройки на {apiKeyName}",
|
||||
"userTitle": "Управление на всички потребители",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
|
||||
"targetErrorNoSite": "Няма избран сайт",
|
||||
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
|
||||
"targetTargetsCleared": "Мишените са премахнати",
|
||||
"targetTargetsClearedDescription": "Всички цели са били премахнати от този ресурс",
|
||||
"targetCreated": "Целта е създадена",
|
||||
"targetCreatedDescription": "Целта беше успешно създадена",
|
||||
"targetErrorCreate": "Неуспешно създаване на целта",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Само за лична употреба (безплатен лиценз — без плащане)",
|
||||
"personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Продължете към плащане"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Машинни клиенти",
|
||||
"install": "Инсталирай",
|
||||
"run": "Изпълни",
|
||||
"envFile": "Файл за среда",
|
||||
"serviceFile": "Файл за услуга",
|
||||
"enableAndStart": "Активиране и стартиране",
|
||||
"clientNameDescription": "Показваното име на клиента, което може да се промени по-късно.",
|
||||
"clientAddress": "Клиентски адрес (Разширено)",
|
||||
"setupFailedToFetchSubnet": "Неуспешно извличане на подмрежа по подразбиране",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Без удостоверяване",
|
||||
"httpDestAuthNoneDescription": "Изпращане на заявки без заглавие за удостоверяване.",
|
||||
"httpDestAuthBearerTitle": "Bearer Токен",
|
||||
"httpDestAuthBearerDescription": "Добавя заглавие за удостоверяване Bearer <token> към всяка заявка.",
|
||||
"httpDestAuthBearerDescription": "Добавя заглавие Authorization: Bearer '<token>' към всяка заявка.",
|
||||
"httpDestAuthBearerPlaceholder": "Вашият API ключ или токен",
|
||||
"httpDestAuthBasicTitle": "Основно удостоверяване",
|
||||
"httpDestAuthBasicDescription": "Добавя заглавие за удостоверяване Basic <credentials> към всяка заявка. Осигурете идентификационни данни като потребителско име:парола.",
|
||||
"httpDestAuthBasicDescription": "Добавя заглавие Authorization: Basic '<credentials>'. Осигурете идентификационни данни като потребителско име:парола.",
|
||||
"httpDestAuthBasicPlaceholder": "потребителско име:парола",
|
||||
"httpDestAuthCustomTitle": "Персонализирано заглавие",
|
||||
"httpDestAuthCustomDescription": "Посочете персонализирано име и стойност на заглавието за удостоверяване (например X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Zajišťovací klíč byl aktualizován",
|
||||
"provisioningKeysUpdatedDescription": "Vaše změny byly uloženy.",
|
||||
"provisioningKeysBannerTitle": "Klíče pro poskytování webu",
|
||||
"provisioningKeysBannerDescription": "Vygenerujte konfigurační klíč a používejte jej pomocí nového konektoru k automatickému vytváření stránek při prvním startu – není třeba nastavovat samostatné přihlašovací údaje pro každý web.",
|
||||
"provisioningKeysBannerDescription": "Vygenerujte klíč pro zřízení a použijte ho s Newt konektorem k automatickému vytvoření stránek při prvním spuštění – není potřeba nastavit samostatné přihlašovací údaje pro každou stránku.",
|
||||
"provisioningKeysBannerButtonText": "Zjistit více",
|
||||
"pendingSitesBannerTitle": "Nevyřízené weby",
|
||||
"pendingSitesBannerDescription": "Zde se zobrazují stránky, které se připojují pomocí doplňovacího klíče. Schválte každý web předtím, než bude aktivní, a získejte přístup k vašim zdrojům.",
|
||||
"pendingSitesBannerDescription": "Stránky, které se připojují pomocí klíče pro zřízení, se zde objeví ke kontrole.",
|
||||
"pendingSitesBannerButtonText": "Zjistit více",
|
||||
"apiKeysSettings": "Nastavení {apiKeyName}",
|
||||
"userTitle": "Spravovat všechny uživatele",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Zadejte platné číslo portu",
|
||||
"targetErrorNoSite": "Není vybrán žádný web",
|
||||
"targetErrorNoSiteDescription": "Vyberte prosím web pro cíl",
|
||||
"targetTargetsCleared": "Cíle vymazány",
|
||||
"targetTargetsClearedDescription": "Všechny cíle byly odstraněny z tohoto zdroje",
|
||||
"targetCreated": "Cíl byl vytvořen",
|
||||
"targetCreatedDescription": "Cíl byl úspěšně vytvořen",
|
||||
"targetErrorCreate": "Nepodařilo se vytvořit cíl",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Podnikové funkce, 50 uživatelů, 50 míst a prioritní podpory."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Pouze osobní použití (bezplatná licence – bez platby)",
|
||||
"personalUseOnly": "Pouze pro osobní použití (zdarma licence - bez ověření)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Pokračovat do pokladny"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Strojoví klienti",
|
||||
"install": "Instalovat",
|
||||
"run": "Spustit",
|
||||
"envFile": "Konfigurační soubor prostředí",
|
||||
"serviceFile": "Služební soubor",
|
||||
"enableAndStart": "Povolit a spustit",
|
||||
"clientNameDescription": "Zobrazované jméno klienta, které lze později změnit.",
|
||||
"clientAddress": "Adresa klienta (Rozšířeno)",
|
||||
"setupFailedToFetchSubnet": "Nepodařilo se načíst výchozí podsíť",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Žádné ověření",
|
||||
"httpDestAuthNoneDescription": "Odešle žádosti bez záhlaví autorizace.",
|
||||
"httpDestAuthBearerTitle": "Token na doručitele",
|
||||
"httpDestAuthBearerDescription": "Přidá autorizaci: Hlavička Bearer <token> ke každému požadavku.",
|
||||
"httpDestAuthBearerDescription": "Přidává hlavičku Authorization: Bearer '<token>' k každému požadavku.",
|
||||
"httpDestAuthBearerPlaceholder": "Váš API klíč nebo token",
|
||||
"httpDestAuthBasicTitle": "Základní ověření",
|
||||
"httpDestAuthBasicDescription": "Přidá autorizaci: Základní <credentials> hlavička. Poskytněte přihlašovací údaje jako uživatelské jméno:password.",
|
||||
"httpDestAuthBasicDescription": "Přidává hlavičku Authorization: Basic '<credentials>'. Poskytněte přihlašovací údaje ve formátu uživatelské jméno:heslo.",
|
||||
"httpDestAuthBasicPlaceholder": "uživatelské jméno:heslo",
|
||||
"httpDestAuthCustomTitle": "Vlastní záhlaví",
|
||||
"httpDestAuthCustomDescription": "Zadejte název a hodnotu vlastního HTTP hlavičky pro ověření (např. X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert",
|
||||
"provisioningKeysUpdatedDescription": "Ihre Änderungen wurden gespeichert.",
|
||||
"provisioningKeysBannerTitle": "Website-Bereitstellungsschlüssel",
|
||||
"provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Konnektor, um beim ersten Start automatisch Sites zu erstellen – keine Notwendigkeit, separate Anmeldeinformationen für jede Seite einzurichten.",
|
||||
"provisioningKeysBannerDescription": "Generieren Sie einen Bereitstellungsschlüssel und verwenden Sie ihn mit dem Newt-Connector, um Standorte beim ersten Start automatisch zu erstellen - keine Notwendigkeit, separate Anmeldedaten für jede Seite einzurichten.",
|
||||
"provisioningKeysBannerButtonText": "Mehr erfahren",
|
||||
"pendingSitesBannerTitle": "Ausstehende Seiten",
|
||||
"pendingSitesBannerDescription": "Sites, die sich mit einem Bereitstellungsschlüssel verbinden, erscheinen hier zur Überprüfung. Bestätigen Sie jede Site, bevor sie aktiv wird und erhalten Zugriff auf Ihre Ressourcen.",
|
||||
"pendingSitesBannerDescription": "Websites, die mit einem Bereitstellungsschlüssel verbunden sind, erscheinen hier zur Überprüfung.",
|
||||
"pendingSitesBannerButtonText": "Mehr erfahren",
|
||||
"apiKeysSettings": "{apiKeyName} Einstellungen",
|
||||
"userTitle": "Alle Benutzer verwalten",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Bitte geben Sie eine gültige Portnummer ein",
|
||||
"targetErrorNoSite": "Kein Standort ausgewählt",
|
||||
"targetErrorNoSiteDescription": "Bitte wähle einen Standort für das Ziel aus",
|
||||
"targetTargetsCleared": "Ziele gelöscht",
|
||||
"targetTargetsClearedDescription": "Alle Ziele wurden aus dieser Ressource entfernt",
|
||||
"targetCreated": "Ziel erstellt",
|
||||
"targetCreatedDescription": "Ziel wurde erfolgreich erstellt",
|
||||
"targetErrorCreate": "Fehler beim Erstellen des Ziels",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Enterprise Features, 50 Benutzer, 50 Sites und Prioritätsunterstützung."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz — keine Kasse)",
|
||||
"personalUseOnly": "Nur persönliche Nutzung (kostenlose Lizenz - kein Checkout)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Weiter zur Kasse"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Maschinen-Clients",
|
||||
"install": "Installieren",
|
||||
"run": "Ausführen",
|
||||
"envFile": "Umgebungsdatei",
|
||||
"serviceFile": "Servicedatei",
|
||||
"enableAndStart": "Aktivieren und Starten",
|
||||
"clientNameDescription": "Der Anzeigename des Clients, der später geändert werden kann.",
|
||||
"clientAddress": "Clientadresse (Erweitert)",
|
||||
"setupFailedToFetchSubnet": "Fehler beim Abrufen des Standard-Subnetzes",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Keine Authentifizierung",
|
||||
"httpDestAuthNoneDescription": "Sendet Anfragen ohne Autorisierungs-Header.",
|
||||
"httpDestAuthBearerTitle": "Bären-Token",
|
||||
"httpDestAuthBearerDescription": "Fügt eine Berechtigung hinzu: Bearer <token> Header zu jeder Anfrage.",
|
||||
"httpDestAuthBearerDescription": "Fügt jedem Anfrage-Header eine \"Authorization: Bearer '<token>'\" hinzu.",
|
||||
"httpDestAuthBearerPlaceholder": "Ihr API-Schlüssel oder Token",
|
||||
"httpDestAuthBasicTitle": "Einfacher Auth",
|
||||
"httpDestAuthBasicDescription": "Fügt eine Autorisierung hinzu: Basic <credentials> Kopfzeile hinzu. Geben Sie Anmeldedaten als Benutzername:password an.",
|
||||
"httpDestAuthBasicDescription": "Fügt einen \"Authorization: Basic '<credentials>'\"-Header hinzu. Geben Sie die Anmeldedaten als Benutzername:Passwort an.",
|
||||
"httpDestAuthBasicPlaceholder": "benutzername:password",
|
||||
"httpDestAuthCustomTitle": "Eigene Kopfzeile",
|
||||
"httpDestAuthCustomDescription": "Geben Sie einen eigenen HTTP-Header-Namen und einen Wert für die Authentifizierung an (z.B. X-API-Key).",
|
||||
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
||||
"targetErrorNoSite": "No site selected",
|
||||
"targetErrorNoSiteDescription": "Please select a site for the target",
|
||||
"targetTargetsCleared": "Targets cleared",
|
||||
"targetTargetsClearedDescription": "All targets have been removed from this resource",
|
||||
"targetCreated": "Target created",
|
||||
"targetCreatedDescription": "Target has been created successfully",
|
||||
"targetErrorCreate": "Failed to create target",
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Machine Clients",
|
||||
"install": "Install",
|
||||
"run": "Run",
|
||||
"envFile": "Environment File",
|
||||
"serviceFile": "Service File",
|
||||
"enableAndStart": "Enable and Start",
|
||||
"clientNameDescription": "The display name of the client that can be changed later.",
|
||||
"clientAddress": "Client Address (Advanced)",
|
||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "No Authentication",
|
||||
"httpDestAuthNoneDescription": "Sends requests without an Authorization header.",
|
||||
"httpDestAuthBearerTitle": "Bearer Token",
|
||||
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer <token> header to each request.",
|
||||
"httpDestAuthBearerDescription": "Adds an Authorization: Bearer '<token>' header to each request.",
|
||||
"httpDestAuthBearerPlaceholder": "Your API key or token",
|
||||
"httpDestAuthBasicTitle": "Basic Auth",
|
||||
"httpDestAuthBasicDescription": "Adds an Authorization: Basic <credentials> header. Provide credentials as username:password.",
|
||||
"httpDestAuthBasicDescription": "Adds an Authorization: Basic '<credentials>' header. Provide credentials as username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "username:password",
|
||||
"httpDestAuthCustomTitle": "Custom Header",
|
||||
"httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Clave de aprovisionamiento actualizada",
|
||||
"provisioningKeysUpdatedDescription": "Sus cambios han sido guardados.",
|
||||
"provisioningKeysBannerTitle": "Claves de aprovisionamiento del sitio",
|
||||
"provisioningKeysBannerDescription": "Generar una clave de aprovisionamiento y usarla con el conector Newt para crear automáticamente sitios en el primer inicio — no es necesario configurar credenciales separadas para cada sitio.",
|
||||
"provisioningKeysBannerDescription": "Genere una clave de aprovisionamiento y utilícela con el conector Newt para crear automáticamente sitios en el primer inicio: no es necesario configurar credenciales separadas para cada sitio.",
|
||||
"provisioningKeysBannerButtonText": "Saber más",
|
||||
"pendingSitesBannerTitle": "Sitios pendientes",
|
||||
"pendingSitesBannerDescription": "Los sitios que se conectan usando una clave de aprovisionamiento aparecen aquí para su revisión. Aprobar cada sitio antes de que se active y obtenga acceso a sus recursos.",
|
||||
"pendingSitesBannerDescription": "Los sitios que se conectan utilizando una clave de aprovisionamiento aparecerán aquí para su revisión.",
|
||||
"pendingSitesBannerButtonText": "Saber más",
|
||||
"apiKeysSettings": "Ajustes {apiKeyName}",
|
||||
"userTitle": "Administrar todos los usuarios",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Por favor, introduzca un número de puerto válido",
|
||||
"targetErrorNoSite": "Ningún sitio seleccionado",
|
||||
"targetErrorNoSiteDescription": "Por favor, seleccione un sitio para el objetivo",
|
||||
"targetTargetsCleared": "Objetivos eliminados",
|
||||
"targetTargetsClearedDescription": "Todos los objetivos han sido eliminados de este recurso",
|
||||
"targetCreated": "Objetivo creado",
|
||||
"targetCreatedDescription": "El objetivo se ha creado correctamente",
|
||||
"targetErrorCreate": "Error al crear el objetivo",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Características de la empresa, 50 usuarios, 50 sitios y soporte prioritario."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Solo uso personal (licencia gratuita, sin pago)",
|
||||
"personalUseOnly": "Solo uso personal (licencia gratuita - sin salida)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continuar con el pago"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Clientes de la máquina",
|
||||
"install": "Instalar",
|
||||
"run": "Ejecutar",
|
||||
"envFile": "Archivo de Entorno",
|
||||
"serviceFile": "Archivo de Servicio",
|
||||
"enableAndStart": "Habilitar y empezar",
|
||||
"clientNameDescription": "El nombre mostrado del cliente que se puede cambiar más adelante.",
|
||||
"clientAddress": "Dirección del cliente (Avanzado)",
|
||||
"setupFailedToFetchSubnet": "No se pudo obtener la subred por defecto",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Sin autenticación",
|
||||
"httpDestAuthNoneDescription": "Envía solicitudes sin un encabezado de autorización.",
|
||||
"httpDestAuthBearerTitle": "Tóken de portador",
|
||||
"httpDestAuthBearerDescription": "Añade una autorización: portador <token> encabezado a cada solicitud.",
|
||||
"httpDestAuthBearerDescription": "Añade un encabezado Authorization: Bearer '<token>' a cada solicitud.",
|
||||
"httpDestAuthBearerPlaceholder": "Tu clave o token API",
|
||||
"httpDestAuthBasicTitle": "Auth Básica",
|
||||
"httpDestAuthBasicDescription": "Añade una Autorización: encabezado básico <credentials> . Proporcione credenciales como nombre de usuario: contraseña.",
|
||||
"httpDestAuthBasicDescription": "Añade un encabezado Authorization: Basic '<credenciales>'. Proporcione las credenciales como nombredeusuario:contraseña.",
|
||||
"httpDestAuthBasicPlaceholder": "usuario:contraseña",
|
||||
"httpDestAuthCustomTitle": "Cabecera personalizada",
|
||||
"httpDestAuthCustomDescription": "Especifique un nombre de cabecera HTTP personalizado y un valor para la autenticación (por ejemplo, X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Clé de provisioning mise à jour",
|
||||
"provisioningKeysUpdatedDescription": "Vos modifications ont été enregistrées.",
|
||||
"provisioningKeysBannerTitle": "Clés de provisioning du site",
|
||||
"provisioningKeysBannerDescription": "Générez une clé de provisioning et utilisez-la avec le connecteur Newt pour créer automatiquement des sites au premier démarrage — pas besoin de configurer des identifiants distincts pour chaque site.",
|
||||
"provisioningKeysBannerDescription": "Générez une clé de provisionnement et utilisez-la avec le connecteur Newt pour créer automatiquement des sites lors du premier démarrage - sans besoin de configurer des identifiants séparés pour chaque site.",
|
||||
"provisioningKeysBannerButtonText": "En savoir plus",
|
||||
"pendingSitesBannerTitle": "Sites en attente",
|
||||
"pendingSitesBannerDescription": "Les sites qui se connectent à l'aide d'une clé de provisioning apparaissent ici pour être revus. Approuver chaque site avant qu'il ne devienne actif et qu'il accède à vos ressources.",
|
||||
"pendingSitesBannerDescription": "Les sites qui se connectent en utilisant une clé de provisionnement apparaissent ici pour révision.",
|
||||
"pendingSitesBannerButtonText": "En savoir plus",
|
||||
"apiKeysSettings": "Paramètres de {apiKeyName}",
|
||||
"userTitle": "Gérer tous les utilisateurs",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Veuillez entrer un numéro de port valide",
|
||||
"targetErrorNoSite": "Aucun site sélectionné",
|
||||
"targetErrorNoSiteDescription": "Veuillez sélectionner un site pour la cible",
|
||||
"targetTargetsCleared": "Cibles effacées",
|
||||
"targetTargetsClearedDescription": "Toutes les cibles ont été retirées de cette ressource",
|
||||
"targetCreated": "Cible créée",
|
||||
"targetCreatedDescription": "La cible a été créée avec succès",
|
||||
"targetErrorCreate": "Impossible de créer la cible",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Fonctionnalités d'entreprise, 50 utilisateurs, 50 sites et une prise en charge prioritaire."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Utilisation personnelle uniquement (licence gratuite — sans checkout)",
|
||||
"personalUseOnly": "Usage personnel uniquement (licence gratuite - pas de validation)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continuer vers le paiement"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Clients Machines",
|
||||
"install": "Installer",
|
||||
"run": "Exécuter",
|
||||
"envFile": "Fichier Environnement",
|
||||
"serviceFile": "Fichier de Service",
|
||||
"enableAndStart": "Activer et Démarrer",
|
||||
"clientNameDescription": "Le nom d'affichage du client qui peut être modifié plus tard.",
|
||||
"clientAddress": "Adresse du client (Avancé)",
|
||||
"setupFailedToFetchSubnet": "Impossible de récupérer le sous-réseau par défaut",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Aucune authentification",
|
||||
"httpDestAuthNoneDescription": "Envoie des requêtes sans en-tête d'autorisation.",
|
||||
"httpDestAuthBearerTitle": "Jeton de Porteur",
|
||||
"httpDestAuthBearerDescription": "Ajoute un en-tête Authorization: Bearer <token> à chaque requête.",
|
||||
"httpDestAuthBearerDescription": "Ajoute un en-tête Authorization: Bearer '<token>' à chaque requête.",
|
||||
"httpDestAuthBearerPlaceholder": "Votre clé API ou votre jeton",
|
||||
"httpDestAuthBasicTitle": "Authentification basique",
|
||||
"httpDestAuthBasicDescription": "Ajoute une autorisation : en-tête de base <credentials> . Fournissez des informations d'identification comme nom d'utilisateur:mot de passe.",
|
||||
"httpDestAuthBasicDescription": "Ajoute un en-tête Authorization: Basic '<credentials>'. Fournissez les identifiants sous la forme nom d'utilisateur:mot de passe.",
|
||||
"httpDestAuthBasicPlaceholder": "nom d'utilisateur:mot de passe",
|
||||
"httpDestAuthCustomTitle": "En-tête personnalisé",
|
||||
"httpDestAuthCustomDescription": "Spécifiez un nom d'en-tête HTTP personnalisé et une valeur pour l'authentification (par exemple X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Chiave di accantonamento aggiornata",
|
||||
"provisioningKeysUpdatedDescription": "Le tue modifiche sono state salvate.",
|
||||
"provisioningKeysBannerTitle": "Chiavi Di Provvedimento Sito",
|
||||
"provisioningKeysBannerDescription": "Generare una chiave di provisioning e usarla con il connettore Newt per creare automaticamente siti al primo avvio — non è necessario impostare credenziali separate per ogni sito.",
|
||||
"provisioningKeysBannerDescription": "Genera una chiave di provisioning e usala con il connettore Newt per creare automaticamente i siti al primo avvio - non è necessario configurare credenziali separate per ogni sito.",
|
||||
"provisioningKeysBannerButtonText": "Scopri di più",
|
||||
"pendingSitesBannerTitle": "Siti In Attesa",
|
||||
"pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning appaiono qui per la revisione. Approva ogni sito prima che diventi attivo e ottenga l'accesso alle tue risorse.",
|
||||
"pendingSitesBannerDescription": "I siti che si connettono utilizzando una chiave di provisioning vengono visualizzati qui per la revisione.",
|
||||
"pendingSitesBannerButtonText": "Scopri di più",
|
||||
"apiKeysSettings": "Impostazioni {apiKeyName}",
|
||||
"userTitle": "Gestisci Tutti Gli Utenti",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Inserisci un numero di porta valido",
|
||||
"targetErrorNoSite": "Nessun sito selezionato",
|
||||
"targetErrorNoSiteDescription": "Si prega di selezionare un sito per l'obiettivo",
|
||||
"targetTargetsCleared": "Obiettivi cancellati",
|
||||
"targetTargetsClearedDescription": "Tutti gli obiettivi sono stati rimossi da questa risorsa",
|
||||
"targetCreated": "Destinazione creata",
|
||||
"targetCreatedDescription": "L'obiettivo è stato creato con successo",
|
||||
"targetErrorCreate": "Impossibile creare l'obiettivo",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Funzionalità aziendali, 50 utenti, 50 siti e supporto prioritario."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Solo uso personale (licenza gratuita — nessun checkout)",
|
||||
"personalUseOnly": "Uso personale esclusivo (licenza gratuita - nessun pagamento)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continua al Checkout"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Machine Clients",
|
||||
"install": "Installa",
|
||||
"run": "Esegui",
|
||||
"envFile": "File di ambiente",
|
||||
"serviceFile": "File di servizio",
|
||||
"enableAndStart": "Abilita e avvia",
|
||||
"clientNameDescription": "Il nome visualizzato del client che può essere modificato in seguito.",
|
||||
"clientAddress": "Indirizzo Client (Avanzato)",
|
||||
"setupFailedToFetchSubnet": "Recupero della sottorete predefinita non riuscito",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Nessuna Autenticazione",
|
||||
"httpDestAuthNoneDescription": "Invia richieste senza intestazione autorizzazione.",
|
||||
"httpDestAuthBearerTitle": "Token Del Portatore",
|
||||
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Autorizzazione: Bearer <token> ad ogni richiesta.",
|
||||
"httpDestAuthBearerDescription": "Aggiunge un'intestazione Authorization: Bearer '<token>' a ogni richiesta.",
|
||||
"httpDestAuthBearerPlaceholder": "La tua chiave API o token",
|
||||
"httpDestAuthBasicTitle": "Autenticazione Base",
|
||||
"httpDestAuthBasicDescription": "Aggiunge un'autorizzazione: intestazione di base <credentials> . Fornisce le credenziali come username:password.",
|
||||
"httpDestAuthBasicDescription": "Aggiunge un'intestazione Authorization: Basic '<credentials>'. Fornire le credenziali come username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "username:password",
|
||||
"httpDestAuthCustomTitle": "Intestazione Personalizzata",
|
||||
"httpDestAuthCustomDescription": "Specifica un nome e un valore di intestazione HTTP personalizzati per l'autenticazione (ad esempio X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "프로비저닝 키가 업데이트되었습니다",
|
||||
"provisioningKeysUpdatedDescription": "변경 사항이 저장되었습니다.",
|
||||
"provisioningKeysBannerTitle": "사이트 프로비저닝 키",
|
||||
"provisioningKeysBannerDescription": "프로비저닝 키를 생성하여 Newt 커넥터와 함께 사용해 첫 실행 시 자동으로 사이트를 생성하세요 — 각 사이트마다 별도의 인증을 설정할 필요가 없습니다.",
|
||||
"provisioningKeysBannerDescription": "프로비저닝 키를 생성하고 Newt 커넥터와 함께 사용하여 첫 시작 시 사이트를 자동 생성 - 각 사이트에 대한 별도 자격 증명이 필요 없습니다.",
|
||||
"provisioningKeysBannerButtonText": "자세히 알아보기",
|
||||
"pendingSitesBannerTitle": "대기중인 사이트",
|
||||
"pendingSitesBannerDescription": "프로비저닝 키를 사용하여 연결하는 사이트는 검토 대기 중입니다. 사이트가 활성화되어 리소스에 액세스하기 전에 각 사이트를 승인하세요.",
|
||||
"pendingSitesBannerDescription": "프로비저닝 키를 사용하여 연결된 사이트가 검토를 위해 여기에 표시됩니다.",
|
||||
"pendingSitesBannerButtonText": "자세히 알아보기",
|
||||
"apiKeysSettings": "{apiKeyName} 설정",
|
||||
"userTitle": "모든 사용자 관리",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "유효한 포트 번호를 입력하세요.",
|
||||
"targetErrorNoSite": "선택된 사이트 없음",
|
||||
"targetErrorNoSiteDescription": "대상을 위해 사이트를 선택하세요.",
|
||||
"targetTargetsCleared": "대상이 제거됨",
|
||||
"targetTargetsClearedDescription": "이 리소스에서 모든 대상이 제거되었습니다",
|
||||
"targetCreated": "대상 생성",
|
||||
"targetCreatedDescription": "대상이 성공적으로 생성되었습니다.",
|
||||
"targetErrorCreate": "대상 생성 실패",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "기업 기능, 50명의 사용자, 50개의 사이트, 우선 지원."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "개인 사용 전용 (무료 라이센스 — 체크아웃 없음)",
|
||||
"personalUseOnly": "개인용으로만 사용 (무료 라이선스 - 결제 없음)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "결제로 진행"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "기계 클라이언트",
|
||||
"install": "설치",
|
||||
"run": "실행",
|
||||
"envFile": "환경 파일",
|
||||
"serviceFile": "서비스 파일",
|
||||
"enableAndStart": "활성화 및 시작",
|
||||
"clientNameDescription": "나중에 변경할 수 있는 클라이언트의 표시 이름입니다.",
|
||||
"clientAddress": "클라이언트 주소(고급)",
|
||||
"setupFailedToFetchSubnet": "기본값 로드 실패",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "인증 없음",
|
||||
"httpDestAuthNoneDescription": "Authorization 헤더 없이 요청을 보냅니다.",
|
||||
"httpDestAuthBearerTitle": "Bearer 토큰",
|
||||
"httpDestAuthBearerDescription": "모든 요청에 Authorization: Bearer <token> 헤더를 추가합니다.",
|
||||
"httpDestAuthBearerDescription": "각 요청에 Authorization: Bearer '<token>' 헤더를 추가합니다.",
|
||||
"httpDestAuthBearerPlaceholder": "API 키 또는 토큰",
|
||||
"httpDestAuthBasicTitle": "기본 인증",
|
||||
"httpDestAuthBasicDescription": "Authorization: Basic <credentials> 헤더를 추가합니다. 자격 증명은 username:password 형식으로 제공하세요.",
|
||||
"httpDestAuthBasicDescription": "Authorization: Basic '<credentials>' 헤더를 추가합니다. 자격 증명은 사용자 이름:비밀번호로 제공합니다.",
|
||||
"httpDestAuthBasicPlaceholder": "사용자 이름:비밀번호",
|
||||
"httpDestAuthCustomTitle": "사용자 정의 헤더",
|
||||
"httpDestAuthCustomDescription": "인증을 위한 사용자 정의 HTTP 헤더 이름 및 값을 지정하세요 (예: X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Foreslå nøkkel oppdatert",
|
||||
"provisioningKeysUpdatedDescription": "Dine endringer er lagret.",
|
||||
"provisioningKeysBannerTitle": "Sidens bestemmende nøkler",
|
||||
"provisioningKeysBannerDescription": "Generer en foreløpig nøkkel og bruk den med Nyhetskontakten for å automatisk opprette sider ved første oppstart — trenger ikke å sette opp separat innloggingsinformasjon for hver side.",
|
||||
"provisioningKeysBannerDescription": "Generer en provisjonsnøkkel og bruk den med Newt-kontakten for automatisk opprettelse av nettsteder ved første oppstart - ingen behov for å sette opp separate legitimasjoner for hvert nettsted.",
|
||||
"provisioningKeysBannerButtonText": "Lær mer",
|
||||
"pendingSitesBannerTitle": "Ventende nettsteder",
|
||||
"pendingSitesBannerDescription": "Nettsteder som kobler deg til ved hjelp av en bestemmelsestekst, vises her for gjennomgang. Godkjenn hvert nettsted før det blir aktivt og får tilgang til ressursene dine.",
|
||||
"pendingSitesBannerDescription": "Nettsteder som kobler seg til ved bruk av en provisjonsnøkkel vises her for vurdering.",
|
||||
"pendingSitesBannerButtonText": "Lær mer",
|
||||
"apiKeysSettings": "{apiKeyName} Innstillinger",
|
||||
"userTitle": "Administrer alle brukere",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Vennligst skriv inn et gyldig portnummer",
|
||||
"targetErrorNoSite": "Ingen nettsted valgt",
|
||||
"targetErrorNoSiteDescription": "Velg et nettsted for målet",
|
||||
"targetTargetsCleared": "Mål ryddet",
|
||||
"targetTargetsClearedDescription": "Alle mål har blitt fjernet fra denne ressursen",
|
||||
"targetCreated": "Mål opprettet",
|
||||
"targetCreatedDescription": "Målet har blitt opprettet",
|
||||
"targetErrorCreate": "Kunne ikke opprette målet",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Enterprise features, 50 brukere, 50 nettsteder og prioritetsstøtte."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen utsjekking)",
|
||||
"personalUseOnly": "Kun personlig bruk (gratis lisens - ingen kasse)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Fortsett til kassen"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Maskinklienter",
|
||||
"install": "Installer",
|
||||
"run": "Kjør",
|
||||
"envFile": "Miljøfil",
|
||||
"serviceFile": "Tjenestefil",
|
||||
"enableAndStart": "Aktiver og start",
|
||||
"clientNameDescription": "Visningsnavnet til klienten som kan endres senere.",
|
||||
"clientAddress": "Klientadresse (avansert)",
|
||||
"setupFailedToFetchSubnet": "Kunne ikke hente standard undernett",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Ingen godkjenning",
|
||||
"httpDestAuthNoneDescription": "Sender forespørsler uten autorisasjonsoverskrift.",
|
||||
"httpDestAuthBearerTitle": "Bærer Symbol",
|
||||
"httpDestAuthBearerDescription": "Legger til en autorisasjon: Bearer <token> header til hver forespørsel.",
|
||||
"httpDestAuthBearerDescription": "Legger til en Autorisasjon: Bearer '<token>' header til hver forespørsel.",
|
||||
"httpDestAuthBearerPlaceholder": "Din API-nøkkel eller token",
|
||||
"httpDestAuthBasicTitle": "Standard Auth",
|
||||
"httpDestAuthBasicDescription": "Legger til en godkjenning: Grunnleggende <credentials> overskrift. Angi legitimasjon som brukernavn:passord.",
|
||||
"httpDestAuthBasicDescription": "Legger til en Autorisasjon: Basic '<credentials>' header. Gi legitimasjon som brukernavn:passord.",
|
||||
"httpDestAuthBasicPlaceholder": "brukernavn:passord",
|
||||
"httpDestAuthCustomTitle": "Egendefinert topptekst",
|
||||
"httpDestAuthCustomDescription": "Angi et egendefinert HTTP headers navn og verdi for autentisering (f.eks X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Provisie sleutel bijgewerkt",
|
||||
"provisioningKeysUpdatedDescription": "Uw wijzigingen zijn opgeslagen.",
|
||||
"provisioningKeysBannerTitle": "Bewerkingssleutels voor websites",
|
||||
"provisioningKeysBannerDescription": "Genereer een provisioning-sleutel en gebruik deze met de Newt-connector om automatisch sites aan te maken bij het opstarten van de eerste opstart- het is niet nodig om afzonderlijke inloggegevens in te stellen voor elke site.",
|
||||
"provisioningKeysBannerDescription": "Genereer een inrichtingssleutel en gebruik deze met de Newt-connector om automatisch sites te maken bij de eerste opstart - er is geen behoefte om aparte inloggegevens voor elke site in te stellen.",
|
||||
"provisioningKeysBannerButtonText": "Meer informatie",
|
||||
"pendingSitesBannerTitle": "Openstaande sites",
|
||||
"pendingSitesBannerDescription": "Sites die met elkaar verbinden met behulp van een provisioning-sleutel verschijnen hier voor beoordeling. Accepteer elke site voordat deze actief wordt en krijgt toegang tot uw bronnen.",
|
||||
"pendingSitesBannerDescription": "Sites die verbinding maken met een inrichtingssleutel verschijnen hier voor beoordeling.",
|
||||
"pendingSitesBannerButtonText": "Meer informatie",
|
||||
"apiKeysSettings": "{apiKeyName} instellingen",
|
||||
"userTitle": "Alle gebruikers beheren",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Voer een geldig poortnummer in",
|
||||
"targetErrorNoSite": "Geen site geselecteerd",
|
||||
"targetErrorNoSiteDescription": "Selecteer een site voor het doel",
|
||||
"targetTargetsCleared": "Doelen gewist",
|
||||
"targetTargetsClearedDescription": "Alle doelen zijn verwijderd van deze bron",
|
||||
"targetCreated": "Doel aangemaakt",
|
||||
"targetCreatedDescription": "Doel is succesvol aangemaakt",
|
||||
"targetErrorCreate": "Kan doel niet aanmaken",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Enterprise functies, 50 gebruikers, 50 sites en prioriteit ondersteuning."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Alleen persoonlijk gebruik (gratis licentie - geen afrekenen)",
|
||||
"personalUseOnly": "Alleen voor persoonlijk gebruik (gratis licentie - geen afrekening)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Doorgaan naar afrekenen"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Machine Clienten",
|
||||
"install": "Installeren",
|
||||
"run": "Uitvoeren",
|
||||
"envFile": "Omgevingsbestand",
|
||||
"serviceFile": "Servicebestand",
|
||||
"enableAndStart": "Inschakelen en Starten",
|
||||
"clientNameDescription": "De weergavenaam van de client die later gewijzigd kan worden.",
|
||||
"clientAddress": "Klant adres (Geavanceerd)",
|
||||
"setupFailedToFetchSubnet": "Kan standaard subnet niet ophalen",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Geen authenticatie",
|
||||
"httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.",
|
||||
"httpDestAuthBearerTitle": "Betere Token",
|
||||
"httpDestAuthBearerDescription": "Voegt een machtiging toe: Drager <token> header aan elke aanvraag.",
|
||||
"httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '<token>' header toe aan elk verzoek.",
|
||||
"httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token",
|
||||
"httpDestAuthBasicTitle": "Basis authenticatie",
|
||||
"httpDestAuthBasicDescription": "Voegt een Authorizatie toe: Basis <credentials> kop. Geef inloggegevens op als gebruikersnaam:wachtwoord.",
|
||||
"httpDestAuthBasicDescription": "Voegt een Authorization: Basic '<credentials>' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.",
|
||||
"httpDestAuthBasicPlaceholder": "Gebruikersnaam:wachtwoord",
|
||||
"httpDestAuthCustomTitle": "Aangepaste koptekst",
|
||||
"httpDestAuthCustomDescription": "Specificeer een aangepaste HTTP header naam en waarde voor authenticatie (bijv. X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Klucz zaopatrzenia zaktualizowany",
|
||||
"provisioningKeysUpdatedDescription": "Twoje zmiany zostały zapisane.",
|
||||
"provisioningKeysBannerTitle": "Klucze Zaopatrzenia witryny",
|
||||
"provisioningKeysBannerDescription": "Wygeneruj klucz tworzenia rezerw i użyj go z konektorem Newt do automatycznego tworzenia witryn przy pierwszym uruchomieniu — nie ma potrzeby ustawiania oddzielnych poświadczeń dla każdej witryny.",
|
||||
"provisioningKeysBannerDescription": "Wygeneruj klucz provisioning i użyj go z konektorem Newt do automatycznego tworzenia witryn przy pierwszym uruchomieniu - nie ma potrzeby konfigurowania oddzielnych poświadczeń dla każdej witryny.",
|
||||
"provisioningKeysBannerButtonText": "Dowiedz się więcej",
|
||||
"pendingSitesBannerTitle": "Witryny oczekujące",
|
||||
"pendingSitesBannerDescription": "Witryny, które łączą się przy użyciu klucza zaopatrzenia, pojawiają się tutaj, aby przejrzeć. Zatwierdź każdą witrynę, zanim stanie się aktywna i uzyska dostęp do twoich zasobów.",
|
||||
"pendingSitesBannerDescription": "Witryny, które łączą się za pomocą klucza provisioning, pojawią się tutaj do przeglądu.",
|
||||
"pendingSitesBannerButtonText": "Dowiedz się więcej",
|
||||
"apiKeysSettings": "Ustawienia {apiKeyName}",
|
||||
"userTitle": "Zarządzaj wszystkimi użytkownikami",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Wprowadź prawidłowy numer portu",
|
||||
"targetErrorNoSite": "Nie wybrano witryny",
|
||||
"targetErrorNoSiteDescription": "Wybierz witrynę docelową",
|
||||
"targetTargetsCleared": "Cele wyczyszczone",
|
||||
"targetTargetsClearedDescription": "Wszystkie cele zostały usunięte z tego zasobu",
|
||||
"targetCreated": "Cel utworzony",
|
||||
"targetCreatedDescription": "Cel został utworzony pomyślnie",
|
||||
"targetErrorCreate": "Nie udało się utworzyć celu",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Cechy przedsiębiorstw, 50 użytkowników, 50 obiektów i wsparcie priorytetowe."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Wyłącznie do użytku osobistego (bezpłatna licencja – brak zamówień)",
|
||||
"personalUseOnly": "Tylko do użytku osobistego (darmowa licencja - bez płatności)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Przejdź do zamówienia"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Klienci maszyn",
|
||||
"install": "Zainstaluj",
|
||||
"run": "Uruchom",
|
||||
"envFile": "Plik środowiska",
|
||||
"serviceFile": "Plik serwisu",
|
||||
"enableAndStart": "Włącz i Uruchom",
|
||||
"clientNameDescription": "Wyświetlana nazwa klienta, która może zostać zmieniona później.",
|
||||
"clientAddress": "Adres klienta (Zaawansowany)",
|
||||
"setupFailedToFetchSubnet": "Nie udało się pobrać domyślnej podsieci",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Brak uwierzytelniania",
|
||||
"httpDestAuthNoneDescription": "Wysyła żądania bez nagłówka autoryzacji.",
|
||||
"httpDestAuthBearerTitle": "Token Bearer",
|
||||
"httpDestAuthBearerDescription": "Dodaje autoryzację: nagłówek Bearer <token> do każdego żądania.",
|
||||
"httpDestAuthBearerDescription": "Dodaje nagłówek Authorization: Bearer '<token>' do każdego żądania.",
|
||||
"httpDestAuthBearerPlaceholder": "Twój klucz API lub token",
|
||||
"httpDestAuthBasicTitle": "Podstawowa Autoryzacja",
|
||||
"httpDestAuthBasicDescription": "Dodaje Autoryzacja: Nagłówek Basic <credentials> . Podaj poświadczenia jako nazwę użytkownika: hasło.",
|
||||
"httpDestAuthBasicDescription": "Dodaje nagłówek Authorization: Basic '<credentials>'. Podaj poświadczenia w formacie użytkownik:hasło.",
|
||||
"httpDestAuthBasicPlaceholder": "Nazwa użytkownika:hasło",
|
||||
"httpDestAuthCustomTitle": "Niestandardowy nagłówek",
|
||||
"httpDestAuthCustomDescription": "Określ niestandardową nazwę nagłówka HTTP i wartość dla uwierzytelniania (np. X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Chave de provisionamento atualizada",
|
||||
"provisioningKeysUpdatedDescription": "Suas alterações foram salvas.",
|
||||
"provisioningKeysBannerTitle": "Chaves de provisionamento do site",
|
||||
"provisioningKeysBannerDescription": "Gerar uma chave de provisionamento e usá-la com o conector de Newt para criar automaticamente sites na primeira inicialização — não é necessário configurar credenciais separadas para cada site.",
|
||||
"provisioningKeysBannerDescription": "Gere uma chave de provisionamento e use-a com o conector Newt para criar sites automaticamente na primeira inicialização - sem necessidade de configurar credenciais separadas para cada site.",
|
||||
"provisioningKeysBannerButtonText": "Saiba mais",
|
||||
"pendingSitesBannerTitle": "Sites pendentes",
|
||||
"pendingSitesBannerDescription": "Sites que conectam usando uma chave de provisionamento aparecem aqui para revisão. Aprovar cada site antes de se tornar ativo e ganhar acesso a seus recursos.",
|
||||
"pendingSitesBannerDescription": "Sites que se conectam usando uma chave de provisionamento aparecem aqui para revisão.",
|
||||
"pendingSitesBannerButtonText": "Saiba mais",
|
||||
"apiKeysSettings": "Configurações de {apiKeyName}",
|
||||
"userTitle": "Gerir Todos os Utilizadores",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Por favor, digite um número de porta válido",
|
||||
"targetErrorNoSite": "Nenhum site selecionado",
|
||||
"targetErrorNoSiteDescription": "Selecione um site para o destino",
|
||||
"targetTargetsCleared": "Alvos limpos",
|
||||
"targetTargetsClearedDescription": "Todos os alvos foram removidos deste recurso",
|
||||
"targetCreated": "Destino criado",
|
||||
"targetCreatedDescription": "O alvo foi criado com sucesso",
|
||||
"targetErrorCreate": "Falha ao criar destino",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Recursos de empresa, 50 usuários, 50 sites e apoio prioritário."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Apenas uso pessoal (licença gratuita — sem check-out)",
|
||||
"personalUseOnly": "Uso pessoal apenas (licença gratuita - sem checkout)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Continuar com checkout"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Clientes de máquina",
|
||||
"install": "Instale",
|
||||
"run": "Executar",
|
||||
"envFile": "Arquivo de Ambiente",
|
||||
"serviceFile": "Arquivo de Serviço",
|
||||
"enableAndStart": "Ativar e Iniciar",
|
||||
"clientNameDescription": "O nome de exibição do cliente que pode ser alterado mais tarde.",
|
||||
"clientAddress": "Endereço do Cliente (Avançado)",
|
||||
"setupFailedToFetchSubnet": "Falha ao buscar a subrede padrão",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Sem Autenticação",
|
||||
"httpDestAuthNoneDescription": "Envia pedidos sem um cabeçalho de autorização.",
|
||||
"httpDestAuthBearerTitle": "Token do portador",
|
||||
"httpDestAuthBearerDescription": "Adiciona uma autorização: Bearer <token> header a cada requisição.",
|
||||
"httpDestAuthBearerDescription": "Adiciona um cabeçalho Authorization: Bearer '<token>' a cada solicitação.",
|
||||
"httpDestAuthBearerPlaceholder": "Sua chave de API ou token",
|
||||
"httpDestAuthBasicTitle": "Autenticação básica",
|
||||
"httpDestAuthBasicDescription": "Adiciona uma Autorização: cabeçalho <credentials> básico. Forneça credenciais como nome de usuário:senha.",
|
||||
"httpDestAuthBasicDescription": "Adiciona um cabeçalho Authorization: Basic '<credentials>'. Forneça as credenciais como username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "Usuário:password",
|
||||
"httpDestAuthCustomTitle": "Cabeçalho personalizado",
|
||||
"httpDestAuthCustomDescription": "Especifique um nome e valor de cabeçalho HTTP personalizado para autenticação (por exemplo, X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Ключ подготовки обновлен",
|
||||
"provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.",
|
||||
"provisioningKeysBannerTitle": "Ключи подготовки сайта",
|
||||
"provisioningKeysBannerDescription": "Генерировать подготовительный ключ и использовать его вместе с Новым коннектором для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
||||
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
||||
"provisioningKeysBannerButtonText": "Узнать больше",
|
||||
"pendingSitesBannerTitle": "Ожидающие сайты",
|
||||
"pendingSitesBannerDescription": "Сайты, связанные с использованием ключа подготовки, появляются здесь для проверки. Одобрите каждый сайт, прежде чем он станет активным и получит доступ к вашим ресурсам.",
|
||||
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
|
||||
"pendingSitesBannerButtonText": "Узнать больше",
|
||||
"apiKeysSettings": "Настройки {apiKeyName}",
|
||||
"userTitle": "Управление всеми пользователями",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Пожалуйста, введите правильный номер порта",
|
||||
"targetErrorNoSite": "Сайт не выбран",
|
||||
"targetErrorNoSiteDescription": "Пожалуйста, выберите сайт для цели",
|
||||
"targetTargetsCleared": "Цели очищены",
|
||||
"targetTargetsClearedDescription": "Все цели удалены из этого ресурса",
|
||||
"targetCreated": "Цель создана",
|
||||
"targetCreatedDescription": "Цель была успешно создана",
|
||||
"targetErrorCreate": "Не удалось создать цель",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Функции предприятия, 50 пользователей, 50 сайтов, а также приоритетная поддержка."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Только для личного пользования (бесплатная лицензия — без оформления)",
|
||||
"personalUseOnly": "Только для личного использования (бесплатная лицензия - без оформления на кассе)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Продолжить оформление заказа"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Машинные клиенты",
|
||||
"install": "Установить",
|
||||
"run": "Запустить",
|
||||
"envFile": "Файл окружения",
|
||||
"serviceFile": "Сервисный файл",
|
||||
"enableAndStart": "Включить и запустить",
|
||||
"clientNameDescription": "Отображаемое имя клиента, которое может быть изменено позже.",
|
||||
"clientAddress": "Адрес клиента (Дополнительно)",
|
||||
"setupFailedToFetchSubnet": "Не удалось получить подсеть по умолчанию",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Нет аутентификации",
|
||||
"httpDestAuthNoneDescription": "Отправляет запросы без заголовка авторизации.",
|
||||
"httpDestAuthBearerTitle": "Жетон носителя",
|
||||
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer <token> к каждому запросу.",
|
||||
"httpDestAuthBearerDescription": "Добавляет заголовок Authorization: Bearer '<token>' к каждому запросу.",
|
||||
"httpDestAuthBearerPlaceholder": "Ваш ключ API или токен",
|
||||
"httpDestAuthBasicTitle": "Базовая авторизация",
|
||||
"httpDestAuthBasicDescription": "Добавляет Authorization: Basic <credentials> header. Предоставьте учетные данные в качестве имени пользователя:password.",
|
||||
"httpDestAuthBasicDescription": "Добавляет заголовок Authorization: Basic '<credentials>'. Укажите учетные данные в формате username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "имя пользователя:пароль",
|
||||
"httpDestAuthCustomTitle": "Пользовательский заголовок",
|
||||
"httpDestAuthCustomDescription": "Укажите пользовательское имя заголовка HTTP и значение для аутентификации (например, X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "Tedarik anahtarı güncellendi",
|
||||
"provisioningKeysUpdatedDescription": "Değişiklikleriniz kaydedildi.",
|
||||
"provisioningKeysBannerTitle": "Site Tedarik Anahtarları",
|
||||
"provisioningKeysBannerDescription": "Tedarik anahtarı oluşturun ve ilk başlangıçta siteleri otomatik olarak oluşturmak için Newt konektörüyle kullanın — her site için ayrı kimlik bilgileri ayarlamaya gerek yoktur.",
|
||||
"provisioningKeysBannerDescription": "Bir sağlama anahtarı oluşturun ve ilk başlangıçta siteleri otomatik olarak oluşturmak için Newt bağlayıcısını kullanın - her site için ayrı kimlik bilgileri ayarlamaya gerek yok.",
|
||||
"provisioningKeysBannerButtonText": "Daha fazla bilgi",
|
||||
"pendingSitesBannerTitle": "Bekleyen Siteler",
|
||||
"pendingSitesBannerDescription": "Tedarik anahtarı kullanarak bağlanan siteler burada incelenmek için görünür. Aktif hale gelmeden ve kaynaklarınıza erişim kazanmadan önce her siteyi onaylayın.",
|
||||
"pendingSitesBannerDescription": "Bir sağlama anahtarı kullanarak bağlanan siteler, inceleme için burada görünür.",
|
||||
"pendingSitesBannerButtonText": "Daha fazla bilgi",
|
||||
"apiKeysSettings": "{apiKeyName} Ayarları",
|
||||
"userTitle": "Tüm Kullanıcıları Yönet",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "Lütfen geçerli bir port numarası girin",
|
||||
"targetErrorNoSite": "Hiçbir site seçili değil",
|
||||
"targetErrorNoSiteDescription": "Lütfen hedef için bir site seçin",
|
||||
"targetTargetsCleared": "Hedefler temizlendi",
|
||||
"targetTargetsClearedDescription": "Bu kaynaktan tüm hedefler kaldırıldı",
|
||||
"targetCreated": "Hedef oluşturuldu",
|
||||
"targetCreatedDescription": "Hedef başarıyla oluşturuldu",
|
||||
"targetErrorCreate": "Hedef oluşturma başarısız oldu",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "Kurumsal özellikler, 50 kullanıcı, 50 site ve öncelikli destek."
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "Yalnızca kişisel kullanım (ücretsiz lisans — ödeme yapılmaz)",
|
||||
"personalUseOnly": "Kişisel kullanım için (ücretsiz lisans - ödeme yok)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "Ödemeye Devam Et"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "Makine İstemcileri",
|
||||
"install": "Yükle",
|
||||
"run": "Çalıştır",
|
||||
"envFile": "Ortam Dosyası",
|
||||
"serviceFile": "Servis Dosyası",
|
||||
"enableAndStart": "Etkinleştir ve Başlat",
|
||||
"clientNameDescription": "Daha sonra değiştirilebilecek istemcinin görünen adı.",
|
||||
"clientAddress": "İstemci Adresi (Gelişmiş)",
|
||||
"setupFailedToFetchSubnet": "Varsayılan alt ağ alınamadı",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "Kimlik Doğrulama Yok",
|
||||
"httpDestAuthNoneDescription": "Yetkilendirme başlığı olmadan istekler gönderir.",
|
||||
"httpDestAuthBearerTitle": "Taşıyıcı Jetonu",
|
||||
"httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı <token> başlığı ekler.",
|
||||
"httpDestAuthBearerDescription": "Her isteğe bir Yetkilendirme: Taşıyıcı '<token>' üst bilgisi ekler.",
|
||||
"httpDestAuthBearerPlaceholder": "API anahtarınız veya jetonunuz",
|
||||
"httpDestAuthBasicTitle": "Temel Kimlik Doğrulama",
|
||||
"httpDestAuthBasicDescription": "Authorization: Temel <belirtecikler> başlığı ekler. Yetkilendirmeleri kullanıcı adı:şifre olarak sağlayın.",
|
||||
"httpDestAuthBasicDescription": "Bir Yetkilendirme: Temel '<credentials>' üst bilgisi ekler. Kimlik bilgilerini kullanıcı adı:şifre olarak sağlayın.",
|
||||
"httpDestAuthBasicPlaceholder": "kullanıcı adı:şifre",
|
||||
"httpDestAuthCustomTitle": "Özel Başlık",
|
||||
"httpDestAuthCustomDescription": "Kimlik doğrulama için özel bir HTTP başlık adı ve değer belirtin (örn. X-API-Key).",
|
||||
|
||||
@@ -371,10 +371,10 @@
|
||||
"provisioningKeysUpdated": "置备密钥已更新",
|
||||
"provisioningKeysUpdatedDescription": "您的更改已保存。",
|
||||
"provisioningKeysBannerTitle": "站点置备密钥",
|
||||
"provisioningKeysBannerDescription": "生成一个预配键并使用它来在首次启动时自动创建站点——无需为每个站点设置单独的凭证。",
|
||||
"provisioningKeysBannerDescription": "生成一个供应密钥,并将其与 Newt 连接器一起使用,以在首次启动时自动创建站点 - 无需为每个站点设置单独的凭据。",
|
||||
"provisioningKeysBannerButtonText": "了解更多",
|
||||
"pendingSitesBannerTitle": "待定站点",
|
||||
"pendingSitesBannerDescription": "使用预配键连接的站点会出现在这里供审核。在站点开始运行之前批准并获取对您资源的访问权限。",
|
||||
"pendingSitesBannerDescription": "使用供应密钥连接的站点将在此显示以供审核。",
|
||||
"pendingSitesBannerButtonText": "了解更多",
|
||||
"apiKeysSettings": "{apiKeyName} 设置",
|
||||
"userTitle": "管理所有用户",
|
||||
@@ -624,6 +624,8 @@
|
||||
"targetErrorInvalidPortDescription": "请输入有效的端口号",
|
||||
"targetErrorNoSite": "没有选择站点",
|
||||
"targetErrorNoSiteDescription": "请选择目标站点",
|
||||
"targetTargetsCleared": "目标已清除",
|
||||
"targetTargetsClearedDescription": "所有目标已从此资源中移除",
|
||||
"targetCreated": "目标已创建",
|
||||
"targetCreatedDescription": "目标已成功创建",
|
||||
"targetErrorCreate": "创建目标失败",
|
||||
@@ -2346,7 +2348,7 @@
|
||||
"description": "企业特征、50个用户、50个站点和优先支持。"
|
||||
}
|
||||
},
|
||||
"personalUseOnly": "仅供个人使用 (免费许可证-无签出)",
|
||||
"personalUseOnly": "仅限个人使用(免费许可 - 无需结账)",
|
||||
"buttons": {
|
||||
"continueToCheckout": "继续签出"
|
||||
},
|
||||
@@ -2607,6 +2609,9 @@
|
||||
"machineClients": "机器客户端",
|
||||
"install": "安装",
|
||||
"run": "运行",
|
||||
"envFile": "环境文件",
|
||||
"serviceFile": "服务文件",
|
||||
"enableAndStart": "启用并启动",
|
||||
"clientNameDescription": "可以稍后更改的客户端的显示名称。",
|
||||
"clientAddress": "客户端地址 (高级)",
|
||||
"setupFailedToFetchSubnet": "获取默认子网失败",
|
||||
@@ -2845,10 +2850,10 @@
|
||||
"httpDestAuthNoneTitle": "无身份验证",
|
||||
"httpDestAuthNoneDescription": "在没有授权头的情况下发送请求。",
|
||||
"httpDestAuthBearerTitle": "持有者令牌",
|
||||
"httpDestAuthBearerDescription": "添加授权:每个请求的标题为 <token>。",
|
||||
"httpDestAuthBearerDescription": "在每个请求中添加授权:Bearer “<token>” 头。",
|
||||
"httpDestAuthBearerPlaceholder": "您的 API 密钥或令牌",
|
||||
"httpDestAuthBasicTitle": "基本认证",
|
||||
"httpDestAuthBasicDescription": "添加授权:基本 <credentials> 头。提供用户名:密码的凭据。",
|
||||
"httpDestAuthBasicDescription": "添加一个Authorization: Basic \"<凭据>\" 标头。 以用户名:密码形式提供凭据。",
|
||||
"httpDestAuthBasicPlaceholder": "用户名:密码",
|
||||
"httpDestAuthCustomTitle": "自定义标题",
|
||||
"httpDestAuthCustomDescription": "指定自定义 HTTP 头名称和身份验证值 (例如,X-API 键)。",
|
||||
|
||||
@@ -479,10 +479,7 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -610,10 +607,7 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return targets
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { and, eq, gt, desc, max, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import {
|
||||
LogType,
|
||||
LOG_TYPES,
|
||||
@@ -127,7 +129,7 @@ export class LogStreamingManager {
|
||||
start(): void {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
logger.info("LogStreamingManager: started");
|
||||
logger.debug("LogStreamingManager: started");
|
||||
this.schedulePoll(POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
@@ -272,19 +274,20 @@ export class LogStreamingManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse config – skip destination if config is unparseable
|
||||
let config: HttpConfig;
|
||||
// Decrypt and parse config – skip destination if either step fails
|
||||
let configFromDb: HttpConfig;
|
||||
try {
|
||||
config = JSON.parse(dest.config) as HttpConfig;
|
||||
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
|
||||
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`LogStreamingManager: destination ${dest.destinationId} has invalid JSON config`,
|
||||
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = this.createProvider(dest.type, config);
|
||||
const provider = this.createProvider(dest.type, configFromDb);
|
||||
if (!provider) {
|
||||
logger.warn(
|
||||
`LogStreamingManager: unsupported destination type "${dest.type}" ` +
|
||||
|
||||
@@ -671,10 +671,7 @@ export async function getTraefikConfig(
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = targets.some(
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -802,10 +799,7 @@ export async function getTraefikConfig(
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = targets.some(
|
||||
(target) =>
|
||||
target.site.online ||
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return targets
|
||||
|
||||
@@ -22,6 +22,8 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -87,7 +89,10 @@ export async function createEventStreamingDestination(
|
||||
);
|
||||
}
|
||||
|
||||
const { type, config, enabled } = parsedBody.data;
|
||||
const { type, config: configToSet, enabled } = parsedBody.data;
|
||||
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
const encryptedConfig = encrypt(configToSet, key);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
@@ -96,7 +101,7 @@ export async function createEventStreamingDestination(
|
||||
.values({
|
||||
orgId,
|
||||
type,
|
||||
config,
|
||||
config: encryptedConfig,
|
||||
enabled,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
@@ -22,6 +22,8 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -121,9 +123,22 @@ export async function listEventStreamingDestinations(
|
||||
.from(eventStreamingDestinations)
|
||||
.where(eq(eventStreamingDestinations.orgId, orgId));
|
||||
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
const decryptedList = list.map((dest) => {
|
||||
try {
|
||||
return { ...dest, config: decrypt(dest.config, key) };
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`listEventStreamingDestinations: failed to decrypt config for destination ${dest.destinationId}`,
|
||||
err
|
||||
);
|
||||
return { ...dest, config: "" };
|
||||
}
|
||||
});
|
||||
|
||||
return response<ListEventStreamingDestinationsResponse>(res, {
|
||||
data: {
|
||||
destinations: list,
|
||||
destinations: decryptedList,
|
||||
pagination: {
|
||||
total: count,
|
||||
limit,
|
||||
|
||||
@@ -22,7 +22,8 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -110,14 +111,17 @@ export async function updateEventStreamingDestination(
|
||||
);
|
||||
}
|
||||
|
||||
const { type, config, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
|
||||
const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
|
||||
if (type !== undefined) updateData.type = type;
|
||||
if (config !== undefined) updateData.config = config;
|
||||
if (configToUpdate !== undefined) {
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
updateData.config = encrypt(configToUpdate, key);
|
||||
}
|
||||
if (enabled !== undefined) updateData.enabled = enabled;
|
||||
if (sendAccessLogs !== undefined) updateData.sendAccessLogs = sendAccessLogs;
|
||||
if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;
|
||||
|
||||
@@ -171,9 +171,8 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
}
|
||||
|
||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
|
||||
const valuesList = chunk.map(
|
||||
([publicKey, { bytesIn, bytesOut }]) =>
|
||||
sql`(${publicKey}, ${bytesIn}, ${bytesOut})`
|
||||
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
|
||||
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
|
||||
);
|
||||
const valuesClause = sql.join(valuesList, sql`, `);
|
||||
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@server/db";
|
||||
import { sites, clients, olms } from "@server/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ import logger from "@server/logger";
|
||||
*/
|
||||
|
||||
const FLUSH_INTERVAL_MS = 10_000; // Flush every 10 seconds
|
||||
const MAX_RETRIES = 2;
|
||||
const MAX_RETRIES = 5;
|
||||
const BASE_DELAY_MS = 50;
|
||||
|
||||
// ── Site (newt) pings ──────────────────────────────────────────────────
|
||||
@@ -36,6 +36,14 @@ const pendingOlmArchiveResets: Set<string> = new Set();
|
||||
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Guard that prevents two flush cycles from running concurrently.
|
||||
* setInterval does not await async callbacks, so without this a slow flush
|
||||
* (e.g. due to DB latency) would overlap with the next scheduled cycle and
|
||||
* the two concurrent bulk UPDATEs would deadlock each other.
|
||||
*/
|
||||
let isFlushing = false;
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -72,6 +80,12 @@ export function recordClientPing(
|
||||
|
||||
/**
|
||||
* Flush all accumulated site pings to the database.
|
||||
*
|
||||
* Each batch of up to BATCH_SIZE rows is written with a **single** UPDATE
|
||||
* statement. We use the maximum timestamp across the batch so that `lastPing`
|
||||
* reflects the most recent ping seen for any site in the group. This avoids
|
||||
* the multi-statement transaction that previously created additional
|
||||
* row-lock ordering hazards.
|
||||
*/
|
||||
async function flushSitePingsToDb(): Promise<void> {
|
||||
if (pendingSitePings.size === 0) {
|
||||
@@ -83,55 +97,35 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
const pingsToFlush = new Map(pendingSitePings);
|
||||
pendingSitePings.clear();
|
||||
|
||||
// Sort by siteId for consistent lock ordering (prevents deadlocks)
|
||||
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
|
||||
([a], [b]) => a - b
|
||||
);
|
||||
const entries = Array.from(pingsToFlush.entries());
|
||||
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
|
||||
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
// Use the latest timestamp in the batch so that `lastPing` always
|
||||
// moves forward. Using a single timestamp for the whole batch means
|
||||
// we only ever need one UPDATE statement (no transaction).
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const siteIds = batch.map(([id]) => id);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
// Group by timestamp for efficient bulk updates
|
||||
const byTimestamp = new Map<number, number[]>();
|
||||
for (const [siteId, timestamp] of batch) {
|
||||
const group = byTimestamp.get(timestamp) || [];
|
||||
group.push(siteId);
|
||||
byTimestamp.set(timestamp, group);
|
||||
}
|
||||
|
||||
if (byTimestamp.size === 1) {
|
||||
const [timestamp, siteIds] = Array.from(
|
||||
byTimestamp.entries()
|
||||
)[0];
|
||||
await db
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: timestamp
|
||||
lastPing: maxTimestamp
|
||||
})
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
} else {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [timestamp, siteIds] of byTimestamp) {
|
||||
await tx
|
||||
.update(sites)
|
||||
.set({
|
||||
online: true,
|
||||
lastPing: timestamp
|
||||
})
|
||||
.where(inArray(sites.siteId, siteIds));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "flushSitePingsToDb");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`,
|
||||
{ error }
|
||||
);
|
||||
// Re-queue only if the preserved timestamp is newer than any
|
||||
// update that may have landed since we snapshotted.
|
||||
for (const [siteId, timestamp] of batch) {
|
||||
const existing = pendingSitePings.get(siteId);
|
||||
if (!existing || existing < timestamp) {
|
||||
@@ -144,6 +138,8 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Flush all accumulated client (OLM) pings to the database.
|
||||
*
|
||||
* Same single-UPDATE-per-batch approach as `flushSitePingsToDb`.
|
||||
*/
|
||||
async function flushClientPingsToDb(): Promise<void> {
|
||||
if (pendingClientPings.size === 0 && pendingOlmArchiveResets.size === 0) {
|
||||
@@ -159,51 +155,25 @@ async function flushClientPingsToDb(): Promise<void> {
|
||||
|
||||
// ── Flush client pings ─────────────────────────────────────────────
|
||||
if (pingsToFlush.size > 0) {
|
||||
const sortedEntries = Array.from(pingsToFlush.entries()).sort(
|
||||
([a], [b]) => a - b
|
||||
);
|
||||
const entries = Array.from(pingsToFlush.entries());
|
||||
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < sortedEntries.length; i += BATCH_SIZE) {
|
||||
const batch = sortedEntries.slice(i, i + BATCH_SIZE);
|
||||
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
||||
const batch = entries.slice(i, i + BATCH_SIZE);
|
||||
|
||||
const maxTimestamp = Math.max(...batch.map(([, ts]) => ts));
|
||||
const clientIds = batch.map(([id]) => id);
|
||||
|
||||
try {
|
||||
await withRetry(async () => {
|
||||
const byTimestamp = new Map<number, number[]>();
|
||||
for (const [clientId, timestamp] of batch) {
|
||||
const group = byTimestamp.get(timestamp) || [];
|
||||
group.push(clientId);
|
||||
byTimestamp.set(timestamp, group);
|
||||
}
|
||||
|
||||
if (byTimestamp.size === 1) {
|
||||
const [timestamp, clientIds] = Array.from(
|
||||
byTimestamp.entries()
|
||||
)[0];
|
||||
await db
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: timestamp,
|
||||
lastPing: maxTimestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.where(inArray(clients.clientId, clientIds));
|
||||
} else {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [timestamp, clientIds] of byTimestamp) {
|
||||
await tx
|
||||
.update(clients)
|
||||
.set({
|
||||
lastPing: timestamp,
|
||||
online: true,
|
||||
archived: false
|
||||
})
|
||||
.where(
|
||||
inArray(clients.clientId, clientIds)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "flushClientPingsToDb");
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -260,7 +230,12 @@ export async function flushPingsToDb(): Promise<void> {
|
||||
|
||||
/**
|
||||
* Simple retry wrapper with exponential backoff for transient errors
|
||||
* (connection timeouts, unexpected disconnects).
|
||||
* (deadlocks, connection timeouts, unexpected disconnects).
|
||||
*
|
||||
* PostgreSQL deadlocks (40P01) are always safe to retry: the database
|
||||
* guarantees exactly one winner per deadlock pair, so the loser just needs
|
||||
* to try again. MAX_RETRIES is intentionally higher than typical connection
|
||||
* retry budgets to give deadlock victims enough chances to succeed.
|
||||
*/
|
||||
async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
@@ -277,7 +252,8 @@ async function withRetry<T>(
|
||||
const jitter = Math.random() * baseDelay;
|
||||
const delay = baseDelay + jitter;
|
||||
logger.warn(
|
||||
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`
|
||||
`Transient DB error in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`,
|
||||
{ code: error?.code ?? error?.cause?.code }
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
@@ -288,14 +264,14 @@ async function withRetry<T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect transient connection errors that are safe to retry.
|
||||
* Detect transient errors that are safe to retry.
|
||||
*/
|
||||
function isTransientError(error: any): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
const message = (error.message || "").toLowerCase();
|
||||
const causeMessage = (error.cause?.message || "").toLowerCase();
|
||||
const code = error.code || "";
|
||||
const code = error.code || error.cause?.code || "";
|
||||
|
||||
// Connection timeout / terminated
|
||||
if (
|
||||
@@ -308,12 +284,17 @@ function isTransientError(error: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PostgreSQL deadlock
|
||||
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
|
||||
if (code === "40P01" || message.includes("deadlock")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ECONNRESET, ECONNREFUSED, EPIPE
|
||||
// PostgreSQL serialization failure
|
||||
if (code === "40001") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT
|
||||
if (
|
||||
code === "ECONNRESET" ||
|
||||
code === "ECONNREFUSED" ||
|
||||
@@ -337,12 +318,26 @@ export function startPingAccumulator(): void {
|
||||
}
|
||||
|
||||
flushTimer = setInterval(async () => {
|
||||
// Skip this tick if the previous flush is still in progress.
|
||||
// setInterval does not await async callbacks, so without this guard
|
||||
// two flush cycles can run concurrently and deadlock each other on
|
||||
// overlapping bulk UPDATE statements.
|
||||
if (isFlushing) {
|
||||
logger.debug(
|
||||
"Ping accumulator: previous flush still in progress, skipping cycle"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isFlushing = true;
|
||||
try {
|
||||
await flushPingsToDb();
|
||||
} catch (error) {
|
||||
logger.error("Unhandled error in ping accumulator flush", {
|
||||
error
|
||||
});
|
||||
} finally {
|
||||
isFlushing = false;
|
||||
}
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
|
||||
@@ -364,7 +359,22 @@ export async function stopPingAccumulator(): Promise<void> {
|
||||
flushTimer = null;
|
||||
}
|
||||
|
||||
// Final flush to persist any remaining pings
|
||||
// Final flush to persist any remaining pings.
|
||||
// Wait for any in-progress flush to finish first so we don't race.
|
||||
if (isFlushing) {
|
||||
logger.debug(
|
||||
"Ping accumulator: waiting for in-progress flush before stopping…"
|
||||
);
|
||||
await new Promise<void>((resolve) => {
|
||||
const poll = setInterval(() => {
|
||||
if (!isFlushing) {
|
||||
clearInterval(poll);
|
||||
resolve();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await flushPingsToDb();
|
||||
} catch (error) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { build } from "@server/build";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
import { INSPECT_MAX_BYTES } from "buffer";
|
||||
import { v } from "@faker-js/faker/dist/airline-Dz1uGqgJ";
|
||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||
|
||||
const bodySchema = z.object({
|
||||
provisioningKey: z.string().nonempty(),
|
||||
@@ -152,6 +152,11 @@ export async function registerNewt(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
|
||||
);
|
||||
}
|
||||
if (!org.subnet) {
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Organization subnet not found")
|
||||
);
|
||||
}
|
||||
|
||||
// SaaS billing check
|
||||
if (build == "saas") {
|
||||
@@ -190,6 +195,20 @@ export async function registerNewt(
|
||||
let newSiteId: number | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
|
||||
const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
||||
if (!newClientAddress) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"No available subnet found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let clientAddress = newClientAddress.split("/")[0];
|
||||
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
|
||||
|
||||
// Create the site (type "newt", name = niceId)
|
||||
const [newSite] = await trx
|
||||
.insert(sites)
|
||||
@@ -197,6 +216,7 @@ export async function registerNewt(
|
||||
orgId,
|
||||
name: name || niceId,
|
||||
niceId,
|
||||
address: clientAddress,
|
||||
type: "newt",
|
||||
dockerSocketEnabled: true,
|
||||
status: keyRecord.approveNewSites ? "approved" : "pending",
|
||||
|
||||
@@ -104,6 +104,42 @@ export default async function migration() {
|
||||
CONSTRAINT "userOrgRoles_userId_orgId_roleId_unique" UNIQUE("userId","orgId","roleId")
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "eventStreamingCursors" (
|
||||
"cursorId" serial PRIMARY KEY NOT NULL,
|
||||
"destinationId" integer NOT NULL,
|
||||
"logType" varchar(50) NOT NULL,
|
||||
"lastSentId" bigint DEFAULT 0 NOT NULL,
|
||||
"lastSentAt" bigint
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "eventStreamingDestinations" (
|
||||
"destinationId" serial PRIMARY KEY NOT NULL,
|
||||
"orgId" varchar(255) NOT NULL,
|
||||
"sendConnectionLogs" boolean DEFAULT false NOT NULL,
|
||||
"sendRequestLogs" boolean DEFAULT false NOT NULL,
|
||||
"sendActionLogs" boolean DEFAULT false NOT NULL,
|
||||
"sendAccessLogs" boolean DEFAULT false NOT NULL,
|
||||
"type" varchar(50) NOT NULL,
|
||||
"config" text NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"createdAt" bigint NOT NULL,
|
||||
"updatedAt" bigint NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "eventStreamingCursors" ADD CONSTRAINT "eventStreamingCursors_destinationId_eventStreamingDestinations_destinationId_fk" FOREIGN KEY ("destinationId") REFERENCES "public"."eventStreamingDestinations"("destinationId") ON DELETE cascade ON UPDATE no action;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "eventStreamingDestinations" ADD CONSTRAINT "eventStreamingDestinations_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`CREATE UNIQUE INDEX "idx_eventStreamingCursors_dest_type" ON "eventStreamingCursors" USING btree ("destinationId","logType");`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "userOrgs" DROP CONSTRAINT "userOrgs_roleId_roles_roleId_fk";`
|
||||
);
|
||||
@@ -177,8 +213,12 @@ export default async function migration() {
|
||||
sql`CREATE INDEX "idx_accessAuditLog_siteResourceId" ON "connectionAuditLog" USING btree ("siteResourceId");`
|
||||
);
|
||||
await db.execute(sql`ALTER TABLE "userInvites" DROP COLUMN "roleId";`);
|
||||
await db.execute(sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`);
|
||||
await db.execute(sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "siteProvisioningKeys" ADD COLUMN "approveNewSites" boolean DEFAULT true NOT NULL;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "sites" ADD COLUMN "status" varchar DEFAULT 'approved';`
|
||||
);
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log("Migrated database");
|
||||
|
||||
@@ -76,9 +76,15 @@ export default async function migration() {
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`).run();
|
||||
db.prepare(`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`).run();
|
||||
db.prepare(`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`).run();
|
||||
db.prepare(
|
||||
`CREATE INDEX 'idx_accessAuditLog_startedAt' ON 'connectionAuditLog' ('startedAt');`
|
||||
).run();
|
||||
db.prepare(
|
||||
`CREATE INDEX 'idx_accessAuditLog_org_startedAt' ON 'connectionAuditLog' ('orgId','startedAt');`
|
||||
).run();
|
||||
db.prepare(
|
||||
`CREATE INDEX 'idx_accessAuditLog_siteResourceId' ON 'connectionAuditLog' ('siteResourceId');`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
@@ -168,6 +174,42 @@ export default async function migration() {
|
||||
);
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE 'eventStreamingCursors' (
|
||||
'cursorId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
'destinationId' integer NOT NULL,
|
||||
'logType' text NOT NULL,
|
||||
'lastSentId' integer DEFAULT 0 NOT NULL,
|
||||
'lastSentAt' integer,
|
||||
FOREIGN KEY ('destinationId') REFERENCES 'eventStreamingDestinations'('destinationId') ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`
|
||||
).run();
|
||||
db.prepare(
|
||||
`
|
||||
CREATE UNIQUE INDEX 'idx_eventStreamingCursors_dest_type' ON 'eventStreamingCursors' ('destinationId','logType');--> statement-breakpoint
|
||||
`
|
||||
).run();
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE 'eventStreamingDestinations' (
|
||||
'destinationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
'orgId' text NOT NULL,
|
||||
'sendConnectionLogs' integer DEFAULT false NOT NULL,
|
||||
'sendRequestLogs' integer DEFAULT false NOT NULL,
|
||||
'sendActionLogs' integer DEFAULT false NOT NULL,
|
||||
'sendAccessLogs' integer DEFAULT false NOT NULL,
|
||||
'type' text NOT NULL,
|
||||
'config' text NOT NULL,
|
||||
'enabled' integer DEFAULT true NOT NULL,
|
||||
'createdAt' integer NOT NULL,
|
||||
'updatedAt' integer NOT NULL,
|
||||
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`
|
||||
).run();
|
||||
db.prepare(
|
||||
`INSERT INTO '__new_userInvites'("inviteId", "orgId", "email", "expiresAt", "token") SELECT "inviteId", "orgId", "email", "expiresAt", "token" FROM 'userInvites';`
|
||||
).run();
|
||||
@@ -191,8 +233,12 @@ export default async function migration() {
|
||||
`ALTER TABLE 'user' ADD 'marketingEmailConsent' integer DEFAULT false;`
|
||||
).run();
|
||||
db.prepare(`ALTER TABLE 'user' ADD 'locale' text;`).run();
|
||||
db.prepare(`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`).run();
|
||||
db.prepare(`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE 'siteProvisioningKeys' ADD COLUMN 'approveNewSites' integer DEFAULT 1 NOT NULL;`
|
||||
).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE 'sites' ADD COLUMN 'status' text DEFAULT 'approved';`
|
||||
).run();
|
||||
})();
|
||||
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
@@ -491,7 +491,7 @@ export default function ConnectionLogsPage() {
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const clientType = row.original.clientType === "olm" ? "machine" : "user";
|
||||
const clientType = row.original.userId ? "user" : "machine";
|
||||
if (row.original.clientName && row.original.clientNiceId) {
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -106,7 +106,9 @@ function DestinationCard({
|
||||
{/* URL preview */}
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{cfg.url || (
|
||||
<span className="italic">{t("streamingNoUrlConfigured")}</span>
|
||||
<span className="italic">
|
||||
{t("streamingNoUrlConfigured")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -160,7 +162,9 @@ function AddDestinationCard({ onClick }: { onClick: () => void }) {
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-md border-2 border-dashed border-current">
|
||||
<Plus className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">{t("streamingAddDestination")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{t("streamingAddDestination")}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -186,7 +190,9 @@ function DestinationTypePicker({
|
||||
const t = useTranslations();
|
||||
const [selected, setSelected] = useState<DestinationType>("http");
|
||||
|
||||
const destinationTypeOptions: ReadonlyArray<StrategyOption<DestinationType>> = [
|
||||
const destinationTypeOptions: ReadonlyArray<
|
||||
StrategyOption<DestinationType>
|
||||
> = [
|
||||
{
|
||||
id: "http",
|
||||
title: t("streamingHttpWebhookTitle"),
|
||||
@@ -233,13 +239,19 @@ function DestinationTypePicker({
|
||||
<Credenza open={open} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent className="sm:max-w-lg">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("streamingAddDestination")}</CredenzaTitle>
|
||||
<CredenzaTitle>
|
||||
{t("streamingAddDestination")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("streamingTypePickerDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className={isPaywalled ? "pointer-events-none opacity-50" : ""}>
|
||||
<div
|
||||
className={
|
||||
isPaywalled ? "pointer-events-none opacity-50" : ""
|
||||
}
|
||||
>
|
||||
<StrategySelect
|
||||
options={destinationTypeOptions}
|
||||
value={selected}
|
||||
@@ -301,10 +313,7 @@ export default function StreamingDestinationsPage() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("streamingFailedToLoad"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("streamingUnexpectedError")
|
||||
)
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -341,10 +350,7 @@ export default function StreamingDestinationsPage() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("streamingFailedToUpdate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("streamingUnexpectedError")
|
||||
)
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setTogglingIds((prev) => {
|
||||
@@ -375,10 +381,7 @@ export default function StreamingDestinationsPage() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("streamingFailedToDelete"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("streamingUnexpectedError")
|
||||
)
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
@@ -459,13 +462,14 @@ export default function StreamingDestinationsPage() {
|
||||
if (!v) setDeleteTarget(null);
|
||||
}}
|
||||
string={
|
||||
parseHttpConfig(deleteTarget.config).name || t("streamingDeleteDialogThisDestination")
|
||||
parseHttpConfig(deleteTarget.config).name ||
|
||||
t("streamingDeleteDialogThisDestination")
|
||||
}
|
||||
title={t("streamingDeleteTitle")}
|
||||
dialog={
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
{t("streamingDeleteDialogAreYouSure")}{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
<span>
|
||||
{parseHttpConfig(deleteTarget.config).name ||
|
||||
t("streamingDeleteDialogThisDestination")}
|
||||
</span>
|
||||
|
||||
@@ -400,7 +400,11 @@ function ProxyResourceTargetsForm({
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
updateTarget(row.original.targetId,
|
||||
config.path === null && config.pathMatchType === null
|
||||
? { ...config, rewritePath: null, rewritePathType: null }
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
@@ -424,7 +428,11 @@ function ProxyResourceTargetsForm({
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
updateTarget(row.original.targetId,
|
||||
config.path === null && config.pathMatchType === null
|
||||
? { ...config, rewritePath: null, rewritePathType: null }
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
@@ -774,8 +782,12 @@ function ProxyResourceTargetsForm({
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("settingsUpdated"),
|
||||
description: t("settingsUpdatedDescription")
|
||||
title: targets.length === 0
|
||||
? t("targetTargetsCleared")
|
||||
: t("settingsUpdated"),
|
||||
description: targets.length === 0
|
||||
? t("targetTargetsClearedDescription")
|
||||
: t("settingsUpdatedDescription")
|
||||
});
|
||||
|
||||
setTargetsToRemove([]);
|
||||
|
||||
@@ -776,7 +776,11 @@ export default function Page() {
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
updateTarget(row.original.targetId,
|
||||
config.path === null && config.pathMatchType === null
|
||||
? { ...config, rewritePath: null, rewritePathType: null }
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
@@ -800,7 +804,11 @@ export default function Page() {
|
||||
pathMatchType: row.original.pathMatchType
|
||||
}}
|
||||
onChange={(config) =>
|
||||
updateTarget(row.original.targetId, config)
|
||||
updateTarget(row.original.targetId,
|
||||
config.path === null && config.pathMatchType === null
|
||||
? { ...config, rewritePath: null, rewritePathType: null }
|
||||
: config
|
||||
)
|
||||
}
|
||||
trigger={
|
||||
<Button
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
|
||||
import { useState } from "react";
|
||||
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { FaApple, FaCubes, FaDocker, FaLinux, FaWindows } from "react-icons/fa";
|
||||
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||
|
||||
export type CommandItem = string | { title: string; command: string };
|
||||
|
||||
const PLATFORMS = [
|
||||
"unix",
|
||||
"linux",
|
||||
"macos",
|
||||
"docker",
|
||||
"kubernetes",
|
||||
"podman",
|
||||
@@ -43,7 +43,7 @@ export function NewtSiteInstallCommands({
|
||||
const t = useTranslations();
|
||||
|
||||
const [acceptClients, setAcceptClients] = useState(true);
|
||||
const [platform, setPlatform] = useState<Platform>("unix");
|
||||
const [platform, setPlatform] = useState<Platform>("linux");
|
||||
const [architecture, setArchitecture] = useState(
|
||||
() => getArchitectures(platform)[0]
|
||||
);
|
||||
@@ -54,8 +54,68 @@ export function NewtSiteInstallCommands({
|
||||
: "";
|
||||
|
||||
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||
unix: {
|
||||
All: [
|
||||
linux: {
|
||||
Run: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
}
|
||||
],
|
||||
"Systemd Service": [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("envFile"),
|
||||
command: `# Create the directory and environment file
|
||||
sudo install -d -m 0755 /etc/newt
|
||||
sudo tee /etc/newt/newt.env > /dev/null << 'EOF'
|
||||
NEWT_ID=${id}
|
||||
NEWT_SECRET=${secret}
|
||||
PANGOLIN_ENDPOINT=${endpoint}${!acceptClients ? `
|
||||
DISABLE_CLIENTS=true` : ""}
|
||||
EOF
|
||||
sudo chmod 600 /etc/newt/newt.env`
|
||||
},
|
||||
{
|
||||
title: t("serviceFile"),
|
||||
command: `sudo tee /etc/systemd/system/newt.service > /dev/null << 'EOF'
|
||||
[Unit]
|
||||
Description=Newt
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
EnvironmentFile=/etc/newt/newt.env
|
||||
ExecStart=/usr/local/bin/newt
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
UMask=0077
|
||||
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF`
|
||||
},
|
||||
{
|
||||
title: t("enableAndStart"),
|
||||
command: `sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now newt`
|
||||
}
|
||||
]
|
||||
},
|
||||
macos: {
|
||||
Run: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
@@ -131,7 +191,7 @@ WantedBy=default.target`
|
||||
]
|
||||
},
|
||||
nixos: {
|
||||
All: [
|
||||
Flake: [
|
||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
}
|
||||
@@ -172,9 +232,9 @@ WantedBy=default.target`
|
||||
|
||||
<OptionSelect<string>
|
||||
label={
|
||||
["docker", "podman"].includes(platform)
|
||||
? t("method")
|
||||
: t("architecture")
|
||||
platform === "windows"
|
||||
? t("architecture")
|
||||
: t("method")
|
||||
}
|
||||
options={getArchitectures(platform).map((arch) => ({
|
||||
value: arch,
|
||||
@@ -261,8 +321,10 @@ function getPlatformIcon(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||
case "unix":
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
case "linux":
|
||||
return <FaLinux className="h-4 w-4 mr-2" />;
|
||||
case "macos":
|
||||
return <FaApple className="h-4 w-4 mr-2" />;
|
||||
case "docker":
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
case "kubernetes":
|
||||
@@ -272,7 +334,7 @@ function getPlatformIcon(platformName: Platform) {
|
||||
case "nixos":
|
||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
return <FaLinux className="h-4 w-4 mr-2" />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,8 +342,10 @@ function getPlatformName(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return "Windows";
|
||||
case "unix":
|
||||
return "Unix & macOS";
|
||||
case "linux":
|
||||
return "Linux";
|
||||
case "macos":
|
||||
return "macOS";
|
||||
case "docker":
|
||||
return "Docker";
|
||||
case "kubernetes":
|
||||
@@ -291,14 +355,16 @@ function getPlatformName(platformName: Platform) {
|
||||
case "nixos":
|
||||
return "NixOS";
|
||||
default:
|
||||
return "Unix / macOS";
|
||||
return "Linux";
|
||||
}
|
||||
}
|
||||
|
||||
function getArchitectures(platform: Platform) {
|
||||
switch (platform) {
|
||||
case "unix":
|
||||
return ["All"];
|
||||
case "linux":
|
||||
return ["Run", "Systemd Service"];
|
||||
case "macos":
|
||||
return ["Run"];
|
||||
case "windows":
|
||||
return ["x64"];
|
||||
case "docker":
|
||||
@@ -308,8 +374,8 @@ function getArchitectures(platform: Platform) {
|
||||
case "podman":
|
||||
return ["Podman Quadlet", "Podman Run"];
|
||||
case "nixos":
|
||||
return ["All"];
|
||||
return ["Flake"];
|
||||
default:
|
||||
return ["x64"];
|
||||
return ["Run"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,21 @@ export async function getUserLocale(): Promise<Locale> {
|
||||
const res = await internal.get("/user", await authCookieHeader());
|
||||
const userLocale = res.data?.data?.locale;
|
||||
if (userLocale && locales.includes(userLocale as Locale)) {
|
||||
// Set the cookie so subsequent requests don't need the API call
|
||||
// Try to cache in a cookie so subsequent requests skip the API
|
||||
// call. cookies().set() is only permitted in Server Actions and
|
||||
// Route Handlers — not during rendering — so we isolate it so
|
||||
// that a write failure doesn't prevent the locale from being
|
||||
// returned for the current request.
|
||||
try {
|
||||
(await cookies()).set(COOKIE_NAME, userLocale, {
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
path: "/",
|
||||
sameSite: "lax"
|
||||
});
|
||||
} catch {
|
||||
// Cannot set cookies in this context (e.g. during rendering);
|
||||
// the correct locale is still returned below.
|
||||
}
|
||||
return userLocale as Locale;
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user