Compare commits

...

53 Commits

Author SHA1 Message Date
Owen Schwartz 452a2b3dd3 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-17 16:13:28 -04:00
Owen Schwartz ace90e99d1 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-17 16:13:26 -04:00
Owen Schwartz efe53a2294 New translations en-us.json (Turkish)
[ci skip]
2026-07-17 16:13:24 -04:00
Owen Schwartz 5c39c98e3a New translations en-us.json (Russian)
[ci skip]
2026-07-17 16:13:23 -04:00
Owen Schwartz 9b9fc5caa1 New translations en-us.json (Portuguese)
[ci skip]
2026-07-17 16:13:21 -04:00
Owen Schwartz ef09b62bf3 New translations en-us.json (Polish)
[ci skip]
2026-07-17 16:13:19 -04:00
Owen Schwartz 9a269d515f New translations en-us.json (Dutch)
[ci skip]
2026-07-17 16:13:17 -04:00
Owen Schwartz 891dbdc6d8 New translations en-us.json (Korean)
[ci skip]
2026-07-17 16:13:15 -04:00
Owen Schwartz d90850e99d New translations en-us.json (Italian)
[ci skip]
2026-07-17 16:13:13 -04:00
Owen Schwartz d363416769 New translations en-us.json (German)
[ci skip]
2026-07-17 16:13:11 -04:00
Owen Schwartz f115ddd2b2 New translations en-us.json (Danish)
[ci skip]
2026-07-17 16:13:09 -04:00
Owen Schwartz 754837b36d New translations en-us.json (Czech)
[ci skip]
2026-07-17 16:13:07 -04:00
Owen Schwartz 0e67c31908 New translations en-us.json (Bulgarian)
[ci skip]
2026-07-17 16:13:05 -04:00
Owen Schwartz b7511e912d New translations en-us.json (Spanish)
[ci skip]
2026-07-17 16:13:03 -04:00
Owen 02d2e09709 Merge branch 'local-connection' into dev 2026-07-17 14:07:30 -04:00
miloschwartz 6e8283957c update remote node routes to support access token updates 2026-07-17 14:06:35 -04:00
miloschwartz bb9b94a983 add persistent session and users to access tokens 2026-07-17 13:39:09 -04:00
Owen 36f807ddbc Merge branch 'dev' into local-connection 2026-07-17 11:15:51 -04:00
miloschwartz cb31546c6b fix spelling in installer 2026-07-17 10:15:11 -04:00
miloschwartz 2d48adc9f9 use proxy for logout on org auth page session expire 2026-07-16 22:06:59 -04:00
Owen Schwartz ca279ee3e0 Merge pull request #3452 from fosrl/crowdin_dev
New Crowdin updates
2026-07-16 21:37:08 -04:00
Owen Schwartz a5808200f0 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-16 21:34:54 -04:00
Owen Schwartz aa9de4d2ac New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-16 21:34:52 -04:00
Owen Schwartz 50821e972d New translations en-us.json (Turkish)
[ci skip]
2026-07-16 21:34:50 -04:00
Owen Schwartz e5b9dfee66 New translations en-us.json (Russian)
[ci skip]
2026-07-16 21:34:48 -04:00
Owen Schwartz 8e09070bc1 New translations en-us.json (Portuguese)
[ci skip]
2026-07-16 21:34:46 -04:00
Owen Schwartz 3d9c413bbb New translations en-us.json (Polish)
[ci skip]
2026-07-16 21:34:45 -04:00
Owen Schwartz 86670c1d60 New translations en-us.json (Dutch)
[ci skip]
2026-07-16 21:34:43 -04:00
Owen Schwartz 5f2ee77a73 New translations en-us.json (Korean)
[ci skip]
2026-07-16 21:34:41 -04:00
Owen Schwartz 714e3a8fb1 New translations en-us.json (Italian)
[ci skip]
2026-07-16 21:34:40 -04:00
Owen Schwartz 672555f051 New translations en-us.json (German)
[ci skip]
2026-07-16 21:34:38 -04:00
Owen Schwartz 7853f2b096 New translations en-us.json (Danish)
[ci skip]
2026-07-16 21:34:36 -04:00
Owen Schwartz 61a7dda29a New translations en-us.json (Czech)
[ci skip]
2026-07-16 21:34:35 -04:00
Owen Schwartz f9bcb25eaa New translations en-us.json (Bulgarian)
[ci skip]
2026-07-16 21:34:33 -04:00
Owen Schwartz 6df804ab32 New translations en-us.json (Spanish)
[ci skip]
2026-07-16 21:34:31 -04:00
Owen Schwartz 9e50735800 New translations en-us.json (French)
[ci skip]
2026-07-16 21:34:29 -04:00
miloschwartz a30119f5e8 encode email in url param 2026-07-16 21:28:36 -04:00
miloschwartz 3c9f0946d2 add site provisioning keys to integration api 2026-07-16 17:56:31 -04:00
Owen 21861a6dcd Merge branch 'local-connection' of github.com:fosrl/pangolin into local-connection 2026-07-16 17:25:58 -04:00
Owen 6135f2b727 Local endpoints from the get config 2026-07-16 17:25:51 -04:00
Owen 3a616cc804 Add handlers 2026-07-16 17:25:51 -04:00
Owen d5eb67a8fc Send localEndpoints on sites to olm 2026-07-16 17:25:51 -04:00
Owen 3ac7ef23ae Save the local endpoints 2026-07-16 17:25:51 -04:00
Owen 08c5ec2be7 Add new column 2026-07-16 17:25:51 -04:00
Owen a88b79e066 Add timezone offset to fix bad display 2026-07-16 16:09:35 -04:00
Owen a48ef77ee5 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-07-16 15:49:52 -04:00
Owen 55f3807491 Enforce uniqueness of aliases in blueprints 2026-07-16 15:10:38 -04:00
Owen dbfb8e83e8 Local endpoints from the get config 2026-07-16 14:43:45 -04:00
Owen 515afc14a5 Add handlers 2026-07-16 11:23:53 -04:00
Owen 903e8c0fa1 Send localEndpoints on sites to olm 2026-07-16 10:47:24 -04:00
Owen 7f9c760380 Save the local endpoints 2026-07-16 09:59:11 -04:00
Owen 530a1a5350 Add new column 2026-07-16 09:59:07 -04:00
miloschwartz 3fb36c1434 add missing translation 2026-07-15 22:03:28 -04:00
63 changed files with 1938 additions and 376 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ var redisFlag *bool
func main() { func main() {
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt") crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as cacheing solution. Required for HA. Not required for the Enterprise version.") redisFlag = flag.Bool("redis", false, "Install Redis as caching solution. Required for HA. Not required for the Enterprise version.")
flag.Parse() flag.Parse()
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking // print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Потвърдете изтриването на споделима връзка", "shareDeleteConfirm": "Потвърдете изтриването на споделима връзка",
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?", "shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.", "shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Достъп Токен", "accessToken": "Достъп Токен",
"usageExamples": "Примери за използване", "usageExamples": "Примери за използване",
"tokenId": "Токен ID", "tokenId": "Токен ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Заглавие (по избор)", "shareTitleOptional": "Заглавие (по избор)",
"sharePathOptional": "Път (по избор)", "sharePathOptional": "Път (по избор)",
"sharePathDescription": "След удостоверяване, линкът ще препрати потребителите на този път.", "sharePathDescription": "След удостоверяване, линкът ще препрати потребителите на този път.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Изтече", "expireIn": "Изтече",
"neverExpire": "Никога не изтича", "neverExpire": "Никога не изтича",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.", "shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.",
"shareSeeOnce": "Ще можете да видите този линк само веднъж. Уверете се, че го копирате.", "shareSeeOnce": "Ще можете да видите този линк само веднъж. Уверете се, че го копирате.",
"shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.", "shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Списък с покани", "actionListInvitations": "Списък с покани",
"actionExportLogs": "Експортиране на дневници", "actionExportLogs": "Експортиране на дневници",
"actionViewLogs": "Преглед на дневници", "actionViewLogs": "Преглед на дневници",
"actionCreateSiteProvisioningKey": "Създаване на ключ за предоставяне на сайт",
"actionListSiteProvisioningKeys": "Списък на ключовете за предоставяне на сайт",
"actionUpdateSiteProvisioningKey": "Актуализиране на ключ за предоставяне на сайт",
"actionDeleteSiteProvisioningKey": "Изтриване на ключ за предоставяне на сайт",
"noneSelected": "Нищо не е избрано", "noneSelected": "Нищо не е избрано",
"orgNotFound2": "Няма намерени организации.", "orgNotFound2": "Няма намерени организации.",
"search": "Търси…", "search": "Търси…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Източен адрес", "sourceAddress": "Източен адрес",
"destinationAddress": "Адрес на дестинация", "destinationAddress": "Адрес на дестинация",
"duration": "Продължителност", "duration": "Продължителност",
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> за използване на тази функция. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Решавач на сертификати", "certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.", "certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати", "selectCertResolver": "Изберете решавач на сертификати",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Качването неуспешно", "rdpUploadFailed": "Качването неуспешно",
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура", "rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
"sessionToolbarShow": "Показване на лентата с инструменти", "sessionToolbarShow": "Показване на лентата с инструменти",
"sessionToolbarHide": "Скриване на лентата с инструменти" "sessionToolbarHide": "Скриване на лентата с инструменти",
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Potvrdit odstranění sdílného odkazu", "shareDeleteConfirm": "Potvrdit odstranění sdílného odkazu",
"shareQuestionRemove": "Jste si jisti, že chcete smazat tento odkaz ke sdílení?", "shareQuestionRemove": "Jste si jisti, že chcete smazat tento odkaz ke sdílení?",
"shareMessageRemove": "Jakmile bude smazán, odkaz přestane fungovat a všichni, kdo jej používají, ztratí přístup k prostředku.", "shareMessageRemove": "Jakmile bude smazán, odkaz přestane fungovat a všichni, kdo jej používají, ztratí přístup k prostředku.",
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Přístupový token", "accessToken": "Přístupový token",
"usageExamples": "Příklady použití", "usageExamples": "Příklady použití",
"tokenId": "ID tokenu", "tokenId": "ID tokenu",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Název (volitelné)", "shareTitleOptional": "Název (volitelné)",
"sharePathOptional": "Cesta (volitelně)", "sharePathOptional": "Cesta (volitelně)",
"sharePathDescription": "Odkaz přesměruje uživatele na tuto cestu po autentikaci.", "sharePathDescription": "Odkaz přesměruje uživatele na tuto cestu po autentikaci.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Platnost vyprší za", "expireIn": "Platnost vyprší za",
"neverExpire": "Nikdy nevyprší", "neverExpire": "Nikdy nevyprší",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.", "shareExpireDescription": "Doba platnosti určuje, jak dlouho bude odkaz použitelný a bude poskytovat přístup ke zdroji. Po této době odkaz již nebude fungovat a uživatelé kteří tento odkaz používali ztratí přístup ke zdroji.",
"shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Nezapomeňte jej zkopírovat.", "shareSeeOnce": "Tento odkaz uvidíte pouze jednou. Nezapomeňte jej zkopírovat.",
"shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.", "shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Seznam pozvánek", "actionListInvitations": "Seznam pozvánek",
"actionExportLogs": "Exportovat protokoly", "actionExportLogs": "Exportovat protokoly",
"actionViewLogs": "Zobrazit logy", "actionViewLogs": "Zobrazit logy",
"actionCreateSiteProvisioningKey": "Vytvořit klíč pro zřízení webu",
"actionListSiteProvisioningKeys": "Seznam klíčů pro zřízení webu",
"actionUpdateSiteProvisioningKey": "Aktualizovat klíč pro zřízení webu",
"actionDeleteSiteProvisioningKey": "Smazat klíč pro zřízení webu",
"noneSelected": "Není vybráno", "noneSelected": "Není vybráno",
"orgNotFound2": "Nebyly nalezeny žádné organizace.", "orgNotFound2": "Nebyly nalezeny žádné organizace.",
"search": "Vyhledávání…", "search": "Vyhledávání…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Zdrojová adresa", "sourceAddress": "Zdrojová adresa",
"destinationAddress": "Cílová adresa", "destinationAddress": "Cílová adresa",
"duration": "Doba trvání", "duration": "Doba trvání",
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> nebo <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezervujte si demo nebo POC zkušební verzi</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce. Tato funkce je také k dispozici v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Rezervujte si demo nebo POC zkušební verzi</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Oddělovač certifikátů", "certResolver": "Oddělovač certifikátů",
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.", "certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
"selectCertResolver": "Vyberte řešič certifikátů", "selectCertResolver": "Vyberte řešič certifikátů",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Nahrání selhalo", "rdpUploadFailed": "Nahrání selhalo",
"rdpUnicodeKeyboardMode": "Režim Unicode klávesnice", "rdpUnicodeKeyboardMode": "Režim Unicode klávesnice",
"sessionToolbarShow": "Zobrazit panel nástrojů", "sessionToolbarShow": "Zobrazit panel nástrojů",
"sessionToolbarHide": "Skrýt panel nástrojů" "sessionToolbarHide": "Skrýt panel nástrojů",
"actionUpdateSiteApprovals": "Aktualizovat schválení webu"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bekræft sletning af delbar link", "shareDeleteConfirm": "Bekræft sletning af delbar link",
"shareQuestionRemove": "Er du sikker på, at du vil slette dette delingslink?", "shareQuestionRemove": "Er du sikker på, at du vil slette dette delingslink?",
"shareMessageRemove": "Når linket er slettet, vil det ikke længere fungere, og alle, der bruger det, mister adgang til ressourcen.", "shareMessageRemove": "Når linket er slettet, vil det ikke længere fungere, og alle, der bruger det, mister adgang til ressourcen.",
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en queryparameter eller i request-headers. Disse skal sendes fra klienten på hver forespørgsel om autentificeret adgang.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Adgangstoken", "accessToken": "Adgangstoken",
"usageExamples": "Brukseksempler", "usageExamples": "Brukseksempler",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (valgfrit)", "shareTitleOptional": "Titel (valgfrit)",
"sharePathOptional": "Sti (valgfrit)", "sharePathOptional": "Sti (valgfrit)",
"sharePathDescription": "Linket vil videresende brugere til denne stien efter autentificering.", "sharePathDescription": "Linket vil videresende brugere til denne stien efter autentificering.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Udløber om", "expireIn": "Udløber om",
"neverExpire": "Udløber aldrig", "neverExpire": "Udløber aldrig",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Udløbstid er hvor længe linket vil være brukbar og give adgang til ressourcen. Efter denne tiden vil linket ikke længere fungere, og brugere som brugte denne linket vil miste adgangen til ressourcen.", "shareExpireDescription": "Udløbstid er hvor længe linket vil være brukbar og give adgang til ressourcen. Efter denne tiden vil linket ikke længere fungere, og brugere som brugte denne linket vil miste adgangen til ressourcen.",
"shareSeeOnce": "Du vil kun kunne se dette link én gang. Sørg for at kopiere det.", "shareSeeOnce": "Du vil kun kunne se dette link én gang. Sørg for at kopiere det.",
"shareAccessHint": "Alle med denne linket kan få adgang til ressourcen. Del forsiktig.", "shareAccessHint": "Alle med denne linket kan få adgang til ressourcen. Del forsiktig.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Vis invitationer", "actionListInvitations": "Vis invitationer",
"actionExportLogs": "Eksportér logs", "actionExportLogs": "Eksportér logs",
"actionViewLogs": "Vis logs", "actionViewLogs": "Vis logs",
"actionCreateSiteProvisioningKey": "Opret provisioneringsnøgle til site",
"actionListSiteProvisioningKeys": "Vis provisioneringsnøgler til site",
"actionUpdateSiteProvisioningKey": "Opdater provisioneringsnøgle til site",
"actionDeleteSiteProvisioningKey": "Slet provisioneringsnøgle til site",
"noneSelected": "Ingen valgt", "noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisationer fundet.", "orgNotFound2": "Ingen organisationer fundet.",
"search": "Søg…", "search": "Søg…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Kildeadresse", "sourceAddress": "Kildeadresse",
"destinationAddress": "Måladresse ", "destinationAddress": "Måladresse ",
"duration": "Varighed", "duration": "Varighed",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>-licens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrævet for at bruge denne funktion. <bookADemoLink>Book en demo eller POC-prøveversion</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for at bruge denne funktion. Denne funktion er også tilgængelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book en demo eller POC-prøve</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Certifikatløser", "certResolver": "Certifikatløser",
"certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.", "certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.",
"selectCertResolver": "Vælg certifikatløser", "selectCertResolver": "Vælg certifikatløser",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Uploaden mislykkedes", "rdpUploadFailed": "Uploaden mislykkedes",
"rdpUnicodeKeyboardMode": "Unicode tastaturtilstand", "rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
"sessionToolbarShow": "Vis værktøjslinje", "sessionToolbarShow": "Vis værktøjslinje",
"sessionToolbarHide": "Skjul værktøjslinje" "sessionToolbarHide": "Skjul værktøjslinje",
"actionUpdateSiteApprovals": "Opdater godkendelser på site"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Löschung des Freigabelinks bestätigen", "shareDeleteConfirm": "Löschung des Freigabelinks bestätigen",
"shareQuestionRemove": "Sind Sie sicher, dass Sie diesen Freigabelink löschen möchten?", "shareQuestionRemove": "Sind Sie sicher, dass Sie diesen Freigabelink löschen möchten?",
"shareMessageRemove": "Nach dem Löschen funktioniert der Link nicht mehr, und jeder, der ihn nutzt, verliert den Zugriff auf die Ressource.", "shareMessageRemove": "Nach dem Löschen funktioniert der Link nicht mehr, und jeder, der ihn nutzt, verliert den Zugriff auf die Ressource.",
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Zugriffstoken", "accessToken": "Zugriffstoken",
"usageExamples": "Nutzungsbeispiele", "usageExamples": "Nutzungsbeispiele",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (optional)", "shareTitleOptional": "Titel (optional)",
"sharePathOptional": "Pfad (optional)", "sharePathOptional": "Pfad (optional)",
"sharePathDescription": "Der Link leitet Benutzer nach der Authentifizierung zu diesem Pfad weiter.", "sharePathDescription": "Der Link leitet Benutzer nach der Authentifizierung zu diesem Pfad weiter.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Läuft ab in", "expireIn": "Läuft ab in",
"neverExpire": "Läuft nie ab", "neverExpire": "Läuft nie ab",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Ablaufzeit ist, wie lange der Link verwendet werden kann und bietet Zugriff auf die Ressource. Nach dieser Zeit wird der Link nicht mehr funktionieren und Benutzer, die diesen Link benutzt haben, verlieren den Zugriff auf die Ressource.", "shareExpireDescription": "Ablaufzeit ist, wie lange der Link verwendet werden kann und bietet Zugriff auf die Ressource. Nach dieser Zeit wird der Link nicht mehr funktionieren und Benutzer, die diesen Link benutzt haben, verlieren den Zugriff auf die Ressource.",
"shareSeeOnce": "Sie können diesen Link nur einmal sehen. Bitte kopieren Sie ihn.", "shareSeeOnce": "Sie können diesen Link nur einmal sehen. Bitte kopieren Sie ihn.",
"shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.", "shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Einladungen auflisten", "actionListInvitations": "Einladungen auflisten",
"actionExportLogs": "Logs exportieren", "actionExportLogs": "Logs exportieren",
"actionViewLogs": "Logs anzeigen", "actionViewLogs": "Logs anzeigen",
"actionCreateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort erstellen",
"actionListSiteProvisioningKeys": "Liste der Bereitstellungsschlüssel für Standorte",
"actionUpdateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort aktualisieren",
"actionDeleteSiteProvisioningKey": "Bereitstellungsschlüssel für Standort löschen",
"noneSelected": "Keine ausgewählt", "noneSelected": "Keine ausgewählt",
"orgNotFound2": "Keine Organisationen gefunden.", "orgNotFound2": "Keine Organisationen gefunden.",
"search": "Suche…", "search": "Suche…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Quelladresse", "sourceAddress": "Quelladresse",
"destinationAddress": "Zieladresse", "destinationAddress": "Zieladresse",
"duration": "Dauer", "duration": "Dauer",
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion nutzen zu können. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> wird benötigt, um diese Funktion nutzen zu können. Diese Funktion ist auch in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>verfügbar. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Zertifikatsauflöser", "certResolver": "Zertifikatsauflöser",
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.", "certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
"selectCertResolver": "Zertifikatsauflöser auswählen", "selectCertResolver": "Zertifikatsauflöser auswählen",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Upload fehlgeschlagen", "rdpUploadFailed": "Upload fehlgeschlagen",
"rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus", "rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus",
"sessionToolbarShow": "Werkzeugleiste zeigen", "sessionToolbarShow": "Werkzeugleiste zeigen",
"sessionToolbarHide": "Werkzeugleiste ausblenden" "sessionToolbarHide": "Werkzeugleiste ausblenden",
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirm Delete Shareable Link", "shareDeleteConfirm": "Confirm Delete Shareable Link",
"shareQuestionRemove": "Are you sure you want to delete this share link?", "shareQuestionRemove": "Are you sure you want to delete this share link?",
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.", "shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Access Token", "accessToken": "Access Token",
"usageExamples": "Usage Examples", "usageExamples": "Usage Examples",
"tokenId": "Token ID", "tokenId": "Token ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Title (optional)", "shareTitleOptional": "Title (optional)",
"sharePathOptional": "Path (optional)", "sharePathOptional": "Path (optional)",
"sharePathDescription": "The link will redirect users to this path after authentication.", "sharePathDescription": "The link will redirect users to this path after authentication.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Expire In", "expireIn": "Expire In",
"neverExpire": "Never expire", "neverExpire": "Never expire",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.", "shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
"shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.", "shareSeeOnce": "You will only be able to see this link once. Make sure to copy it.",
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.", "shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "List Invitations", "actionListInvitations": "List Invitations",
"actionExportLogs": "Export Logs", "actionExportLogs": "Export Logs",
"actionViewLogs": "View Logs", "actionViewLogs": "View Logs",
"actionCreateSiteProvisioningKey": "Create Site Provisioning Key",
"actionListSiteProvisioningKeys": "List Site Provisioning Keys",
"actionUpdateSiteProvisioningKey": "Update Site Provisioning Key",
"actionDeleteSiteProvisioningKey": "Delete Site Provisioning Key",
"noneSelected": "None selected", "noneSelected": "None selected",
"orgNotFound2": "No organizations found.", "orgNotFound2": "No organizations found.",
"search": "Search…", "search": "Search…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Source Address", "sourceAddress": "Source Address",
"destinationAddress": "Destination Address", "destinationAddress": "Destination Address",
"duration": "Duration", "duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Certificate Resolver", "certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.", "certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver", "selectCertResolver": "Select Certificate Resolver",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Upload failed", "rdpUploadFailed": "Upload failed",
"rdpUnicodeKeyboardMode": "Unicode keyboard mode", "rdpUnicodeKeyboardMode": "Unicode keyboard mode",
"sessionToolbarShow": "Show toolbar", "sessionToolbarShow": "Show toolbar",
"sessionToolbarHide": "Hide toolbar" "sessionToolbarHide": "Hide toolbar",
"actionUpdateSiteApprovals": "Update Site Approvals"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmar Eliminación de Enlace Compartible", "shareDeleteConfirm": "Confirmar Eliminación de Enlace Compartible",
"shareQuestionRemove": "¿Está seguro de que desea borrar este enlace compartido?", "shareQuestionRemove": "¿Está seguro de que desea borrar este enlace compartido?",
"shareMessageRemove": "Una vez borrado, el enlace dejará de funcionar y cualquier persona que lo use perderá acceso al recurso.", "shareMessageRemove": "Una vez borrado, el enlace dejará de funcionar y cualquier persona que lo use perderá acceso al recurso.",
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Token de acceso", "accessToken": "Token de acceso",
"usageExamples": "Ejemplos de uso", "usageExamples": "Ejemplos de uso",
"tokenId": "ID de token", "tokenId": "ID de token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Título (opcional)", "shareTitleOptional": "Título (opcional)",
"sharePathOptional": "Ruta (opcional)", "sharePathOptional": "Ruta (opcional)",
"sharePathDescription": "El enlace redirigirá a los usuarios a esta ruta tras la autenticación.", "sharePathDescription": "El enlace redirigirá a los usuarios a esta ruta tras la autenticación.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Caduca en", "expireIn": "Caduca en",
"neverExpire": "Nunca expirar", "neverExpire": "Nunca expirar",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "El tiempo de caducidad es cuánto tiempo el enlace será utilizable y proporcionará acceso al recurso. Después de este tiempo, el enlace ya no funcionará, y los usuarios que usaron este enlace perderán el acceso al recurso.", "shareExpireDescription": "El tiempo de caducidad es cuánto tiempo el enlace será utilizable y proporcionará acceso al recurso. Después de este tiempo, el enlace ya no funcionará, y los usuarios que usaron este enlace perderán el acceso al recurso.",
"shareSeeOnce": "Sólo podrás ver este enlace una vez. Asegúrate de copiarlo.", "shareSeeOnce": "Sólo podrás ver este enlace una vez. Asegúrate de copiarlo.",
"shareAccessHint": "Cualquiera con este enlace puede acceder al recurso. Compártelo con cuidado.", "shareAccessHint": "Cualquiera con este enlace puede acceder al recurso. Compártelo con cuidado.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Listar invitaciones", "actionListInvitations": "Listar invitaciones",
"actionExportLogs": "Exportar registros", "actionExportLogs": "Exportar registros",
"actionViewLogs": "Ver registros", "actionViewLogs": "Ver registros",
"actionCreateSiteProvisioningKey": "Crear clave de aprovisionamiento del sitio",
"actionListSiteProvisioningKeys": "Listado de claves de aprovisionamiento del sitio",
"actionUpdateSiteProvisioningKey": "Actualizar clave de aprovisionamiento del sitio",
"actionDeleteSiteProvisioningKey": "Eliminar clave de aprovisionamiento del sitio",
"noneSelected": "Ninguno seleccionado", "noneSelected": "Ninguno seleccionado",
"orgNotFound2": "No se encontraron organizaciones.", "orgNotFound2": "No se encontraron organizaciones.",
"search": "Buscar…", "search": "Buscar…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Dirección de origen", "sourceAddress": "Dirección de origen",
"destinationAddress": "Dirección de destino", "destinationAddress": "Dirección de destino",
"duration": "Duración", "duration": "Duración",
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> para usar esta función. <bookADemoLink>Reserve una demostración o prueba POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> es necesaria para utilizar esta función. Esta función también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserva una demostración o prueba POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Resolver certificado", "certResolver": "Resolver certificado",
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.", "certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
"selectCertResolver": "Seleccionar Resolver Certificado", "selectCertResolver": "Seleccionar Resolver Certificado",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Error de subida", "rdpUploadFailed": "Error de subida",
"rdpUnicodeKeyboardMode": "Modo teclado Unicode", "rdpUnicodeKeyboardMode": "Modo teclado Unicode",
"sessionToolbarShow": "Mostrar barra de herramientas", "sessionToolbarShow": "Mostrar barra de herramientas",
"sessionToolbarHide": "Ocultar barra de herramientas" "sessionToolbarHide": "Ocultar barra de herramientas",
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio"
} }
+6 -1
View File
@@ -1511,6 +1511,10 @@
"actionListInvitations": "Lister les invitations", "actionListInvitations": "Lister les invitations",
"actionExportLogs": "Exporter les journaux", "actionExportLogs": "Exporter les journaux",
"actionViewLogs": "Voir les logs", "actionViewLogs": "Voir les logs",
"actionCreateSiteProvisioningKey": "Créer une clé de provisionnement de site",
"actionListSiteProvisioningKeys": "Lister les clés de provisionnement de site",
"actionUpdateSiteProvisioningKey": "Mettre à jour la clé de provisionnement de site",
"actionDeleteSiteProvisioningKey": "Supprimer la clé de provisionnement de site",
"noneSelected": "Aucune sélection", "noneSelected": "Aucune sélection",
"orgNotFound2": "Aucune organisation trouvée.", "orgNotFound2": "Aucune organisation trouvée.",
"search": "Rechercher…", "search": "Rechercher…",
@@ -3811,5 +3815,6 @@
"rdpUploadFailed": "Échec du téléchargement", "rdpUploadFailed": "Échec du téléchargement",
"rdpUnicodeKeyboardMode": "Mode clavier Unicode", "rdpUnicodeKeyboardMode": "Mode clavier Unicode",
"sessionToolbarShow": "Afficher la barre d'outils", "sessionToolbarShow": "Afficher la barre d'outils",
"sessionToolbarHide": "Masquer la barre d'outils" "sessionToolbarHide": "Masquer la barre d'outils",
"actionUpdateSiteApprovals": "Mettre à jour les approbations de site"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Conferma Eliminazione Collegamento Condivisibile", "shareDeleteConfirm": "Conferma Eliminazione Collegamento Condivisibile",
"shareQuestionRemove": "Sei sicuro di voler eliminare questo link di condivisione?", "shareQuestionRemove": "Sei sicuro di voler eliminare questo link di condivisione?",
"shareMessageRemove": "Una volta eliminato, il link non funzionerà più e chiunque lo utilizzi perderà l'accesso alla risorsa.", "shareMessageRemove": "Una volta eliminato, il link non funzionerà più e chiunque lo utilizzi perderà l'accesso alla risorsa.",
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Token Di Accesso", "accessToken": "Token Di Accesso",
"usageExamples": "Esempi Di Utilizzo", "usageExamples": "Esempi Di Utilizzo",
"tokenId": "ID del Token", "tokenId": "ID del Token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titolo (facoltativo)", "shareTitleOptional": "Titolo (facoltativo)",
"sharePathOptional": "Percorso (opzionale)", "sharePathOptional": "Percorso (opzionale)",
"sharePathDescription": "Il link reindirizzerà gli utenti a questo percorso dopo l'autenticazione.", "sharePathDescription": "Il link reindirizzerà gli utenti a questo percorso dopo l'autenticazione.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Scadenza In", "expireIn": "Scadenza In",
"neverExpire": "Nessuna scadenza", "neverExpire": "Nessuna scadenza",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Il tempo di scadenza indica per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.", "shareExpireDescription": "Il tempo di scadenza indica per quanto tempo il link sarà utilizzabile e fornirà accesso alla risorsa. Dopo questo tempo, il link non funzionerà più e gli utenti che hanno utilizzato questo link perderanno l'accesso alla risorsa.",
"shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.", "shareSeeOnce": "Potrai vedere questo link solo una volta. Assicurati di copiarlo.",
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.", "shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Elenco Inviti", "actionListInvitations": "Elenco Inviti",
"actionExportLogs": "Esporta Log", "actionExportLogs": "Esporta Log",
"actionViewLogs": "Visualizza Log", "actionViewLogs": "Visualizza Log",
"actionCreateSiteProvisioningKey": "Crea Chiave di Provisioning del Sito",
"actionListSiteProvisioningKeys": "Elenca Chiavi di Provisioning del Sito",
"actionUpdateSiteProvisioningKey": "Aggiorna Chiave di Provisioning del Sito",
"actionDeleteSiteProvisioningKey": "Elimina Chiave di Provisioning del Sito",
"noneSelected": "Nessuna selezione", "noneSelected": "Nessuna selezione",
"orgNotFound2": "Nessuna organizzazione trovata.", "orgNotFound2": "Nessuna organizzazione trovata.",
"search": "Cerca…", "search": "Cerca…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Indirizzo Di Origine", "sourceAddress": "Indirizzo Di Origine",
"destinationAddress": "Indirizzo Di Destinazione", "destinationAddress": "Indirizzo Di Destinazione",
"duration": "Durata", "duration": "Durata",
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Prenota una demo o una prova POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione. Questa funzione è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Prenota una demo o una prova POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Risolutore Di Certificato", "certResolver": "Risolutore Di Certificato",
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.", "certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
"selectCertResolver": "Seleziona Risolutore Di Certificato", "selectCertResolver": "Seleziona Risolutore Di Certificato",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Caricamento fallito", "rdpUploadFailed": "Caricamento fallito",
"rdpUnicodeKeyboardMode": "Modalità tastiera Unicode", "rdpUnicodeKeyboardMode": "Modalità tastiera Unicode",
"sessionToolbarShow": "Mostra barra degli strumenti", "sessionToolbarShow": "Mostra barra degli strumenti",
"sessionToolbarHide": "Nascondi barra degli strumenti" "sessionToolbarHide": "Nascondi barra degli strumenti",
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "공유 가능한 링크 삭제 확인", "shareDeleteConfirm": "공유 가능한 링크 삭제 확인",
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?", "shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.", "shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "액세스 토큰", "accessToken": "액세스 토큰",
"usageExamples": "사용 예", "usageExamples": "사용 예",
"tokenId": "토큰 ID", "tokenId": "토큰 ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "제목 (선택 사항)", "shareTitleOptional": "제목 (선택 사항)",
"sharePathOptional": "경로 (선택 사항)", "sharePathOptional": "경로 (선택 사항)",
"sharePathDescription": "링크는 인증 후 이 경로로 사용자를 리디렉션합니다.", "sharePathDescription": "링크는 인증 후 이 경로로 사용자를 리디렉션합니다.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "만료됨", "expireIn": "만료됨",
"neverExpire": "만료되지 않음", "neverExpire": "만료되지 않음",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.", "shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.",
"shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.", "shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.",
"shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.", "shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "초대 목록", "actionListInvitations": "초대 목록",
"actionExportLogs": "로그 내보내기", "actionExportLogs": "로그 내보내기",
"actionViewLogs": "로그 보기", "actionViewLogs": "로그 보기",
"actionCreateSiteProvisioningKey": "사이트 프로비저닝 키 생성",
"actionListSiteProvisioningKeys": "사이트 프로비저닝 키 목록",
"actionUpdateSiteProvisioningKey": "사이트 프로비저닝 키 업데이트",
"actionDeleteSiteProvisioningKey": "사이트 프로비저닝 키 삭제",
"noneSelected": "선택된 항목 없음", "noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.", "orgNotFound2": "조직이 없습니다.",
"search": "검색…", "search": "검색…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "소스 주소", "sourceAddress": "소스 주소",
"destinationAddress": "대상 주소", "destinationAddress": "대상 주소",
"duration": "지속 시간", "duration": "지속 시간",
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이(가) 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "인증서 해결사", "certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.", "certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택", "selectCertResolver": "인증서 해결사 선택",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "업로드 실패", "rdpUploadFailed": "업로드 실패",
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드", "rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
"sessionToolbarShow": "툴바 보기", "sessionToolbarShow": "툴바 보기",
"sessionToolbarHide": "툴바 숨기기" "sessionToolbarHide": "툴바 숨기기",
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bekreft sletting av delbar lenke", "shareDeleteConfirm": "Bekreft sletting av delbar lenke",
"shareQuestionRemove": "Er du sikker på at du vil slette denne delingslenken?", "shareQuestionRemove": "Er du sikker på at du vil slette denne delingslenken?",
"shareMessageRemove": "Når slettet, vil lenken ikke lenger fungere, og alle som bruker den vil miste tilgang til ressursen.", "shareMessageRemove": "Når slettet, vil lenken ikke lenger fungere, og alle som bruker den vil miste tilgang til ressursen.",
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Tilgangsnøkkel", "accessToken": "Tilgangsnøkkel",
"usageExamples": "Brukseksempler", "usageExamples": "Brukseksempler",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Tittel (valgfritt)", "shareTitleOptional": "Tittel (valgfritt)",
"sharePathOptional": "Bane (valgfritt)", "sharePathOptional": "Bane (valgfritt)",
"sharePathDescription": "Lenken vil videresende brukere til denne stien etter autentisering.", "sharePathDescription": "Lenken vil videresende brukere til denne stien etter autentisering.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Utløper om", "expireIn": "Utløper om",
"neverExpire": "Utløper aldri", "neverExpire": "Utløper aldri",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.", "shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.",
"shareSeeOnce": "Du vil bare kunne se denne linken én gang. Pass på å kopiere den.", "shareSeeOnce": "Du vil bare kunne se denne linken én gang. Pass på å kopiere den.",
"shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.", "shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Liste invitasjoner", "actionListInvitations": "Liste invitasjoner",
"actionExportLogs": "Eksportlogger", "actionExportLogs": "Eksportlogger",
"actionViewLogs": "Vis logger", "actionViewLogs": "Vis logger",
"actionCreateSiteProvisioningKey": "Opprett Klargjøringsnøkkel for Sted",
"actionListSiteProvisioningKeys": "List Klargjøringsnøkler for Sted",
"actionUpdateSiteProvisioningKey": "Oppdater Klargjøringsnøkkel for Sted",
"actionDeleteSiteProvisioningKey": "Slett Klargjøringsnøkkel for Sted",
"noneSelected": "Ingen valgt", "noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisasjoner funnet.", "orgNotFound2": "Ingen organisasjoner funnet.",
"search": "Søk…", "search": "Søk…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Kilde adresse", "sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)", "destinationAddress": "Måladresse (Automatic Translation)",
"duration": "Varighet", "duration": "Varighet",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrevd for å bruke denne funksjonen. <bookADemoLink>Bestill en demo eller POC prøveversjon</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Bestill en demo eller POC studie</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Sertifikat løser", "certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.", "certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser", "selectCertResolver": "Velg sertifikatløser",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Opplastningen mislyktes", "rdpUploadFailed": "Opplastningen mislyktes",
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus", "rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
"sessionToolbarShow": "Vis verktøylinje", "sessionToolbarShow": "Vis verktøylinje",
"sessionToolbarHide": "Skjul verktøylinje" "sessionToolbarHide": "Skjul verktøylinje",
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bevestig Verwijdering Deelbare Link", "shareDeleteConfirm": "Bevestig Verwijdering Deelbare Link",
"shareQuestionRemove": "Weet u zeker dat u deze deel link wilt verwijderen?", "shareQuestionRemove": "Weet u zeker dat u deze deel link wilt verwijderen?",
"shareMessageRemove": "Zodra verwijderd, zal de link niet meer werken en zal iedereen die het gebruikt de toegang tot de bron verliezen.", "shareMessageRemove": "Zodra verwijderd, zal de link niet meer werken en zal iedereen die het gebruikt de toegang tot de bron verliezen.",
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Toegangs-token", "accessToken": "Toegangs-token",
"usageExamples": "Voorbeelden van gebruik", "usageExamples": "Voorbeelden van gebruik",
"tokenId": "Token ID", "tokenId": "Token ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (optioneel)", "shareTitleOptional": "Titel (optioneel)",
"sharePathOptional": "Pad (optioneel)", "sharePathOptional": "Pad (optioneel)",
"sharePathDescription": "De link zal gebruikers naar dit pad doorsturen na authenticatie.", "sharePathDescription": "De link zal gebruikers naar dit pad doorsturen na authenticatie.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Vervalt in", "expireIn": "Vervalt in",
"neverExpire": "Nooit verlopen", "neverExpire": "Nooit verlopen",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Vervaltijd is hoe lang de link bruikbaar is en geeft toegang tot de bron. Na deze tijd zal de link niet meer werken en zullen gebruikers die deze link hebben gebruikt de toegang tot de pagina verliezen.", "shareExpireDescription": "Vervaltijd is hoe lang de link bruikbaar is en geeft toegang tot de bron. Na deze tijd zal de link niet meer werken en zullen gebruikers die deze link hebben gebruikt de toegang tot de pagina verliezen.",
"shareSeeOnce": "U kunt deze link slechts één keer zien. Zorg ervoor dat u deze kopieert.", "shareSeeOnce": "U kunt deze link slechts één keer zien. Zorg ervoor dat u deze kopieert.",
"shareAccessHint": "Iedereen met deze link heeft toegang tot de bron. Deel deze met zorg.", "shareAccessHint": "Iedereen met deze link heeft toegang tot de bron. Deel deze met zorg.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Toon uitnodigingen", "actionListInvitations": "Toon uitnodigingen",
"actionExportLogs": "Logboeken exporteren", "actionExportLogs": "Logboeken exporteren",
"actionViewLogs": "Logboeken bekijken", "actionViewLogs": "Logboeken bekijken",
"actionCreateSiteProvisioningKey": "Een sitevoorzie-ningssleutel aanmaken",
"actionListSiteProvisioningKeys": "Sitevoorzie-ningssleutels weergeven",
"actionUpdateSiteProvisioningKey": "Sitevoorzie-ningssleutel bijwerken",
"actionDeleteSiteProvisioningKey": "Sitevoorzie-ningssleutel verwijderen",
"noneSelected": "Niet geselecteerd", "noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.", "orgNotFound2": "Geen organisaties gevonden.",
"search": "Zoeken…", "search": "Zoeken…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Bron adres", "sourceAddress": "Bron adres",
"destinationAddress": "Adres bestemming", "destinationAddress": "Adres bestemming",
"duration": "Duur", "duration": "Duur",
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Certificaat Resolver", "certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.", "certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren", "selectCertResolver": "Certificaat Resolver selecteren",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Upload mislukt", "rdpUploadFailed": "Upload mislukt",
"rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus", "rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus",
"sessionToolbarShow": "Toon werkbalk", "sessionToolbarShow": "Toon werkbalk",
"sessionToolbarHide": "Verberg werkbalk" "sessionToolbarHide": "Verberg werkbalk",
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Potwierdź usunięcie linku do udostępnienia", "shareDeleteConfirm": "Potwierdź usunięcie linku do udostępnienia",
"shareQuestionRemove": "Czy na pewno chcesz usunąć ten link udostępniania?", "shareQuestionRemove": "Czy na pewno chcesz usunąć ten link udostępniania?",
"shareMessageRemove": "Po usunięciu, link przestanie działać i wszyscy korzystający z niego stracą dostęp do zasobu.", "shareMessageRemove": "Po usunięciu, link przestanie działać i wszyscy korzystający z niego stracą dostęp do zasobu.",
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Token dostępu", "accessToken": "Token dostępu",
"usageExamples": "Przykłady użycia", "usageExamples": "Przykłady użycia",
"tokenId": "Identyfikator tokena", "tokenId": "Identyfikator tokena",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Tytuł (opcjonalnie)", "shareTitleOptional": "Tytuł (opcjonalnie)",
"sharePathOptional": "Ścieżka (opcjonalnie)", "sharePathOptional": "Ścieżka (opcjonalnie)",
"sharePathDescription": "Link przekieruje użytkowników do tej ścieżki po uwierzytelnieniu.", "sharePathDescription": "Link przekieruje użytkowników do tej ścieżki po uwierzytelnieniu.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Wygasa za", "expireIn": "Wygasa za",
"neverExpire": "Nigdy nie wygasa", "neverExpire": "Nigdy nie wygasa",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Czas wygaśnięcia to jak długo link będzie mógł być użyty i zapewni dostęp do zasobu. Po tym czasie link nie będzie już działał, a użytkownicy, którzy użyli tego linku, utracą dostęp do zasobu.", "shareExpireDescription": "Czas wygaśnięcia to jak długo link będzie mógł być użyty i zapewni dostęp do zasobu. Po tym czasie link nie będzie już działał, a użytkownicy, którzy użyli tego linku, utracą dostęp do zasobu.",
"shareSeeOnce": "Możesz zobaczyć ten link tylko raz. Pamiętaj, aby go skopiować.", "shareSeeOnce": "Możesz zobaczyć ten link tylko raz. Pamiętaj, aby go skopiować.",
"shareAccessHint": "Każdy z tym linkiem może uzyskać dostęp do zasobu. Podziel się nim ostrożnie.", "shareAccessHint": "Każdy z tym linkiem może uzyskać dostęp do zasobu. Podziel się nim ostrożnie.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Lista zaproszeń", "actionListInvitations": "Lista zaproszeń",
"actionExportLogs": "Eksportuj dzienniki", "actionExportLogs": "Eksportuj dzienniki",
"actionViewLogs": "Zobacz dzienniki", "actionViewLogs": "Zobacz dzienniki",
"actionCreateSiteProvisioningKey": "Utwórz klucz konfiguracji strony",
"actionListSiteProvisioningKeys": "Lista kluczy konfiguracji strony",
"actionUpdateSiteProvisioningKey": "Zaktualizuj klucz konfiguracji strony",
"actionDeleteSiteProvisioningKey": "Usuń klucz konfiguracji strony",
"noneSelected": "Nie wybrano", "noneSelected": "Nie wybrano",
"orgNotFound2": "Nie znaleziono organizacji.", "orgNotFound2": "Nie znaleziono organizacji.",
"search": "Szukaj…", "search": "Szukaj…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Adres źródłowy", "sourceAddress": "Adres źródłowy",
"destinationAddress": "Adres docelowy", "destinationAddress": "Adres docelowy",
"duration": "Czas trwania", "duration": "Czas trwania",
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lub <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezerwuj wersję demonstracyjną lub wersję próbną POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezerwuj demo lub okres próbny POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Rozwiązywanie certyfikatów", "certResolver": "Rozwiązywanie certyfikatów",
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.", "certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
"selectCertResolver": "Wybierz Resolver certyfikatów", "selectCertResolver": "Wybierz Resolver certyfikatów",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Niepowodzenie przesyłania", "rdpUploadFailed": "Niepowodzenie przesyłania",
"rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode", "rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode",
"sessionToolbarShow": "Pokaż pasek narzędzi", "sessionToolbarShow": "Pokaż pasek narzędzi",
"sessionToolbarHide": "Ukryj pasek narzędzi" "sessionToolbarHide": "Ukryj pasek narzędzi",
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmar exclusão do Link Compartilhável", "shareDeleteConfirm": "Confirmar exclusão do Link Compartilhável",
"shareQuestionRemove": "Tem certeza de que deseja excluir este link de compartilhamento?", "shareQuestionRemove": "Tem certeza de que deseja excluir este link de compartilhamento?",
"shareMessageRemove": "Uma vez excluído, o link não funcionará mais e qualquer pessoa que o utilizar perderá o acesso ao recurso.", "shareMessageRemove": "Uma vez excluído, o link não funcionará mais e qualquer pessoa que o utilizar perderá o acesso ao recurso.",
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Token de acesso", "accessToken": "Token de acesso",
"usageExamples": "Exemplos de uso", "usageExamples": "Exemplos de uso",
"tokenId": "ID do Token", "tokenId": "ID do Token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Título (opcional)", "shareTitleOptional": "Título (opcional)",
"sharePathOptional": "Caminho (opcional)", "sharePathOptional": "Caminho (opcional)",
"sharePathDescription": "O link redirecionará os usuários para este caminho após a autenticação.", "sharePathDescription": "O link redirecionará os usuários para este caminho após a autenticação.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Expira em", "expireIn": "Expira em",
"neverExpire": "Nunca expirar", "neverExpire": "Nunca expirar",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Tempo de expiração é quanto tempo o link será utilizável e oferecerá acesso ao recurso. Após este tempo, o link não funcionará mais, e os utilizadores que usaram este link perderão acesso ao recurso.", "shareExpireDescription": "Tempo de expiração é quanto tempo o link será utilizável e oferecerá acesso ao recurso. Após este tempo, o link não funcionará mais, e os utilizadores que usaram este link perderão acesso ao recurso.",
"shareSeeOnce": "Você só poderá ver este link uma vez. Certifique-se de copiá-lo.", "shareSeeOnce": "Você só poderá ver este link uma vez. Certifique-se de copiá-lo.",
"shareAccessHint": "Qualquer um com este link pode aceder o recurso. Compartilhe com cuidado.", "shareAccessHint": "Qualquer um com este link pode aceder o recurso. Compartilhe com cuidado.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Listar Convites", "actionListInvitations": "Listar Convites",
"actionExportLogs": "Exportar logs", "actionExportLogs": "Exportar logs",
"actionViewLogs": "Visualizar registros", "actionViewLogs": "Visualizar registros",
"actionCreateSiteProvisioningKey": "Criar Chave de Provisionamento do Site",
"actionListSiteProvisioningKeys": "Listar Chaves de Provisionamento do Site",
"actionUpdateSiteProvisioningKey": "Atualizar Chave de Provisionamento do Site",
"actionDeleteSiteProvisioningKey": "Excluir Chave de Provisionamento do Site",
"noneSelected": "Nenhum selecionado", "noneSelected": "Nenhum selecionado",
"orgNotFound2": "Nenhuma organização encontrada.", "orgNotFound2": "Nenhuma organização encontrada.",
"search": "Pesquisar…", "search": "Pesquisar…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Endereço de origem", "sourceAddress": "Endereço de origem",
"destinationAddress": "Endereço de destino", "destinationAddress": "Endereço de destino",
"duration": "Duração", "duration": "Duração",
"licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> é necessária para usar este recurso. <bookADemoLink>Reserve um teste de demonstração ou POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserve uma demonstração ou avaliação POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Resolvedor de Certificado", "certResolver": "Resolvedor de Certificado",
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.", "certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
"selectCertResolver": "Selecionar solucionador de certificado", "selectCertResolver": "Selecionar solucionador de certificado",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Falha no upload", "rdpUploadFailed": "Falha no upload",
"rdpUnicodeKeyboardMode": "Modo de teclado Unicode", "rdpUnicodeKeyboardMode": "Modo de teclado Unicode",
"sessionToolbarShow": "Mostrar barra de ferramentas", "sessionToolbarShow": "Mostrar barra de ferramentas",
"sessionToolbarHide": "Ocultar barra de ferramentas" "sessionToolbarHide": "Ocultar barra de ferramentas",
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Подтвердить удаление общей ссылки", "shareDeleteConfirm": "Подтвердить удаление общей ссылки",
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?", "shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.", "shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Токен доступа", "accessToken": "Токен доступа",
"usageExamples": "Примеры использования", "usageExamples": "Примеры использования",
"tokenId": "ID токена", "tokenId": "ID токена",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Заголовок (необязательно)", "shareTitleOptional": "Заголовок (необязательно)",
"sharePathOptional": "Путь (необязательно)", "sharePathOptional": "Путь (необязательно)",
"sharePathDescription": "Ссылка перенаправит пользователей на этот путь после аутентификации.", "sharePathDescription": "Ссылка перенаправит пользователей на этот путь после аутентификации.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Срок действия", "expireIn": "Срок действия",
"neverExpire": "Бессрочный доступ", "neverExpire": "Бессрочный доступ",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Срок действия - это период, в течение которого ссылка будет работать и предоставлять доступ к ресурсу. После этого времени ссылка перестанет работать, и пользователи, использовавшие эту ссылку, потеряют доступ к ресурсу.", "shareExpireDescription": "Срок действия - это период, в течение которого ссылка будет работать и предоставлять доступ к ресурсу. После этого времени ссылка перестанет работать, и пользователи, использовавшие эту ссылку, потеряют доступ к ресурсу.",
"shareSeeOnce": "Вы сможете увидеть эту ссылку только один раз. Обязательно скопируйте ее.", "shareSeeOnce": "Вы сможете увидеть эту ссылку только один раз. Обязательно скопируйте ее.",
"shareAccessHint": "Любой, у кого есть эта ссылка, может получить доступ к ресурсу. Делитесь ею с осторожностью.", "shareAccessHint": "Любой, у кого есть эта ссылка, может получить доступ к ресурсу. Делитесь ею с осторожностью.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Список приглашений", "actionListInvitations": "Список приглашений",
"actionExportLogs": "Экспорт журналов", "actionExportLogs": "Экспорт журналов",
"actionViewLogs": "Просмотр журналов", "actionViewLogs": "Просмотр журналов",
"actionCreateSiteProvisioningKey": "Создать ключ конфигурации сайта",
"actionListSiteProvisioningKeys": "Список ключей конфигурации сайтов",
"actionUpdateSiteProvisioningKey": "Обновить ключ конфигурации сайта",
"actionDeleteSiteProvisioningKey": "Удалить ключ конфигурации сайта",
"noneSelected": "Ничего не выбрано", "noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.", "orgNotFound2": "Организации не найдены.",
"search": "Поиск…", "search": "Поиск…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Адрес источника", "sourceAddress": "Адрес источника",
"destinationAddress": "Адрес назначения", "destinationAddress": "Адрес назначения",
"duration": "Продолжительность", "duration": "Продолжительность",
"licenseRequiredToUse": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> для использования этой функции. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Резольвер сертификата", "certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.", "certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата", "selectCertResolver": "Выберите резолвер сертификата",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Ошибка загрузки", "rdpUploadFailed": "Ошибка загрузки",
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode", "rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
"sessionToolbarShow": "Показать панель инструментов", "sessionToolbarShow": "Показать панель инструментов",
"sessionToolbarHide": "Скрыть панель инструментов" "sessionToolbarHide": "Скрыть панель инструментов",
"actionUpdateSiteApprovals": "Обновить утверждения сайта"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Paylaşılabilir Bağlantıyı Silmeyi Onayla", "shareDeleteConfirm": "Paylaşılabilir Bağlantıyı Silmeyi Onayla",
"shareQuestionRemove": "Bu paylaşım bağlantısını silmek istediğinizden emin misiniz?", "shareQuestionRemove": "Bu paylaşım bağlantısını silmek istediğinizden emin misiniz?",
"shareMessageRemove": "Silindikten sonra, bağlantı artık çalışmayacak ve kullanan herkes kaynağa erişimini kaybedecek.", "shareMessageRemove": "Silindikten sonra, bağlantı artık çalışmayacak ve kullanan herkes kaynağa erişimini kaybedecek.",
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Erişim Jetonu", "accessToken": "Erişim Jetonu",
"usageExamples": "Kullanım Örnekleri", "usageExamples": "Kullanım Örnekleri",
"tokenId": "Jeton ID", "tokenId": "Jeton ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Başlık (isteğe bağlı)", "shareTitleOptional": "Başlık (isteğe bağlı)",
"sharePathOptional": "Yol (isteğe bağlı)", "sharePathOptional": "Yol (isteğe bağlı)",
"sharePathDescription": "Bağlantıdan sonra kullanıcıları bu yola yönlendirecek bağlantıyı tanımlayın.", "sharePathDescription": "Bağlantıdan sonra kullanıcıları bu yola yönlendirecek bağlantıyı tanımlayın.",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "Süresi Dolacak", "expireIn": "Süresi Dolacak",
"neverExpire": "Hiçbir Zaman Sona Ermez", "neverExpire": "Hiçbir Zaman Sona Ermez",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "Son kullanma süresi, bağlantının kullanılabilir ve kaynağa erişim sağlayacak süresidir. Bu süreden sonra bağlantı çalışmayı durduracak ve bu bağlantıyı kullanan kullanıcılar kaynağa erişimini kaybedecektir.", "shareExpireDescription": "Son kullanma süresi, bağlantının kullanılabilir ve kaynağa erişim sağlayacak süresidir. Bu süreden sonra bağlantı çalışmayı durduracak ve bu bağlantıyı kullanan kullanıcılar kaynağa erişimini kaybedecektir.",
"shareSeeOnce": "Bu bağlantıyı yalnızca bir kez görebileceksiniz. Kopyaladığınızdan emin olun.", "shareSeeOnce": "Bu bağlantıyı yalnızca bir kez görebileceksiniz. Kopyaladığınızdan emin olun.",
"shareAccessHint": "Bu bağlantıya sahip olan herkes kaynağa erişebilir. Dikkatle paylaşın.", "shareAccessHint": "Bu bağlantıya sahip olan herkes kaynağa erişebilir. Dikkatle paylaşın.",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "Davetiyeleri Listele", "actionListInvitations": "Davetiyeleri Listele",
"actionExportLogs": "Kayıtları Dışa Aktar", "actionExportLogs": "Kayıtları Dışa Aktar",
"actionViewLogs": "Kayıtları Görüntüle", "actionViewLogs": "Kayıtları Görüntüle",
"actionCreateSiteProvisioningKey": "Site Sağlama Anahtarı Oluştur",
"actionListSiteProvisioningKeys": "Site Sağlama Anahtarlarını Listele",
"actionUpdateSiteProvisioningKey": "Site Sağlama Anahtarını Güncelle",
"actionDeleteSiteProvisioningKey": "Site Sağlama Anahtarını Sil",
"noneSelected": "Hiçbiri seçili değil", "noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.", "orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"search": "Ara…", "search": "Ara…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "Kaynak Adresi", "sourceAddress": "Kaynak Adresi",
"destinationAddress": "Hedef Adresi", "destinationAddress": "Hedef Adresi",
"duration": "Süre", "duration": "Süre",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı veya <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> gereklidir. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>da da mevcuttur. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "Sertifika Çözücü", "certResolver": "Sertifika Çözücü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.", "certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin", "selectCertResolver": "Sertifika Çözücü Seçin",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "Yükleme başarısız", "rdpUploadFailed": "Yükleme başarısız",
"rdpUnicodeKeyboardMode": "Unicode klavye modu", "rdpUnicodeKeyboardMode": "Unicode klavye modu",
"sessionToolbarShow": "Araç çubuğunu göster", "sessionToolbarShow": "Araç çubuğunu göster",
"sessionToolbarHide": "Araç çubuğunu gizle" "sessionToolbarHide": "Araç çubuğunu gizle",
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle"
} }
+15 -4
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "确认删除共享链接", "shareDeleteConfirm": "确认删除共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?", "shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。", "shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。", "shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "访问令牌", "accessToken": "访问令牌",
"usageExamples": "用法示例", "usageExamples": "用法示例",
"tokenId": "令牌 ID", "tokenId": "令牌 ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "标题 (可选)", "shareTitleOptional": "标题 (可选)",
"sharePathOptional": "路径(可选)", "sharePathOptional": "路径(可选)",
"sharePathDescription": "认证后,链接将把用户重定向到此路径。", "sharePathDescription": "认证后,链接将把用户重定向到此路径。",
"shareAssociateUserOptional": "Associate User (optional)",
"shareAssociateUserDescription": "When set, requests using this link are attributed to the user in access logs and identity headers. The link is removed if the user leaves the organization.",
"userSelect": "Select user",
"usersNotFound": "No users found",
"expireIn": "过期时间", "expireIn": "过期时间",
"neverExpire": "永不过期", "neverExpire": "永不过期",
"sharePersistSession": "Persist session after first use",
"sharePersistSessionDescription": "When enabled, the first request with this token via a query param or header sets a session cookie so later requests do not need the token. Leave off for API clients that should send the token on every request.",
"shareExpireDescription": "过期时间是链接可以使用并提供对资源的访问时间。 此时间后,链接将不再工作,使用此链接的用户将失去对资源的访问。", "shareExpireDescription": "过期时间是链接可以使用并提供对资源的访问时间。 此时间后,链接将不再工作,使用此链接的用户将失去对资源的访问。",
"shareSeeOnce": "您只能看到一次此链接。请确保复制它。", "shareSeeOnce": "您只能看到一次此链接。请确保复制它。",
"shareAccessHint": "任何具有此链接的人都可以访问该资源。小心地分享它。", "shareAccessHint": "任何具有此链接的人都可以访问该资源。小心地分享它。",
@@ -1511,6 +1517,10 @@
"actionListInvitations": "邀请列表", "actionListInvitations": "邀请列表",
"actionExportLogs": "导出日志", "actionExportLogs": "导出日志",
"actionViewLogs": "查看日志", "actionViewLogs": "查看日志",
"actionCreateSiteProvisioningKey": "创建站点预配密钥",
"actionListSiteProvisioningKeys": "列出站点预配密钥",
"actionUpdateSiteProvisioningKey": "更新站点预配密钥",
"actionDeleteSiteProvisioningKey": "删除站点预配密钥",
"noneSelected": "未选择", "noneSelected": "未选择",
"orgNotFound2": "未找到组织。", "orgNotFound2": "未找到组织。",
"search": "搜索…", "search": "搜索…",
@@ -3085,8 +3095,8 @@
"sourceAddress": "源地址", "sourceAddress": "源地址",
"destinationAddress": "目的地址", "destinationAddress": "目的地址",
"duration": "期限", "duration": "期限",
"licenseRequiredToUse": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>。<bookADemoLink>预约演示或POC试用</bookADemoLink>", "licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "需要 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 才能使用此功能。 此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。 <bookADemoLink>预订演示或POC 试用</bookADemoLink>", "ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"certResolver": "证书解决器", "certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。", "certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析", "selectCertResolver": "选择证书解析",
@@ -3811,5 +3821,6 @@
"rdpUploadFailed": "上传失败", "rdpUploadFailed": "上传失败",
"rdpUnicodeKeyboardMode": "Unicode 键盘模式", "rdpUnicodeKeyboardMode": "Unicode 键盘模式",
"sessionToolbarShow": "显示工具栏", "sessionToolbarShow": "显示工具栏",
"sessionToolbarHide": "隐藏工具栏" "sessionToolbarHide": "隐藏工具栏",
"actionUpdateSiteApprovals": "更新站点审批"
} }
+5
View File
@@ -107,6 +107,7 @@ export const sites = pgTable(
lastPing: integer("lastPing"), lastPing: integer("lastPing"),
address: varchar("address"), address: varchar("address"),
endpoint: varchar("endpoint"), endpoint: varchar("endpoint"),
localEndpoints: varchar("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
publicKey: varchar("publicKey"), publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }), lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"), listenPort: integer("listenPort"),
@@ -905,12 +906,16 @@ export const resourceAccessToken = pgTable("resourceAccessToken", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
}),
path: varchar("path"), path: varchar("path"),
tokenHash: varchar("tokenHash").notNull(), tokenHash: varchar("tokenHash").notNull(),
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(), sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }), expiresAt: bigint("expiresAt", { mode: "number" }),
title: varchar("title"), title: varchar("title"),
description: varchar("description"), description: varchar("description"),
persistSession: boolean("persistSession").notNull().default(false),
createdAt: bigint("createdAt", { mode: "number" }).notNull() createdAt: bigint("createdAt", { mode: "number" }).notNull()
}); });
+7
View File
@@ -118,6 +118,7 @@ export const sites = sqliteTable("sites", {
// exit node stuff that is how to connect to the site when it has a wg server // exit node stuff that is how to connect to the site when it has a wg server
address: text("address"), // this is the address of the wireguard interface in newt address: text("address"), // this is the address of the wireguard interface in newt
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
localEndpoints: text("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
publicKey: text("publicKey"), // TODO: Fix typo in publicKey publicKey: text("publicKey"), // TODO: Fix typo in publicKey
lastHolePunch: integer("lastHolePunch"), lastHolePunch: integer("lastHolePunch"),
listenPort: integer("listenPort"), listenPort: integer("listenPort"),
@@ -1125,12 +1126,18 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
}),
path: text("path"), path: text("path"),
tokenHash: text("tokenHash").notNull(), tokenHash: text("tokenHash").notNull(),
sessionLength: integer("sessionLength").notNull(), sessionLength: integer("sessionLength").notNull(),
expiresAt: integer("expiresAt"), expiresAt: integer("expiresAt"),
title: text("title"), title: text("title"),
description: text("description"), description: text("description"),
persistSession: integer("persistSession", { mode: "boolean" })
.notNull()
.default(false),
createdAt: integer("createdAt").notNull() createdAt: integer("createdAt").notNull()
}); });
+46
View File
@@ -239,6 +239,31 @@ export async function updatePrivateResources(
); );
} }
if (resourceData.alias) {
const [aliasConflict] = await trx
.select({
siteResourceId: siteResources.siteResourceId
})
.from(siteResources)
.where(
and(
eq(siteResources.orgId, orgId),
eq(siteResources.alias, resourceData.alias),
ne(
siteResources.siteResourceId,
existingResource.siteResourceId
)
)
)
.limit(1);
if (aliasConflict) {
throw new Error(
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
);
}
}
// Update existing resource // Update existing resource
const [updatedResource] = await trx const [updatedResource] = await trx
.update(siteResources) .update(siteResources)
@@ -480,6 +505,27 @@ export async function updatePrivateResources(
); );
} }
if (resourceData.alias) {
const [aliasConflict] = await trx
.select({
siteResourceId: siteResources.siteResourceId
})
.from(siteResources)
.where(
and(
eq(siteResources.orgId, orgId),
eq(siteResources.alias, resourceData.alias)
)
)
.limit(1);
if (aliasConflict) {
throw new Error(
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
);
}
}
const [network] = await trx const [network] = await trx
.insert(networks) .insert(networks)
.values({ .values({
+49 -18
View File
@@ -8,26 +8,42 @@ const STATUS_HISTORY_CACHE_TTL = 60; // seconds
function statusHistoryCacheKey( function statusHistoryCacheKey(
entityType: string, entityType: string,
entityId: number, entityId: number,
days: number days: number,
tzOffsetMinutes: number
): string { ): string {
return `statusHistory:${entityType}:${entityId}:${days}`; return `statusHistory:${entityType}:${entityId}:${days}:${tzOffsetMinutes}`;
}
// Returns the epoch seconds of the most recent local-calendar-day midnight,
// where "local" is defined by tzOffsetMinutes (minutes to ADD to UTC to get
// local time, e.g. Australia/Sydney standard time is 600). Defaults to 0
// (UTC) so callers that don't pass a timezone keep the original behavior.
function localMidnightSec(tzOffsetMinutes: number): number {
const localNow = new Date(Date.now() + tzOffsetMinutes * 60_000);
localNow.setUTCHours(0, 0, 0, 0);
return Math.floor(localNow.getTime() / 1000) - tzOffsetMinutes * 60;
} }
export async function getCachedStatusHistory( export async function getCachedStatusHistory(
entityType: string, entityType: string,
entityId: number, entityId: number,
days: number days: number,
tzOffsetMinutes: number = 0
): Promise<StatusHistoryResponse> { ): Promise<StatusHistoryResponse> {
const cacheKey = statusHistoryCacheKey(entityType, entityId, days); const cacheKey = statusHistoryCacheKey(
entityType,
entityId,
days,
tzOffsetMinutes
);
const cached = await cache.get<StatusHistoryResponse>(cacheKey); const cached = await cache.get<StatusHistoryResponse>(cacheKey);
if (cached !== undefined) { if (cached !== undefined) {
return cached; return cached;
} }
// Anchor to UTC midnight so the query window aligns with stable calendar days // Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
const utcToday = new Date(); // window aligns with stable calendar days for the requesting client
utcToday.setUTCHours(0, 0, 0, 0); const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
const startSec = todayMidnightSec - days * 86400; const startSec = todayMidnightSec - days * 86400;
const events = await logsDb const events = await logsDb
@@ -63,7 +79,8 @@ export async function getCachedStatusHistory(
const { buckets, totalDowntime } = computeBuckets( const { buckets, totalDowntime } = computeBuckets(
events, events,
days, days,
priorStatus priorStatus,
tzOffsetMinutes
); );
const totalWindow = days * 86400; const totalWindow = days * 86400;
const overallUptime = const overallUptime =
@@ -99,11 +116,19 @@ export const statusHistoryQuerySchema = z
days: z days: z
.string() .string()
.optional() .optional()
.transform((v) => (v ? parseInt(v, 10) : 90)) .transform((v) => (v ? parseInt(v, 10) : 90)),
// Minutes to add to UTC to get the requesting client's local time
// (e.g. Australia/Sydney standard time is 600). Optional and
// defaults to 0 (UTC) so older clients keep the prior behavior.
tzOffsetMinutes: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 0))
}) })
.pipe( .pipe(
z.object({ z.object({
days: z.number().int().min(1).max(365) days: z.number().int().min(1).max(365),
tzOffsetMinutes: z.number().int().min(-720).max(840)
}) })
); );
@@ -133,15 +158,15 @@ export function computeBuckets(
id: number; id: number;
}[], }[],
days: number, days: number,
priorStatus: string | null = null priorStatus: string | null = null,
tzOffsetMinutes: number = 0
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } { ): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
const nowSec = Math.floor(Date.now() / 1000); const nowSec = Math.floor(Date.now() / 1000);
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days // Anchor bucket boundaries to local midnight (UTC when tzOffsetMinutes is
// and don't drift as the cache expires and is recomputed // 0) so dates are stable calendar days for the requesting client and
const utcToday = new Date(); // don't drift as the cache expires and is recomputed
utcToday.setUTCHours(0, 0, 0, 0); const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
const buckets: StatusHistoryDayBucket[] = []; const buckets: StatusHistoryDayBucket[] = [];
let totalDowntime = 0; let totalDowntime = 0;
@@ -237,7 +262,13 @@ export function computeBuckets(
) )
: 100; : 100;
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10); // Shift by the client's offset before formatting so the label reflects
// their local calendar date rather than the UTC date of dayStartSec
const dateStr = new Date(
(dayStartSec + tzOffsetMinutes * 60) * 1000
)
.toISOString()
.slice(0, 10);
const hasAnyData = currentStatus !== null || dayEvents.length > 0; const hasAnyData = currentStatus !== null || dayEvents.length > 0;
+10
View File
@@ -2,6 +2,7 @@ import {
db, db,
Org, Org,
orgs, orgs,
resourceAccessToken,
resources, resources,
siteResources, siteResources,
sites, sites,
@@ -83,6 +84,15 @@ export async function removeUserFromOrg(
.delete(userOrgs) .delete(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId))); .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));
await trx
.delete(resourceAccessToken)
.where(
and(
eq(resourceAccessToken.userId, userId),
eq(resourceAccessToken.orgId, org.orgId)
)
);
await trx.delete(userResources).where( await trx.delete(userResources).where(
and( and(
eq(userResources.userId, userId), eq(userResources.userId, userId),
+1
View File
@@ -17,3 +17,4 @@ export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess"; export * from "./verifyApiKeyIdpAccess";
export * from "./verifyApiKeyDomainAccess"; export * from "./verifyApiKeyDomainAccess";
export * from "./verifyApiKeyResourcePolicyAccess"; export * from "./verifyApiKeyResourcePolicyAccess";
export * from "./verifyApiKeySiteProvisioningKeyAccess";
@@ -0,0 +1,111 @@
import { Request, Response, NextFunction } from "express";
import {
db,
siteProvisioningKeys,
siteProvisioningKeyOrg,
apiKeyOrg
} from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeySiteProvisioningKeyAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const siteProvisioningKeyId =
getFirstString(req.params.siteProvisioningKeyId) ||
getFirstString(req.body.siteProvisioningKeyId) ||
getFirstString(req.query.siteProvisioningKeyId);
const orgId = getFirstString(req.params.orgId);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!siteProvisioningKeyId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
if (apiKey.isRoot) {
// Root keys can access any site provisioning key in any org
return next();
}
const [row] = await db
.select()
.from(siteProvisioningKeys)
.innerJoin(
siteProvisioningKeyOrg,
and(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.limit(1);
if (!row?.siteProvisioningKeys || !row.siteProvisioningKeyOrg) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site provisioning key with ID ${siteProvisioningKeyId} not found for organization ${orgId}`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, row.siteProvisioningKeyOrg.orgId)
)
)
.limit(1);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site provisioning key access"
)
);
}
}
+1
View File
@@ -18,6 +18,7 @@ export enum OpenAPITags {
OrgIdp = "Identity Provider (Organization Only)", OrgIdp = "Identity Provider (Organization Only)",
Client = "Client", Client = "Client",
ApiKey = "API Key", ApiKey = "API Key",
SiteProvisioningKey = "Site Provisioning Key",
Domain = "Domain", Domain = "Domain",
Blueprint = "Blueprint", Blueprint = "Blueprint",
Ssh = "SSH", Ssh = "SSH",
@@ -55,9 +55,14 @@ export async function getHealthCheckStatusHistory(
const entityType = "health_check"; const entityType = "health_check";
const entityId = parsedParams.data.healthCheckId; const entityId = parsedParams.data.healthCheckId;
const { days } = parsedQuery.data; const { days, tzOffsetMinutes } = parsedQuery.data;
const data = await getCachedStatusHistory(entityType, entityId, days); const data = await getCachedStatusHistory(
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, { return response<StatusHistoryResponse>(res, {
data, data,
+317 -2
View File
@@ -58,7 +58,8 @@ import {
resourceRules, resourceRules,
resourcePolicyRules, resourcePolicyRules,
userOrgRoles, userOrgRoles,
roles roles,
resourceAccessToken
} from "@server/db"; } from "@server/db";
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm"; import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
import { alias } from "@server/db"; import { alias } from "@server/db";
@@ -81,11 +82,16 @@ import config from "@server/lib/config";
import { exchangeSession } from "@server/routers/badger"; import { exchangeSession } from "@server/routers/badger";
import { import {
ResourceSessionValidationResult, ResourceSessionValidationResult,
createResourceSession,
serializeResourceSessionCookie,
validateResourceSessionToken validateResourceSessionToken
} from "@server/auth/sessions/resource"; } from "@server/auth/sessions/resource";
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
import { maxmindLookup } from "@server/db/maxmind"; import { maxmindLookup } from "@server/db/maxmind";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import { generateSessionToken } from "@server/auth/sessions/app";
import { logAccessAudit } from "#private/lib/logAccessAudit";
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
import semver from "semver"; import semver from "semver";
import { maxmindAsnLookup } from "@server/db/maxmindAsn"; import { maxmindAsnLookup } from "@server/db/maxmindAsn";
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy"; import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
@@ -178,6 +184,73 @@ const validateResourceAccessTokenBodySchema = z.strictObject({
accessToken: z.string() accessToken: z.string()
}); });
const createAccessTokenSessionParamsSchema = z.strictObject({
resourceId: z.coerce.number().int().positive()
});
const createAccessTokenSessionBodySchema = z.strictObject({
accessTokenId: z.string().min(1)
});
const getAccessTokenParamsSchema = z.strictObject({
accessTokenId: z.string().min(1)
});
const logAccessAuditBodySchema = z.strictObject({
action: z.boolean(),
type: z.string(),
orgId: z.string(),
resourceId: z.number().optional(),
siteResourceId: z.number().optional(),
user: z
.object({
username: z.string(),
userId: z.string()
})
.optional(),
apiKey: z
.object({
name: z.string().nullable(),
apiKeyId: z.string()
})
.optional(),
metadata: z.any().optional(),
userAgent: z.string().optional(),
requestIp: z.string().optional()
});
type AccessTokenUserData = {
userId: string;
username: string;
email: string | null;
name: string | null;
role: string | null;
};
async function resolveAccessTokenUserData(
userId: string,
orgId: string
): Promise<AccessTokenUserData | undefined> {
const [user] = await db
.select()
.from(users)
.where(eq(users.userId, userId))
.limit(1);
if (!user) {
return undefined;
}
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
return {
userId: user.userId,
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
};
}
// Certificates by domains query validation // Certificates by domains query validation
const getCertificatesByDomainsQuerySchema = z.strictObject({ const getCertificatesByDomainsQuerySchema = z.strictObject({
// Accept domains as string or array (domains or domains[]) // Accept domains as string or array (domains or domains[])
@@ -1829,8 +1902,23 @@ hybridRouter.post(
resourceId resourceId
}); });
let userData: AccessTokenUserData | undefined;
if (
result.valid &&
result.tokenItem?.userId &&
result.tokenItem.orgId
) {
userData = await resolveAccessTokenUserData(
result.tokenItem.userId,
result.tokenItem.orgId
);
}
return response(res, { return response(res, {
data: result, data: {
...result,
userData
},
success: true, success: true,
error: false, error: false,
message: result.valid message: result.valid
@@ -1850,6 +1938,233 @@ hybridRouter.post(
} }
); );
// Create a resource session from a valid access token (for remote nodes)
hybridRouter.post(
"/resource/:resourceId/session/create-access-token",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = createAccessTokenSessionParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = createAccessTokenSessionBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const { accessTokenId } = parsedBody.data;
const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
.limit(1);
if (!tokenItem || tokenItem.resourceId !== resourceId) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Access token not found"
)
);
}
if (!tokenItem.persistSession) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Access token does not allow session persistence"
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource || !resource.fullDomain) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
const token = generateSessionToken();
const sess = await createResourceSession({
resourceId: resource.resourceId,
token,
accessTokenId: tokenItem.accessTokenId,
sessionLength: tokenItem.sessionLength,
expiresAt: tokenItem.expiresAt,
doNotExtend: tokenItem.expiresAt ? true : false
});
const cookieName = config.getRawConfig().server.session_cookie_name;
const cookie = serializeResourceSessionCookie(
cookieName,
resource.fullDomain,
token,
!resource.ssl,
new Date(sess.expiresAt)
);
return response(res, {
data: { cookie },
success: true,
error: false,
message: "Access token session created successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create access token session"
)
);
}
}
);
// Resolve access token metadata for remote nodes (cookie session path)
hybridRouter.get(
"/resource/access-token/:accessTokenId",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getAccessTokenParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { accessTokenId } = parsedParams.data;
const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
.limit(1);
if (!tokenItem) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Access token not found"
)
);
}
let userData: AccessTokenUserData | undefined;
if (tokenItem.userId) {
userData = await resolveAccessTokenUserData(
tokenItem.userId,
tokenItem.orgId
);
}
return response(res, {
data: { tokenItem, userData },
success: true,
error: false,
message: "Access token retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get access token"
)
);
}
}
);
// Access audit log from remote nodes
hybridRouter.post(
"/logs/access",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedBody = logAccessAuditBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
if (
await checkExitNodeOrg(
remoteExitNode.exitNodeId,
parsedBody.data.orgId
)
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Exit node not allowed for this organization"
)
);
}
await logAccessAudit(parsedBody.data);
return response(res, {
data: null,
success: true,
error: false,
message: "Access audit log saved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to save access audit log"
)
);
}
}
);
const geoIpLookupParamsSchema = z.object({ const geoIpLookupParamsSchema = z.object({
ip: z.union([z.ipv4(), z.ipv6()]) ip: z.union([z.ipv4(), z.ipv6()])
}); });
+44
View File
@@ -16,6 +16,7 @@ import * as org from "#private/routers/org";
import * as logs from "#private/routers/auditLogs"; import * as logs from "#private/routers/auditLogs";
import * as alertEvents from "#private/routers/alertEvents"; import * as alertEvents from "#private/routers/alertEvents";
import * as certificates from "#private/routers/certificates"; import * as certificates from "#private/routers/certificates";
import * as siteProvisioning from "#private/routers/siteProvisioning";
import { import {
verifyApiKeyHasAction, verifyApiKeyHasAction,
@@ -24,6 +25,7 @@ import {
verifyApiKeyIdpAccess, verifyApiKeyIdpAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyApiKeyUserAccess, verifyApiKeyUserAccess,
verifyApiKeySiteProvisioningKeyAccess,
verifyLimits verifyLimits
} from "@server/middlewares"; } from "@server/middlewares";
import * as user from "#private/routers/user"; import * as user from "#private/routers/user";
@@ -215,3 +217,45 @@ authenticated.delete(
logActionAudit(ActionsEnum.removeUserRole), logActionAudit(ActionsEnum.removeUserRole),
user.removeUserRole user.removeUserRole
); );
authenticated.put(
"/org/:orgId/site-provisioning-key",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSiteProvisioningKey),
logActionAudit(ActionsEnum.createSiteProvisioningKey),
siteProvisioning.createSiteProvisioningKey
);
authenticated.get(
"/org/:orgId/site-provisioning-keys",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteProvisioningKeys),
siteProvisioning.listSiteProvisioningKeys
);
authenticated.delete(
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyApiKeyOrgAccess,
verifyApiKeySiteProvisioningKeyAccess,
verifyApiKeyHasAction(ActionsEnum.deleteSiteProvisioningKey),
logActionAudit(ActionsEnum.deleteSiteProvisioningKey),
siteProvisioning.deleteSiteProvisioningKey
);
authenticated.patch(
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyApiKeyOrgAccess,
verifyApiKeySiteProvisioningKeyAccess,
verifyApiKeyHasAction(ActionsEnum.updateSiteProvisioningKey),
logActionAudit(ActionsEnum.updateSiteProvisioningKey),
siteProvisioning.updateSiteProvisioningKey
);
@@ -26,6 +26,8 @@ import {
import logger from "@server/logger"; import logger from "@server/logger";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types"; import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({ const paramsSchema = z.object({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -34,10 +36,17 @@ const paramsSchema = z.object({
const bodySchema = z const bodySchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
maxBatchSize: z.union([ maxBatchSize: z
.union([
z.null(), z.null(),
z.coerce.number().int().positive().max(1_000_000) z.coerce.number().int().positive().max(1_000_000)
]), ])
.openapi({
type: "number",
default: null,
description:
"Maximum number of sites that can be provisioned in a single batch. If null, there is no limit."
}),
validUntil: z.string().max(255).optional(), validUntil: z.string().max(255).optional(),
approveNewSites: z.boolean().optional().default(true) approveNewSites: z.boolean().optional().default(true)
}) })
@@ -57,6 +66,49 @@ const bodySchema = z
export type CreateSiteProvisioningKeyBody = z.infer<typeof bodySchema>; export type CreateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
const CreateSiteProvisioningKeyResponseDataSchema = z.object({
siteProvisioningKeyId: z.string(),
orgId: z.string(),
name: z.string(),
siteProvisioningKey: z.string(),
lastChars: z.string(),
createdAt: z.string(),
lastUsed: z.string().nullable(),
maxBatchSize: z.number().nullable(),
numUsed: z.number(),
validUntil: z.string().nullable(),
approveNewSites: z.boolean()
});
registry.registerPath({
method: "put",
path: "/org/{orgId}/site-provisioning-key",
description: "Create a new site provisioning key for the organization.",
tags: [OpenAPITags.SiteProvisioningKey],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {
201: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
CreateSiteProvisioningKeyResponseDataSchema
)
}
}
}
}
});
export async function createSiteProvisioningKey( export async function createSiteProvisioningKey(
req: Request, req: Request,
res: Response, res: Response,
@@ -13,23 +13,46 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({ const paramsSchema = z.object({
siteProvisioningKeyId: z.string().nonempty(), siteProvisioningKeyId: z.string().nonempty(),
orgId: z.string().nonempty() orgId: z.string().nonempty()
}); });
registry.registerPath({
method: "delete",
path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}",
description: "Delete a site provisioning key.",
tags: [OpenAPITags.SiteProvisioningKey],
request: {
params: paramsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
export async function deleteSiteProvisioningKey( export async function deleteSiteProvisioningKey(
req: Request, req: Request,
res: Response, res: Response,
@@ -11,11 +11,7 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
@@ -25,6 +21,8 @@ import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types"; import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({ const paramsSchema = z.object({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -45,11 +43,55 @@ const querySchema = z.object({
.pipe(z.int().nonnegative()) .pipe(z.int().nonnegative())
}); });
const ListSiteProvisioningKeysResponseDataSchema = z.object({
siteProvisioningKeys: z.array(
z.object({
siteProvisioningKeyId: z.string(),
orgId: z.string(),
lastChars: z.string(),
createdAt: z.string(),
name: z.string(),
lastUsed: z.string().nullable(),
maxBatchSize: z.number().nullable(),
numUsed: z.number(),
validUntil: z.string().nullable(),
approveNewSites: z.boolean()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/site-provisioning-keys",
description: "List all site provisioning keys for an organization.",
tags: [OpenAPITags.SiteProvisioningKey],
request: {
params: paramsSchema,
query: querySchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListSiteProvisioningKeysResponseDataSchema
)
}
}
}
}
});
function querySiteProvisioningKeys(orgId: string) { function querySiteProvisioningKeys(orgId: string) {
return db return db
.select({ .select({
siteProvisioningKeyId: siteProvisioningKeyId: siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeys.siteProvisioningKeyId,
orgId: siteProvisioningKeyOrg.orgId, orgId: siteProvisioningKeyOrg.orgId,
lastChars: siteProvisioningKeys.lastChars, lastChars: siteProvisioningKeys.lastChars,
createdAt: siteProvisioningKeys.createdAt, createdAt: siteProvisioningKeys.createdAt,
@@ -13,11 +13,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -25,6 +21,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types"; import type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({ const paramsSchema = z.object({
siteProvisioningKeyId: z.string().nonempty(), siteProvisioningKeyId: z.string().nonempty(),
@@ -38,7 +36,13 @@ const bodySchema = z
z.null(), z.null(),
z.coerce.number().int().positive().max(1_000_000) z.coerce.number().int().positive().max(1_000_000)
]) ])
.optional(), .optional()
.openapi({
type: "number",
default: null,
description:
"Maximum number of sites that can be provisioned in a single batch. If null, there is no limit."
}),
validUntil: z.string().max(255).optional(), validUntil: z.string().max(255).optional(),
approveNewSites: z.boolean().optional() approveNewSites: z.boolean().optional()
}) })
@@ -50,7 +54,8 @@ const bodySchema = z
) { ) {
ctx.addIssue({ ctx.addIssue({
code: "custom", code: "custom",
message: "Provide maxBatchSize and/or validUntil and/or approveNewSites", message:
"Provide maxBatchSize and/or validUntil and/or approveNewSites",
path: ["maxBatchSize"] path: ["maxBatchSize"]
}); });
} }
@@ -69,6 +74,48 @@ const bodySchema = z
export type UpdateSiteProvisioningKeyBody = z.infer<typeof bodySchema>; export type UpdateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
const UpdateSiteProvisioningKeyResponseDataSchema = z.object({
siteProvisioningKeyId: z.string(),
orgId: z.string(),
name: z.string(),
lastChars: z.string(),
createdAt: z.string(),
lastUsed: z.string().nullable(),
maxBatchSize: z.number().nullable(),
numUsed: z.number(),
validUntil: z.string().nullable(),
approveNewSites: z.boolean()
});
registry.registerPath({
method: "patch",
path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}",
description: "Update a site provisioning key.",
tags: [OpenAPITags.SiteProvisioningKey],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
UpdateSiteProvisioningKeyResponseDataSchema
)
}
}
}
}
});
export async function updateSiteProvisioningKey( export async function updateSiteProvisioningKey(
req: Request, req: Request,
res: Response, res: Response,
@@ -1,4 +1,3 @@
import { hash } from "@node-rs/argon2";
import { import {
generateId, generateId,
generateIdFromEntropySize, generateIdFromEntropySize,
@@ -8,18 +7,18 @@ import { db } from "@server/db";
import { import {
ResourceAccessToken, ResourceAccessToken,
resourceAccessToken, resourceAccessToken,
resources resources,
userOrgs
} from "@server/db"; } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { createDate, TimeSpan } from "oslo"; import { createDate, TimeSpan } from "oslo";
import { hashPassword } from "@server/auth/password";
import { encodeHexLowerCase } from "@oslojs/encoding"; import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
@@ -28,7 +27,9 @@ export const generateAccessTokenBodySchema = z.strictObject({
validForSeconds: z.int().positive().optional(), // seconds validForSeconds: z.int().positive().optional(), // seconds
title: z.string().optional(), title: z.string().optional(),
path: z.string().optional(), path: z.string().optional(),
description: z.string().optional() description: z.string().optional(),
persistSession: z.boolean().optional().default(false),
userId: z.string().optional()
}); });
export const generateAccssTokenParamsSchema = z.strictObject({ export const generateAccssTokenParamsSchema = z.strictObject({
@@ -101,7 +102,14 @@ export async function generateAccessToken(
} }
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
const { validForSeconds, title, path, description } = parsedBody.data; const {
validForSeconds,
title,
path,
description,
persistSession,
userId
} = parsedBody.data;
const [resource] = await db const [resource] = await db
.select() .select()
@@ -112,6 +120,28 @@ export async function generateAccessToken(
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found")); return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
} }
if (userId) {
const [membership] = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, resource.orgId)
)
)
.limit(1);
if (!membership) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"User is not a member of this organization"
)
);
}
}
try { try {
const sessionLength = validForSeconds const sessionLength = validForSeconds
? validForSeconds * 1000 ? validForSeconds * 1000
@@ -133,23 +163,27 @@ export async function generateAccessToken(
accessTokenId: id, accessTokenId: id,
orgId: resource.orgId, orgId: resource.orgId,
resourceId, resourceId,
userId: userId || null,
tokenHash, tokenHash,
expiresAt: expiresAt || null, expiresAt: expiresAt || null,
sessionLength: sessionLength, sessionLength: sessionLength,
title: title || null, title: title || null,
path: path || null, path: path || null,
description: description || null, description: description || null,
persistSession,
createdAt: new Date().getTime() createdAt: new Date().getTime()
}) })
.returning({ .returning({
accessTokenId: resourceAccessToken.accessTokenId, accessTokenId: resourceAccessToken.accessTokenId,
orgId: resourceAccessToken.orgId, orgId: resourceAccessToken.orgId,
resourceId: resourceAccessToken.resourceId, resourceId: resourceAccessToken.resourceId,
userId: resourceAccessToken.userId,
expiresAt: resourceAccessToken.expiresAt, expiresAt: resourceAccessToken.expiresAt,
sessionLength: resourceAccessToken.sessionLength, sessionLength: resourceAccessToken.sessionLength,
title: resourceAccessToken.title, title: resourceAccessToken.title,
path: resourceAccessToken.path, path: resourceAccessToken.path,
description: resourceAccessToken.description, description: resourceAccessToken.description,
persistSession: resourceAccessToken.persistSession,
createdAt: resourceAccessToken.createdAt createdAt: resourceAccessToken.createdAt
}) })
.execute(); .execute();
+10 -2
View File
@@ -6,12 +6,13 @@ import {
userResources, userResources,
roleResources, roleResources,
resourceAccessToken, resourceAccessToken,
sites sites,
users
} from "@server/db"; } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm"; import { sql, eq, or, inArray, and, count, isNull, gt } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import stoi from "@server/lib/stoi"; import stoi from "@server/lib/stoi";
import { fromZodError } from "zod-validation-error"; import { fromZodError } from "zod-validation-error";
@@ -55,11 +56,16 @@ function queryAccessTokens(
accessTokenId: resourceAccessToken.accessTokenId, accessTokenId: resourceAccessToken.accessTokenId,
orgId: resourceAccessToken.orgId, orgId: resourceAccessToken.orgId,
resourceId: resourceAccessToken.resourceId, resourceId: resourceAccessToken.resourceId,
userId: resourceAccessToken.userId,
userName: users.name,
username: users.username,
userEmail: users.email,
sessionLength: resourceAccessToken.sessionLength, sessionLength: resourceAccessToken.sessionLength,
expiresAt: resourceAccessToken.expiresAt, expiresAt: resourceAccessToken.expiresAt,
tokenHash: resourceAccessToken.tokenHash, tokenHash: resourceAccessToken.tokenHash,
title: resourceAccessToken.title, title: resourceAccessToken.title,
description: resourceAccessToken.description, description: resourceAccessToken.description,
persistSession: resourceAccessToken.persistSession,
createdAt: resourceAccessToken.createdAt, createdAt: resourceAccessToken.createdAt,
resourceName: resources.name, resourceName: resources.name,
resourceNiceId: resources.niceId, resourceNiceId: resources.niceId,
@@ -75,6 +81,7 @@ function queryAccessTokens(
eq(resourceAccessToken.resourceId, resources.resourceId) eq(resourceAccessToken.resourceId, resources.resourceId)
) )
.leftJoin(sites, eq(resources.resourceId, sites.siteId)) .leftJoin(sites, eq(resources.resourceId, sites.siteId))
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
.where( .where(
and( and(
inArray( inArray(
@@ -97,6 +104,7 @@ function queryAccessTokens(
eq(resourceAccessToken.resourceId, resources.resourceId) eq(resourceAccessToken.resourceId, resources.resourceId)
) )
.leftJoin(sites, eq(resources.resourceId, sites.siteId)) .leftJoin(sites, eq(resources.resourceId, sites.siteId))
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
.where( .where(
and( and(
inArray( inArray(
+15 -10
View File
@@ -16,18 +16,26 @@ export async function logout(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
const { user, session } = await verifySession(req); const { user, session } = await verifySession(req);
const isSecure = req.protocol === "https";
// Always clear the session cookie so logout is idempotent, even when
// the session is already missing or invalid
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
if (!user || !session) { if (!user || !session) {
if (config.getRawConfig().app.log_failed_attempts) { if (config.getRawConfig().app.log_failed_attempts) {
logger.info( logger.info(
`Log out failed because missing or invalid session. IP: ${req.ip}.` `Log out with missing or invalid session. IP: ${req.ip}.`
); );
} }
return next(
createHttpError( return response<null>(res, {
HttpCode.BAD_REQUEST, data: null,
"You must be logged in to sign out" success: true,
) error: false,
); message: "Logged out successfully",
status: HttpCode.OK
});
} }
try { try {
@@ -37,9 +45,6 @@ export async function logout(
logger.error("Failed to invalidate session", error); logger.error("Failed to invalidate session", error);
} }
const isSecure = req.protocol === "https";
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
return response<null>(res, { return response<null>(res, {
data: null, data: null,
success: true, success: true,
+1 -1
View File
@@ -99,7 +99,7 @@ export async function requestPasswordReset(
}); });
}); });
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`; const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(email)}&token=${token}`;
if (!config.getRawConfig().email) { if (!config.getRawConfig().email) {
logger.info( logger.info(
+273 -40
View File
@@ -1,4 +1,9 @@
import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import {
createResourceSession,
serializeResourceSessionCookie,
validateResourceSessionToken
} from "@server/auth/sessions/resource";
import { generateSessionToken } from "@server/auth/sessions/app";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import { import {
getResourceByDomain, getResourceByDomain,
@@ -13,6 +18,7 @@ import {
LoginPage, LoginPage,
Org, Org,
Resource, Resource,
ResourceAccessToken,
ResourceHeaderAuth, ResourceHeaderAuth,
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
ResourcePassword, ResourcePassword,
@@ -21,7 +27,10 @@ import {
ResourcePolicyPassword, ResourcePolicyPassword,
ResourcePolicyHeaderAuth, ResourcePolicyHeaderAuth,
ResourceRule, ResourceRule,
ResourceSession ResourceSession,
db,
resourceAccessToken,
users
} from "@server/db"; } from "@server/db";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip"; import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
@@ -41,11 +50,13 @@ import {
enforceResourceSessionLength enforceResourceSessionLength
} from "#dynamic/lib/checkOrgAccessPolicy"; } from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit"; import { logRequestAudit } from "./logRequestAudit";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
import { REGIONS } from "@server/db/regions"; import { REGIONS } from "@server/db/regions";
import { localCache } from "#dynamic/lib/cache"; import { localCache } from "#dynamic/lib/cache";
import { APP_VERSION } from "@server/lib/consts"; import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { eq } from "drizzle-orm";
const verifyResourceSessionSchema = z.object({ const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(), sessions: z.record(z.string(), z.string()).optional(),
@@ -350,22 +361,15 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
logRequestAudit( return await allowAccessToken(
{ res,
action: true, resource,
reason: 102, // valid access token tokenItem,
resourceId: resource.resourceId, sessions,
orgId: resource.orgId, dontStripSession,
location: ipCC, parsedBody.data,
apiKey: { ipCC
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
); );
return allowed(res, undefined, dontStripSession);
} }
} }
@@ -401,22 +405,15 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
logRequestAudit( return await allowAccessToken(
{ res,
action: true, resource,
reason: 102, // valid access token tokenItem,
resourceId: resource.resourceId, sessions,
orgId: resource.orgId, dontStripSession,
location: ipCC, parsedBody.data,
apiKey: { ipCC
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
); );
return allowed(res, undefined, dontStripSession);
} }
} }
@@ -666,22 +663,37 @@ export async function verifyResourceSession(
"Resource allowed because access token session is valid" "Resource allowed because access token session is valid"
); );
logRequestAudit( const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(
eq(
resourceAccessToken.accessTokenId,
resourceSession.accessTokenId
)
)
.limit(1);
const userData = tokenItem
? await getAccessTokenUserData(
tokenItem,
resource.orgId
)
: undefined;
logAccessTokenRequestAudit(
{ {
action: true,
reason: 102, // valid access token
resourceId: resource.resourceId, resourceId: resource.resourceId,
orgId: resource.orgId, orgId: resource.orgId,
location: ipCC, location: ipCC,
apiKey: { accessTokenId: resourceSession.accessTokenId,
name: null, tokenTitle: tokenItem?.title ?? null,
apiKeyId: resourceSession.accessTokenId userData
}
}, },
parsedBody.data parsedBody.data
); );
return allowed(res, undefined, dontStripSession); return allowed(res, userData, dontStripSession);
} }
if (resourceSession.userSessionId && sso) { if (resourceSession.userSessionId && sso) {
@@ -892,6 +904,227 @@ function allowed(
return response<VerifyUserResponse>(res, data); return response<VerifyUserResponse>(res, data);
} }
async function allowAccessToken(
res: Response,
resource: Resource,
tokenItem: ResourceAccessToken,
sessions: Record<string, string> | undefined,
dontStripSession: boolean | undefined,
auditBody: VerifyResourceSessionSchema,
location?: string
) {
const userData = await getAccessTokenUserData(tokenItem, resource.orgId);
logAccessTokenRequestAudit(
{
resourceId: resource.resourceId,
orgId: resource.orgId,
location,
accessTokenId: tokenItem.accessTokenId,
tokenTitle: tokenItem.title,
userData
},
auditBody
);
if (!tokenItem.persistSession) {
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
return allowed(res, userData, dontStripSession);
}
const resourceSessionToken = extractResourceSessionToken(
sessions ?? {},
resource.ssl
);
if (resourceSessionToken) {
const sessionCacheKey = `session:${resourceSessionToken}`;
let resourceSession: ResourceSession | null | undefined =
localCache.get(sessionCacheKey);
if (!resourceSession) {
const result = await validateResourceSessionToken(
resourceSessionToken,
resource.resourceId
);
resourceSession = result?.resourceSession;
localCache.set(sessionCacheKey, resourceSession, 5);
}
if (
resourceSession &&
!resourceSession.isRequestToken &&
resourceSession.accessTokenId === tokenItem.accessTokenId
) {
logger.debug(
"Resource allowed because existing access token session is valid"
);
return allowed(res, userData, dontStripSession);
}
}
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
return await createAccessTokenSession(res, resource, tokenItem, userData);
}
async function createAccessTokenSession(
res: Response,
resource: Resource,
tokenItem: ResourceAccessToken,
userData?: BasicUserData
) {
const token = generateSessionToken();
const sess = await createResourceSession({
resourceId: resource.resourceId,
token,
accessTokenId: tokenItem.accessTokenId,
sessionLength: tokenItem.sessionLength,
expiresAt: tokenItem.expiresAt,
doNotExtend: tokenItem.expiresAt ? true : false
});
const cookieName = config.getRawConfig().server.session_cookie_name;
const cookie = serializeResourceSessionCookie(
cookieName,
resource.fullDomain!,
token,
!resource.ssl,
new Date(sess.expiresAt)
);
res.appendHeader("Set-Cookie", cookie);
logger.debug("Access token is valid, creating new session");
return allowed(res, userData);
}
async function getAccessTokenUserData(
tokenItem: ResourceAccessToken,
orgId: string
): Promise<BasicUserData | undefined> {
if (!tokenItem.userId) {
return undefined;
}
const cacheKey = `accessTokenUser:${tokenItem.userId}:${orgId}`;
const cached = localCache.get(cacheKey) as BasicUserData | null | undefined;
if (cached !== undefined) {
return cached ?? undefined;
}
const [user] = await db
.select()
.from(users)
.where(eq(users.userId, tokenItem.userId))
.limit(1);
if (!user) {
localCache.set(cacheKey, null, 5);
return undefined;
}
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
const userData: BasicUserData = {
userId: user.userId,
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
};
localCache.set(cacheKey, userData, 12);
return userData;
}
function logAccessTokenRequestAudit(
data: {
resourceId: number;
orgId: string;
location?: string;
accessTokenId: string;
tokenTitle: string | null;
userData?: BasicUserData;
},
body: VerifyResourceSessionSchema
) {
if (data.userData) {
logRequestAudit(
{
action: true,
reason: 102, // valid access token
resourceId: data.resourceId,
orgId: data.orgId,
location: data.location,
user: {
username: data.userData.username,
userId: data.userData.userId
},
metadata: {
accessTokenId: data.accessTokenId,
accessTokenTitle: data.tokenTitle
}
},
body
);
return;
}
logRequestAudit(
{
action: true,
reason: 102, // valid access token
resourceId: data.resourceId,
orgId: data.orgId,
location: data.location,
apiKey: {
name: data.tokenTitle,
apiKeyId: data.accessTokenId
}
},
body
);
}
function logAccessTokenAccessAudit(
tokenItem: ResourceAccessToken,
resource: Resource,
userData: BasicUserData | undefined,
body: VerifyResourceSessionSchema
) {
const userAgent =
body.headers?.["user-agent"] || body.headers?.["User-Agent"];
if (userData) {
logAccessAudit({
orgId: resource.orgId,
resourceId: resource.resourceId,
action: true,
type: "accessToken",
user: {
username: userData.username,
userId: userData.userId
},
metadata: {
accessTokenId: tokenItem.accessTokenId,
accessTokenTitle: tokenItem.title
},
userAgent,
requestIp: body.requestIp
});
return;
}
logAccessAudit({
orgId: resource.orgId,
resourceId: resource.resourceId,
action: true,
type: "accessToken",
apiKey: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
},
userAgent,
requestIp: body.requestIp
});
}
async function headerAuthChallenged( async function headerAuthChallenged(
res: Response, res: Response,
redirectPath?: string, redirectPath?: string,
-10
View File
@@ -809,16 +809,6 @@ authenticated.post(
accessToken.generateAccessToken accessToken.generateAccessToken
); );
authenticated.post(
`/resource/:resourceId/session-token`,
verifyApiKeyResourceAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
logActionAudit(ActionsEnum.createResourceSessionToken),
resource.createResourceSessionToken
);
authenticated.delete( authenticated.delete(
`/access-token/:accessTokenId`, `/access-token/:accessTokenId`,
verifyApiKeyAccessTokenAccess, verifyApiKeyAccessTokenAccess,
@@ -29,7 +29,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
return; return;
} }
const { publicKey, port, chainId } = message.data; const { publicKey, port, localEndpoints, chainId } = message.data;
const siteId = newt.siteId; const siteId = newt.siteId;
// Get the current site data // Get the current site data
@@ -69,7 +69,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
.update(sites) .update(sites)
.set({ .set({
publicKey, publicKey,
listenPort: port listenPort: port,
localEndpoints: localEndpoints
? JSON.stringify(localEndpoints)
: null
}) })
.where(eq(sites.siteId, siteId)) .where(eq(sites.siteId, siteId))
.returning(); .returning();
+4
View File
@@ -30,6 +30,7 @@ export async function buildSiteConfigurationForOlmClient(
siteId: number; siteId: number;
name?: string; name?: string;
endpoint?: string; endpoint?: string;
localEndpoints?: string[];
publicKey?: string; publicKey?: string;
serverIP?: string | null; serverIP?: string | null;
serverPort?: number | null; serverPort?: number | null;
@@ -206,6 +207,9 @@ export async function buildSiteConfigurationForOlmClient(
name: site.name, name: site.name,
// relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing // relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing
endpoint: site.endpoint, endpoint: site.endpoint,
localEndpoints: site.localEndpoints
? JSON.parse(site.localEndpoints)
: undefined,
publicKey: site.publicKey, publicKey: site.publicKey,
serverIP: site.address, serverIP: site.address,
serverPort: site.listenPort, serverPort: site.listenPort,
@@ -0,0 +1,74 @@
import { db, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, Olm } from "@server/db";
import { and, eq } from "drizzle-orm";
import { updatePeer as newtUpdatePeer } from "../newt/peers";
import logger from "@server/logger";
export const handleOlmLocalMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
logger.info("Handling local olm message!");
if (!olm) {
logger.warn("Olm not found");
return;
}
if (!olm.clientId) {
logger.warn("Olm has no client!");
return;
}
const clientId = olm.clientId;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
logger.warn("Client not found");
return;
}
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
if (!client.pubKey) {
logger.warn("Client has no endpoint or listen port");
return;
}
const { siteId, chainId } = message.data;
// Get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site || !site.exitNodeId) {
logger.warn("Site not found or has no exit node");
return;
}
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: "" // this removes the endpoint so the newt knows to accept local
});
// Just ack the message, we don't keep sending it
return {
message: {
type: "olm/wg/peer/local",
data: {
siteId: siteId,
chainId
}
},
broadcast: false,
excludeSender: false
};
};
+2 -2
View File
@@ -79,9 +79,9 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
) )
); );
// update the peer on the exit node // update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, { await newtUpdatePeer(siteId, client.pubKey, {
endpoint: "" // this removes the endpoint so the exit node knows to relay endpoint: "" // this removes the endpoint so the newt knows to relay
}); });
return { return {
@@ -3,24 +3,15 @@ import {
db, db,
networks, networks,
siteNetworks, siteNetworks,
siteResources, siteResources
} from "@server/db"; } from "@server/db";
import { MessageHandler } from "@server/routers/ws"; import { MessageHandler } from "@server/routers/ws";
import { import { clients, clientSitesAssociationsCache, Olm, sites } from "@server/db";
clients,
clientSitesAssociationsCache,
Olm,
sites
} from "@server/db";
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm"; import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { import { generateAliasConfig } from "@server/lib/ip";
generateAliasConfig,
} from "@server/lib/ip";
import { generateRemoteSubnets } from "@server/lib/ip"; import { generateRemoteSubnets } from "@server/lib/ip";
import { import { addPeer as newtAddPeer } from "@server/routers/newt/peers";
addPeer as newtAddPeer,
} from "@server/routers/newt/peers";
export const handleOlmServerPeerAddMessage: MessageHandler = async ( export const handleOlmServerPeerAddMessage: MessageHandler = async (
context context
@@ -135,10 +126,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
clientSiteResourcesAssociationsCache.siteResourceId clientSiteResourcesAssociationsCache.siteResourceId
) )
) )
.innerJoin( .innerJoin(networks, eq(siteResources.networkId, networks.networkId))
networks,
eq(siteResources.networkId, networks.networkId)
)
.innerJoin( .innerJoin(
siteNetworks, siteNetworks,
and( and(
@@ -147,10 +135,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
) )
) )
.where( .where(
eq( eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
); );
// Return connect message with all site configurations // Return connect message with all site configurations
@@ -161,6 +146,9 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
siteId: site.siteId, siteId: site.siteId,
name: site.name, name: site.name,
endpoint: site.endpoint, endpoint: site.endpoint,
localEndpoints: site.localEndpoints
? JSON.parse(site.localEndpoints)
: undefined,
publicKey: site.publicKey, publicKey: site.publicKey,
serverIP: site.address, serverIP: site.address,
serverPort: site.listenPort, serverPort: site.listenPort,
@@ -170,7 +158,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
aliases: generateAliasConfig( aliases: generateAliasConfig(
allSiteResources.map(({ siteResources }) => siteResources) allSiteResources.map(({ siteResources }) => siteResources)
), ),
chainId: chainId, chainId: chainId
} }
}, },
broadcast: false, broadcast: false,
@@ -0,0 +1,95 @@
import { db, exitNodes, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
import { and, eq } from "drizzle-orm";
import { updatePeer as newtUpdatePeer } from "../newt/peers";
import logger from "@server/logger";
export const handleOlmUnLocalMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
logger.info("Handling unlocal olm message!");
if (!olm) {
logger.warn("Olm not found");
return;
}
if (!olm.clientId) {
logger.warn("Olm has no client!");
return;
}
const clientId = olm.clientId;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
logger.warn("Client not found");
return;
}
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
if (!client.pubKey) {
logger.warn("Client has no endpoint or listen port");
return;
}
const { siteId, chainId } = message.data;
// Get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site) {
logger.warn("Site not found or has no exit node");
return;
}
const [clientSiteAssociation] = await db
.select()
.from(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.clientId, olm.clientId),
eq(clientSitesAssociationsCache.siteId, siteId)
)
);
if (!clientSiteAssociation) {
logger.warn("Client-Site association not found");
return;
}
if (!clientSiteAssociation.endpoint) {
logger.warn("Client-Site association has no endpoint, cannot unrelay");
return;
}
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: clientSiteAssociation.isRelayed
? ""
: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
});
return {
message: {
type: "olm/wg/peer/unlocal",
data: {
siteId: siteId,
chainId
}
},
broadcast: false,
excludeSender: false
};
};
@@ -77,9 +77,9 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
return; return;
} }
// update the peer on the exit node // update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, { await newtUpdatePeer(siteId, client.pubKey, {
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
}); });
return { return {
+2
View File
@@ -13,3 +13,5 @@ export * from "./recoverOlmWithFingerprint";
export * from "./handleOlmDisconnectingMessage"; export * from "./handleOlmDisconnectingMessage";
export * from "./handleOlmServerInitAddPeerHandshake"; export * from "./handleOlmServerInitAddPeerHandshake";
export * from "./offlineChecker"; export * from "./offlineChecker";
export * from "./handleOlmUnLocalMessage";
export * from "./handleOlmLocalMessage";
+2
View File
@@ -18,6 +18,7 @@ export async function addPeer(
serverPort: number | null; serverPort: number | null;
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
aliases: Alias[]; aliases: Alias[];
localEndpoints?: string[]; // optional, list of local endpoints for the peer
}, },
olmId?: string, olmId?: string,
version?: string | null version?: string | null
@@ -44,6 +45,7 @@ export async function addPeer(
name: peer.name, name: peer.name,
publicKey: peer.publicKey, publicKey: peer.publicKey,
endpoint: peer.endpoint, endpoint: peer.endpoint,
localEndpoints: peer.localEndpoints,
relayEndpoint: peer.relayEndpoint, relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP, serverIP: peer.serverIP,
serverPort: peer.serverPort, serverPort: peer.serverPort,
+32 -1
View File
@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import { db } from "@server/db"; import { db } from "@server/db";
import { Resource, resources } from "@server/db"; import { Resource, resources, users } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -156,11 +156,42 @@ export async function authWithAccessToken(
doNotExtend: true doNotExtend: true
}); });
let accessAuditUser: { username: string; userId: string } | undefined;
if (tokenItem.userId) {
const [associatedUser] = await db
.select({
userId: users.userId,
username: users.username
})
.from(users)
.where(eq(users.userId, tokenItem.userId))
.limit(1);
if (associatedUser) {
accessAuditUser = {
userId: associatedUser.userId,
username: associatedUser.username
};
}
}
logAccessAudit({ logAccessAudit({
orgId: resource.orgId, orgId: resource.orgId,
resourceId: resource.resourceId, resourceId: resource.resourceId,
action: true, action: true,
type: "accessToken", type: "accessToken",
apiKey: accessAuditUser
? undefined
: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
},
user: accessAuditUser,
metadata: accessAuditUser
? {
accessTokenId: tokenItem.accessTokenId,
accessTokenTitle: tokenItem.title
}
: undefined,
userAgent: req.headers["user-agent"], userAgent: req.headers["user-agent"],
requestIp: req.ip requestIp: req.ip
}); });
@@ -1,133 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { resources, users, userOrgs } from "@server/db";
import { eq, and } from "drizzle-orm";
import { createResourceSession } from "@server/auth/sessions/resource";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
import { response } from "@server/lib/response";
const createResourceSessionTokenParams = z.strictObject({
resourceId: z.coerce.number().int().positive()
});
const createResourceSessionTokenBody = z.strictObject({
userId: z.string().nonempty(),
idpId: z.coerce.number().int().positive().optional()
});
export type CreateResourceSessionTokenResponse = {
requestToken: string;
};
export async function createResourceSessionToken(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = createResourceSessionTokenParams.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = createResourceSessionTokenBody.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const { userId, idpId } = parsedBody.data;
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found`
)
);
}
const candidates = await db
.select({ userId: users.userId })
.from(userOrgs)
.innerJoin(users, eq(userOrgs.userId, users.userId))
.where(
and(
eq(users.userId, userId),
eq(userOrgs.orgId, resource.orgId)
)
);
if (candidates.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`User not found in the organization that owns this resource`
)
);
}
if (candidates.length > 1) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Multiple users match this username (external users from different identity providers). Specify idpId to disambiguate."
)
);
}
const targetUserId = candidates[0].userId;
const appSessionToken = generateSessionToken();
const appSession = await createSession(appSessionToken, targetUserId);
const requestToken = generateSessionToken();
await createResourceSession({
resourceId,
token: requestToken,
userSessionId: appSession.sessionId,
isRequestToken: true,
expiresAt: Date.now() + 1000 * 30, // 30 seconds
sessionLength: 1000 * 30,
doNotExtend: true
});
logger.debug("Resource session token created successfully");
return response<CreateResourceSessionTokenResponse>(res, {
data: { requestToken },
success: true,
error: false,
message: "Resource session token created successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+7 -2
View File
@@ -42,9 +42,14 @@ export async function getResourceStatusHistory(
const entityType = "resource"; const entityType = "resource";
const entityId = parsedParams.data.resourceId; const entityId = parsedParams.data.resourceId;
const { days } = parsedQuery.data; const { days, tzOffsetMinutes } = parsedQuery.data;
const data = await getCachedStatusHistory(entityType, entityId, days); const data = await getCachedStatusHistory(
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, { return response<StatusHistoryResponse>(res, {
data, data,
-1
View File
@@ -17,7 +17,6 @@ export * from "./getResourceWhitelist";
export * from "./authWithWhitelist"; export * from "./authWithWhitelist";
export * from "./authWithAccessToken"; export * from "./authWithAccessToken";
export * from "./getExchangeToken"; export * from "./getExchangeToken";
export * from "./createResourceSessionToken";
export * from "./createResourceRule"; export * from "./createResourceRule";
export * from "./deleteResourceRule"; export * from "./deleteResourceRule";
export * from "./listResourceRules"; export * from "./listResourceRules";
+7 -2
View File
@@ -42,9 +42,14 @@ export async function getSiteStatusHistory(
const entityType = "site"; const entityType = "site";
const entityId = parsedParams.data.siteId; const entityId = parsedParams.data.siteId;
const { days } = parsedQuery.data; const { days, tzOffsetMinutes } = parsedQuery.data;
const data = await getCachedStatusHistory(entityType, entityId, days); const data = await getCachedStatusHistory(
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, { return response<StatusHistoryResponse>(res, {
data, data,
@@ -94,7 +94,7 @@ export async function adminGeneratePasswordResetCode(
}); });
}); });
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${existingUser[0].email}&token=${token}`; const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(existingUser[0].email!)}&token=${token}`;
logger.info( logger.info(
`Admin generated password reset code for user ${existingUser[0].email} (${userId})` `Admin generated password reset code for user ${existingUser[0].email} (${userId})`
+2 -2
View File
@@ -287,7 +287,7 @@ export async function inviteUser(
) )
); );
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(
@@ -341,7 +341,7 @@ export async function inviteUser(
.values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId }))); .values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
}); });
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(
+5 -1
View File
@@ -20,7 +20,9 @@ import {
handleOlmServerPeerAddMessage, handleOlmServerPeerAddMessage,
handleOlmUnRelayMessage, handleOlmUnRelayMessage,
handleOlmDisconnectingMessage, handleOlmDisconnectingMessage,
handleOlmServerInitAddPeerHandshake handleOlmServerInitAddPeerHandshake,
handleOlmLocalMessage,
handleOlmUnLocalMessage
} from "../olm"; } from "../olm";
import { handleHealthcheckStatusMessage } from "../target"; import { handleHealthcheckStatusMessage } from "../target";
import { handleRoundTripMessage } from "./handleRoundTripMessage"; import { handleRoundTripMessage } from "./handleRoundTripMessage";
@@ -32,6 +34,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
"olm/wg/register": handleOlmRegisterMessage, "olm/wg/register": handleOlmRegisterMessage,
"olm/wg/relay": handleOlmRelayMessage, "olm/wg/relay": handleOlmRelayMessage,
"olm/wg/unrelay": handleOlmUnRelayMessage, "olm/wg/unrelay": handleOlmUnRelayMessage,
"olm/wg/local": handleOlmLocalMessage,
"olm/wg/unlocal": handleOlmUnLocalMessage,
"olm/ping": handleOlmPingMessage, "olm/ping": handleOlmPingMessage,
"olm/disconnecting": handleOlmDisconnectingMessage, "olm/disconnecting": handleOlmDisconnectingMessage,
"newt/disconnecting": handleNewtDisconnectingMessage, "newt/disconnecting": handleNewtDisconnectingMessage,
+33
View File
@@ -248,6 +248,39 @@ export async function loginProxy(
return await makeApiRequest<LoginResponse>(url, "POST", request); return await makeApiRequest<LoginResponse>(url, "POST", request);
} }
export async function logoutProxy(): Promise<ResponseT<null>> {
const env = pullEnv();
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/logout`;
const result = await makeApiRequest<null>(url, "POST");
try {
const headersList = await reqHeaders();
const host = headersList.get("host")?.split(":")[0];
const allCookies = await cookies();
const clearOptions = {
httpOnly: true,
secure: true,
sameSite: "lax" as const,
path: "/",
maxAge: 0
};
// Clear both host-only and domain-scoped variants.
allCookies.set(env.server.sessionCookieName, "", clearOptions);
if (host) {
allCookies.set(env.server.sessionCookieName, "", {
...clearOptions,
domain: host
});
}
} catch (cookieError) {
console.error("Failed to clear session cookie:", cookieError);
}
return result;
}
export async function securityKeyStartProxy( export async function securityKeyStartProxy(
request: SecurityKeyStartRequest, request: SecurityKeyStartRequest,
forceLogin?: boolean forceLogin?: boolean
+99 -4
View File
@@ -52,7 +52,6 @@ import { ChevronsUpDown } from "lucide-react";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
import { constructShareLink } from "@app/lib/shareLinks"; import { constructShareLink } from "@app/lib/shareLinks";
import { ShareLinkRow } from "@app/components/ShareLinksTable";
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
import { import {
Collapsible, Collapsible,
@@ -63,11 +62,26 @@ import AccessTokenSection from "@app/components/AccessTokenUsage";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toUnicode } from "punycode"; import { toUnicode } from "punycode";
import { ResourceSelector, type SelectedResource } from "./resource-selector"; import { ResourceSelector, type SelectedResource } from "./resource-selector";
import { UserSelector, type SelectedUser } from "@app/components/user-selector";
type CreatedShareLink = {
accessTokenId: string;
resourceId: number;
resourceName: string;
resourceNiceId: string;
title: string | null;
createdAt: number;
expiresAt: number | null;
userId?: string | null;
userName?: string | null;
username?: string | null;
userEmail?: string | null;
};
type FormProps = { type FormProps = {
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
onCreated?: (result: ShareLinkRow) => void; onCreated?: (result: CreatedShareLink) => void;
}; };
export default function CreateShareLinkForm({ export default function CreateShareLinkForm({
@@ -85,6 +99,8 @@ export default function CreateShareLinkForm({
const [accessToken, setAccessToken] = useState<string | null>(null); const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [neverExpire, setNeverExpire] = useState(false); const [neverExpire, setNeverExpire] = useState(false);
const [persistSession, setPersistSession] = useState(false);
const [selectedUser, setSelectedUser] = useState<SelectedUser | null>(null);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const t = useTranslations(); const t = useTranslations();
@@ -175,7 +191,9 @@ export default function CreateShareLinkForm({
values.resourceName || values.resourceName ||
"Resource" + values.resourceId "Resource" + values.resourceId
}), }),
path: values.path path: values.path,
persistSession,
userId: selectedUser?.id
} }
) )
.catch((e) => { .catch((e) => {
@@ -205,7 +223,11 @@ export default function CreateShareLinkForm({
resourceNiceId: selectedResource ? selectedResource.niceId : "", resourceNiceId: selectedResource ? selectedResource.niceId : "",
title: token.title, title: token.title,
createdAt: token.createdAt, createdAt: token.createdAt,
expiresAt: token.expiresAt expiresAt: token.expiresAt,
userId: token.userId,
userName: selectedUser?.text ?? null,
username: null,
userEmail: null
}); });
} }
@@ -220,6 +242,9 @@ export default function CreateShareLinkForm({
setOpen(val); setOpen(val);
setLink(null); setLink(null);
setLoading(false); setLoading(false);
setNeverExpire(false);
setPersistSession(false);
setSelectedUser(null);
form.reset(); form.reset();
}} }}
> >
@@ -344,6 +369,48 @@ export default function CreateShareLinkForm({
)} )}
/> />
<div className="space-y-2">
<FormLabel>
{t(
"shareAssociateUserOptional"
)}
</FormLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between",
!selectedUser &&
"text-muted-foreground"
)}
>
{selectedUser?.text
? selectedUser.text
: t("userSelect")}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<UserSelector
orgId={org.org.orgId}
selectedUser={
selectedUser
}
onSelectUser={
setSelectedUser
}
/>
</PopoverContent>
</Popover>
<p className="text-sm text-muted-foreground">
{t(
"shareAssociateUserDescription"
)}
</p>
</div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<FormLabel> <FormLabel>
@@ -437,6 +504,34 @@ export default function CreateShareLinkForm({
</label> </label>
</div> </div>
<div className="flex items-start space-x-2">
<Checkbox
id="persist-session"
checked={persistSession}
onCheckedChange={(val) =>
setPersistSession(
val as boolean
)
}
className="mt-0.5"
/>
<div className="space-y-1">
<label
htmlFor="persist-session"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t(
"sharePersistSession"
)}
</label>
<p className="text-sm text-muted-foreground">
{t(
"sharePersistSessionDescription"
)}
</p>
</div>
</div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("shareExpireDescription")} {t("shareExpireDescription")}
</p> </p>
+20 -8
View File
@@ -93,14 +93,20 @@ export default function InviteStatusCard({
setType(type); setType(type);
if (!user && type === "user_does_not_exist") { if (!user && type === "user_does_not_exist") {
const inviteRedirect = encodeURIComponent(
`/invite?token=${tokenParam}`
);
const redirectUrl = email const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}` ? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`; : `/auth/signup?redirect=${inviteRedirect}`;
router.push(redirectUrl); router.push(redirectUrl);
} else if (!user && type === "not_logged_in") { } else if (!user && type === "not_logged_in") {
const inviteRedirect = encodeURIComponent(
`/invite?token=${tokenParam}`
);
const redirectUrl = email const redirectUrl = email
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}` ? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${tokenParam}`; : `/auth/login?redirect=${inviteRedirect}`;
router.push(redirectUrl); router.push(redirectUrl);
} else { } else {
setLoading(false); setLoading(false);
@@ -112,17 +118,23 @@ export default function InviteStatusCard({
async function goToLogin() { async function goToLogin() {
await api.post("/auth/logout", {}); await api.post("/auth/logout", {});
const inviteRedirect = encodeURIComponent(
`/invite?token=${tokenParam}`
);
const redirectUrl = email const redirectUrl = email
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}` ? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${tokenParam}`; : `/auth/login?redirect=${inviteRedirect}`;
router.push(redirectUrl); router.push(redirectUrl);
} }
async function goToSignup() { async function goToSignup() {
await api.post("/auth/logout", {}); await api.post("/auth/logout", {});
const inviteRedirect = encodeURIComponent(
`/invite?token=${tokenParam}`
);
const redirectUrl = email const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}` ? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`; : `/auth/signup?redirect=${inviteRedirect}`;
router.push(redirectUrl); router.push(redirectUrl);
} }
+12 -9
View File
@@ -12,8 +12,8 @@ import { Shield, ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { createApiClient } from "@app/lib/api"; import { useState } from "react";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { logoutProxy } from "@app/actions/server";
type OrgPolicyRequiredProps = { type OrgPolicyRequiredProps = {
orgId: string; orgId: string;
@@ -40,21 +40,23 @@ export default function OrgPolicyRequired({
}: OrgPolicyRequiredProps) { }: OrgPolicyRequiredProps) {
const t = useTranslations(); const t = useTranslations();
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext());
const sessionExpired = const sessionExpired =
policies?.maxSessionLength && policies?.maxSessionLength &&
policies.maxSessionLength.compliant === false; policies.maxSessionLength.compliant === false;
function reauthenticate() { async function reauthenticate() {
api.post("/auth/logout") setLoading(true);
.catch(() => {}) try {
.then(() => { await logoutProxy();
} catch (error) {
console.error("Error during logout:", error);
} finally {
const destination = redirectAfterAuth ?? `/${orgId}`; const destination = redirectAfterAuth ?? `/${orgId}`;
router.push(destination); router.push(destination);
router.refresh(); router.refresh();
}); }
} }
if (sessionExpired) { if (sessionExpired) {
@@ -76,6 +78,7 @@ export default function OrgPolicyRequired({
<Button <Button
className="w-full" className="w-full"
onClick={reauthenticate} onClick={reauthenticate}
loading={loading}
> >
{t("reauthenticate")} {t("reauthenticate")}
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
+7
View File
@@ -143,6 +143,13 @@ function getActionsCategories(root: boolean) {
Logs: { Logs: {
[t("actionExportLogs")]: "exportLogs", [t("actionExportLogs")]: "exportLogs",
[t("actionViewLogs")]: "viewLogs" [t("actionViewLogs")]: "viewLogs"
},
"Site Provisioning Key": {
[t("actionCreateSiteProvisioningKey")]: "createSiteProvisioningKey",
[t("actionListSiteProvisioningKeys")]: "listSiteProvisioningKeys",
[t("actionUpdateSiteProvisioningKey")]: "updateSiteProvisioningKey",
[t("actionDeleteSiteProvisioningKey")]: "deleteSiteProvisioningKey"
} }
}; };
+40
View File
@@ -35,6 +35,7 @@ import moment from "moment";
import CreateShareLinkForm from "@app/components/CreateShareLinkForm"; import CreateShareLinkForm from "@app/components/CreateShareLinkForm";
import { constructShareLink } from "@app/lib/shareLinks"; import { constructShareLink } from "@app/lib/shareLinks";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
export type ShareLinkRow = { export type ShareLinkRow = {
accessTokenId: string; accessTokenId: string;
@@ -44,6 +45,10 @@ export type ShareLinkRow = {
title: string | null; title: string | null;
createdAt: number; createdAt: number;
expiresAt: number | null; expiresAt: number | null;
userId?: string | null;
userName?: string | null;
username?: string | null;
userEmail?: string | null;
}; };
type ShareLinksTableProps = { type ShareLinksTableProps = {
@@ -155,6 +160,41 @@ export default function ShareLinksTable({
); );
} }
}, },
{
accessorKey: "userId",
friendlyName: t("user"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("user")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const r = row.original;
if (!r.userId) {
return <span>-</span>;
}
return (
<Link href={`/${orgId}/settings/access/users/${r.userId}`}>
<Button variant="outline" size="sm">
{getUserDisplayName({
email: r.userEmail,
name: r.userName,
username: r.username
})}
<ArrowUpRight className="ml-2 h-3 w-3" />
</Button>
</Link>
);
}
},
// { // {
// accessorKey: "domain", // accessorKey: "domain",
// header: "Link", // header: "Link",
+106
View File
@@ -0,0 +1,106 @@
import { orgQueries } from "@app/lib/queries";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useQuery } from "@tanstack/react-query";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "./ui/command";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { CheckIcon } from "lucide-react";
import { cn } from "@app/lib/cn";
import { useDebounce } from "use-debounce";
import type { SelectedUser } from "./users-selector";
export type { SelectedUser };
export type UserSelectorProps = {
orgId: string;
selectedUser?: SelectedUser | null;
onSelectUser: (user: SelectedUser | null) => void;
allowClear?: boolean;
};
export function UserSelector({
orgId,
selectedUser,
onSelectUser,
allowClear = true
}: UserSelectorProps) {
const t = useTranslations();
const [userSearchQuery, setUserSearchQuery] = useState("");
const [debouncedValue] = useDebounce(userSearchQuery, 150);
const { data: users = [] } = useQuery(
orgQueries.users({ orgId, perPage: 10, query: debouncedValue })
);
const usersShown = useMemo(() => {
const allUsers: Array<SelectedUser> = users.map((u) => ({
id: u.id,
text: getUserDisplayName(u)
}));
if (
debouncedValue.trim().length === 0 &&
selectedUser &&
!allUsers.find((user) => user.id === selectedUser.id)
) {
allUsers.unshift(selectedUser);
}
return allUsers;
}, [users, selectedUser, debouncedValue]);
return (
<Command shouldFilter={false}>
<CommandInput
placeholder={t("userSearch")}
value={userSearchQuery}
onValueChange={setUserSearchQuery}
/>
<CommandList>
<CommandEmpty>{t("usersNotFound")}</CommandEmpty>
<CommandGroup>
{allowClear && (
<CommandItem
value="__none__"
onSelect={() => {
onSelectUser(null);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
!selectedUser ? "opacity-100" : "opacity-0"
)}
/>
{t("none")}
</CommandItem>
)}
{usersShown.map((user) => (
<CommandItem
value={`${user.text}:${user.id}`}
key={user.id}
onSelect={() => {
onSelectUser(user);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
user.id === selectedUser?.id
? "opacity-100"
: "opacity-0"
)}
/>
{user.text}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
);
}
+12 -5
View File
@@ -650,9 +650,13 @@ export const orgQueries = {
queryOptions({ queryOptions({
queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const, queryKey: ["SITE_STATUS_HISTORY", siteId, days] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const tzOffsetMinutes = -new Date().getTimezoneOffset();
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<StatusHistoryResponse> AxiosResponse<StatusHistoryResponse>
>(`/site/${siteId}/status-history?days=${days}`, { signal }); >(
`/site/${siteId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
{ signal }
);
return res.data.data; return res.data.data;
} }
}), }),
@@ -667,11 +671,13 @@ export const orgQueries = {
queryOptions({ queryOptions({
queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, days] as const, queryKey: ["RESOURCE_STATUS_HISTORY", resourceId, days] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const tzOffsetMinutes = -new Date().getTimezoneOffset();
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<StatusHistoryResponse> AxiosResponse<StatusHistoryResponse>
>(`/resource/${resourceId}/status-history?days=${days}`, { >(
signal `/resource/${resourceId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
}); { signal }
);
return res.data.data; return res.data.data;
} }
}), }),
@@ -693,10 +699,11 @@ export const orgQueries = {
days days
] as const, ] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const tzOffsetMinutes = -new Date().getTimezoneOffset();
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<StatusHistoryResponse> AxiosResponse<StatusHistoryResponse>
>( >(
`/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}`, `/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`,
{ signal } { signal }
); );
return res.data.data; return res.data.data;