Compare commits

..

15 Commits

Author SHA1 Message Date
dependabot[bot] b66430d8d8 Bump golang.org/x/term
Bumps the go-install-dependencies group with 1 update in the /install directory: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-install-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 01:33:07 +00:00
Owen Schwartz 5d20956a0e Merge pull request #3450 from fosrl/dev
1.20.0-s.2
2026-07-15 20:55:40 -04:00
Milo Schwartz e5398d441e Merge pull request #3425 from fosrl/dev
Dev
2026-07-09 21:33:17 -04:00
Milo Schwartz d4138e2141 Merge pull request #3421 from fosrl/dev
1.20.0
2026-07-09 10:40:35 -04:00
Milo Schwartz 09d5d9082e Merge pull request #3413 from fosrl/dev
1.20.0
2026-07-08 15:30:03 -04:00
Owen Schwartz 5cb316f4e9 Merge pull request #3404 from fosrl/dev
Add ? to support undefined
2026-07-07 10:59:56 -04:00
Owen Schwartz 6b6c9cf4d8 Merge pull request #3403 from fosrl/dev
Move jit mode to a config param
2026-07-07 10:41:59 -04:00
Owen Schwartz 05617c63c0 Merge pull request #3402 from fosrl/dev
1.19.4-s.5
2026-07-07 10:21:57 -04:00
Owen Schwartz d4c52bbf2f Merge pull request #3396 from fosrl/dev
1.19.4-s.5
2026-07-03 16:15:18 -04:00
Owen Schwartz 87f50bf0cc Merge pull request #3389 from fosrl/dev
1.19.4-s.3
2026-07-03 10:29:15 -04:00
Owen Schwartz 2c66da1b19 Merge pull request #3386 from v1rusnl/main
Upgrade Traefik image to version 3.7
2026-07-03 10:18:44 -04:00
v1rusnl ab19955502 Upgrade Traefik image to version 3.7 2026-07-03 08:22:13 +02:00
v1rusnl 1db9dcec81 Update Traefik image version to v3.7 2026-07-03 08:21:12 +02:00
Owen Schwartz 49c2d3163e Merge pull request #3381 from fosrl/dev
dev
2026-07-02 10:56:39 -04:00
Owen Schwartz 45b9e13a13 Merge pull request #3378 from fosrl/dev
1.19.4-s.1
2026-07-01 21:48:01 -04:00
67 changed files with 387 additions and 2000 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode
traefik: traefik:
image: traefik:v3.6 image: traefik:v3.7
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service network_mode: service:gerbil # Ports appear on the gerbil service
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}} - 80:80{{end}}
traefik: traefik:
image: docker.io/traefik:v3.6 image: docker.io/traefik:v3.7
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}} {{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
+2 -2
View File
@@ -5,7 +5,7 @@ go 1.25.0
require ( require (
github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.44.0 golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
) )
+4 -4
View File
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+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 caching solution. Required for HA. Not required for the Enterprise version.") redisFlag = flag.Bool("redis", false, "Install Redis as cacheing 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
+4 -18
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Потвърдете изтриването на споделима връзка", "shareDeleteConfirm": "Потвърдете изтриването на споделима връзка",
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?", "shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.", "shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
"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.", "shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
"accessToken": "Достъп Токен", "accessToken": "Достъп Токен",
"usageExamples": "Примери за използване", "usageExamples": "Примери за използване",
"tokenId": "Токен ID", "tokenId": "Токен ID",
@@ -196,14 +196,8 @@
"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": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.",
@@ -1517,10 +1511,6 @@
"actionListInvitations": "Списък с покани", "actionListInvitations": "Списък с покани",
"actionExportLogs": "Експортиране на дневници", "actionExportLogs": "Експортиране на дневници",
"actionViewLogs": "Преглед на дневници", "actionViewLogs": "Преглед на дневници",
"actionCreateSiteProvisioningKey": "Създаване на ключ за предоставяне на сайт",
"actionListSiteProvisioningKeys": "Списък на ключовете за предоставяне на сайт",
"actionUpdateSiteProvisioningKey": "Актуализиране на ключ за предоставяне на сайт",
"actionDeleteSiteProvisioningKey": "Изтриване на ключ за предоставяне на сайт",
"noneSelected": "Нищо не е избрано", "noneSelected": "Нищо не е избрано",
"orgNotFound2": "Няма намерени организации.", "orgNotFound2": "Няма намерени организации.",
"search": "Търси…", "search": "Търси…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Поддомейн: {subdomain}", "domainPickerSubdomain": "Поддомейн: {subdomain}",
"domainPickerNamespace": "Име на пространство: {namespace}", "domainPickerNamespace": "Име на пространство: {namespace}",
"domainPickerShowMore": "Покажи повече", "domainPickerShowMore": "Покажи повече",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Избор на регион", "regionSelectorTitle": "Избор на регион",
"domainPickerRemoteExitNodeWarning": "Предоставените домейни не се поддържат, когато сайтовете се свързват към отдалечени крайни възли. За да бъдат ресурсите налични на отдалечени възли, използвайте персонализиран домейн вместо това.", "domainPickerRemoteExitNodeWarning": "Предоставените домейни не се поддържат, когато сайтовете се свързват към отдалечени крайни възли. За да бъдат ресурсите налични на отдалечени възли, използвайте персонализиран домейн вместо това.",
"regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.", "regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Източен адрес", "sourceAddress": "Източен адрес",
"destinationAddress": "Адрес на дестинация", "destinationAddress": "Адрес на дестинация",
"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": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <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>", "ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"certResolver": "Решавач на сертификати", "certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.", "certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати", "selectCertResolver": "Изберете решавач на сертификати",
@@ -3824,6 +3811,5 @@
"rdpUploadFailed": "Качването неуспешно", "rdpUploadFailed": "Качването неуспешно",
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура", "rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
"sessionToolbarShow": "Показване на лентата с инструменти", "sessionToolbarShow": "Показване на лентата с инструменти",
"sessionToolbarHide": "Скриване на лентата с инструменти", "sessionToolbarHide": "Скриване на лентата с инструменти"
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
} }
+4 -18
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": "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.", "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.",
"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,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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í…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdoména: {subdomain}", "domainPickerSubdomain": "Subdoména: {subdomain}",
"domainPickerNamespace": "Jmenný prostor: {namespace}", "domainPickerNamespace": "Jmenný prostor: {namespace}",
"domainPickerShowMore": "Zobrazit více", "domainPickerShowMore": "Zobrazit více",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Vybrat region", "regionSelectorTitle": "Vybrat region",
"domainPickerRemoteExitNodeWarning": "Poskytnuté domény nejsou podporovány, když se stránky připojují k vzdáleným výstupním uzlům. Pro dostupné zdroje na vzdálených uzlech použijte vlastní doménu.", "domainPickerRemoteExitNodeWarning": "Poskytnuté domény nejsou podporovány, když se stránky připojují k vzdáleným výstupním uzlům. Pro dostupné zdroje na vzdálených uzlech použijte vlastní doménu.",
"regionSelectorInfo": "Výběr regionu nám pomáhá poskytovat lepší výkon pro vaši polohu. Nemusíte být ve stejném regionu jako váš server.", "regionSelectorInfo": "Výběr regionu nám pomáhá poskytovat lepší výkon pro vaši polohu. Nemusíte být ve stejném regionu jako váš server.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Zdrojová adresa", "sourceAddress": "Zdrojová adresa",
"destinationAddress": "Cílová adresa", "destinationAddress": "Cílová adresa",
"duration": "Doba trvání", "duration": "Doba trvání",
"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": "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>.",
"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": "<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>.",
"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ů",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"accessToken": "Adgangstoken", "accessToken": "Adgangstoken",
"usageExamples": "Brukseksempler", "usageExamples": "Brukseksempler",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Underdomæne: {subdomain}", "domainPickerSubdomain": "Underdomæne: {subdomain}",
"domainPickerNamespace": "Navnerom: {namespace}", "domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mere", "domainPickerShowMore": "Vis mere",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Vælg Region", "regionSelectorTitle": "Vælg Region",
"domainPickerRemoteExitNodeWarning": "Leverede domæner understøttes ikke, når sites kobles til eksterne exitnoder. For ressourcer, der skal være tilgængelige på eksterne noder, skal du i stedet bruge et brugerdefineret domæne.", "domainPickerRemoteExitNodeWarning": "Leverede domæner understøttes ikke, når sites kobles til eksterne exitnoder. For ressourcer, der skal være tilgængelige på eksterne noder, skal du i stedet bruge et brugerdefineret domæne.",
"regionSelectorInfo": "At vælge en region hjelper oss med at give bedre ydeevne for din placering. Du behøver ikke være i samme region som serveren.", "regionSelectorInfo": "At vælge en region hjelper oss med at give bedre ydeevne for din placering. Du behøver ikke være i samme region som serveren.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Kildeadresse", "sourceAddress": "Kildeadresse",
"destinationAddress": "Måladresse ", "destinationAddress": "Måladresse ",
"duration": "Varighed", "duration": "Varighed",
"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": "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>.",
"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": "<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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"accessToken": "Zugriffstoken", "accessToken": "Zugriffstoken",
"usageExamples": "Nutzungsbeispiele", "usageExamples": "Nutzungsbeispiele",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdomain: {subdomain}", "domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}", "domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mehr anzeigen", "domainPickerShowMore": "Mehr anzeigen",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Region auswählen", "regionSelectorTitle": "Region auswählen",
"domainPickerRemoteExitNodeWarning": "Angegebene Domains werden nicht unterstützt, wenn sich Websites mit externen Exit-Knoten verbinden. Damit Ressourcen auf entfernten Knoten verfügbar sind, verwenden Sie stattdessen eine eigene Domain.", "domainPickerRemoteExitNodeWarning": "Angegebene Domains werden nicht unterstützt, wenn sich Websites mit externen Exit-Knoten verbinden. Damit Ressourcen auf entfernten Knoten verfügbar sind, verwenden Sie stattdessen eine eigene Domain.",
"regionSelectorInfo": "Das Auswählen einer Region hilft uns, eine bessere Leistung für Ihren Standort bereitzustellen. Sie müssen sich nicht in derselben Region wie Ihr Server befinden.", "regionSelectorInfo": "Das Auswählen einer Region hilft uns, eine bessere Leistung für Ihren Standort bereitzustellen. Sie müssen sich nicht in derselben Region wie Ihr Server befinden.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Quelladresse", "sourceAddress": "Quelladresse",
"destinationAddress": "Zieladresse", "destinationAddress": "Zieladresse",
"duration": "Dauer", "duration": "Dauer",
"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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -15
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 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.", "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.",
"accessToken": "Access Token", "accessToken": "Access Token",
"usageExamples": "Usage Examples", "usageExamples": "Usage Examples",
"tokenId": "Token ID", "tokenId": "Token ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -3095,8 +3085,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",
@@ -3821,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"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,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdominio: {subdomain}", "domainPickerSubdomain": "Subdominio: {subdomain}",
"domainPickerNamespace": "Espacio de nombres: {namespace}", "domainPickerNamespace": "Espacio de nombres: {namespace}",
"domainPickerShowMore": "Mostrar más", "domainPickerShowMore": "Mostrar más",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Seleccionar Región", "regionSelectorTitle": "Seleccionar Región",
"domainPickerRemoteExitNodeWarning": "Los dominios suministrados no son compatibles cuando los sitios se conectan a nodos de salida remotos. Para que los recursos estén disponibles en nodos remotos, utilice un dominio personalizado en su lugar.", "domainPickerRemoteExitNodeWarning": "Los dominios suministrados no son compatibles cuando los sitios se conectan a nodos de salida remotos. Para que los recursos estén disponibles en nodos remotos, utilice un dominio personalizado en su lugar.",
"regionSelectorInfo": "Seleccionar una región nos ayuda a brindar un mejor rendimiento para tu ubicación. No tienes que estar en la misma región que tu servidor.", "regionSelectorInfo": "Seleccionar una región nos ayuda a brindar un mejor rendimiento para tu ubicación. No tienes que estar en la misma región que tu servidor.",
@@ -3098,8 +3085,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": "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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmer la suppression du lien partageable", "shareDeleteConfirm": "Confirmer la suppression du lien partageable",
"shareQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce lien de partage ?", "shareQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce lien de partage ?",
"shareMessageRemove": "Une fois supprimé, le lien ne fonctionnera plus et toute personne l'utilisant perdra l'accès à la ressource.", "shareMessageRemove": "Une fois supprimé, le lien ne fonctionnera plus et toute personne l'utilisant perdra l'accès à la ressource.",
"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.", "shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
"accessToken": "Jeton d'accès", "accessToken": "Jeton d'accès",
"usageExamples": "Exemples d'utilisation", "usageExamples": "Exemples d'utilisation",
"tokenId": "ID du jeton", "tokenId": "ID du jeton",
@@ -196,14 +196,8 @@
"shareTitleOptional": "Titre (facultatif)", "shareTitleOptional": "Titre (facultatif)",
"sharePathOptional": "Chemin (optionnel)", "sharePathOptional": "Chemin (optionnel)",
"sharePathDescription": "Le lien redirigera les utilisateurs vers ce chemin après l'authentification.", "sharePathDescription": "Le lien redirigera les utilisateurs vers ce chemin après l'authentification.",
"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 dans", "expireIn": "Expire dans",
"neverExpire": "N'expire jamais", "neverExpire": "N'expire jamais",
"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": "Le délai d'expiration correspond à la période pendant laquelle le lien sera utilisable et permettra d'accéder à la ressource. Passé ce délai, le lien ne fonctionnera plus et les utilisateurs qui l'ont utilisé perdront l'accès à la ressource.", "shareExpireDescription": "Le délai d'expiration correspond à la période pendant laquelle le lien sera utilisable et permettra d'accéder à la ressource. Passé ce délai, le lien ne fonctionnera plus et les utilisateurs qui l'ont utilisé perdront l'accès à la ressource.",
"shareSeeOnce": "Vous ne pourrez voir ce lien qu'une seule fois. N'oubliez pas de le copier.", "shareSeeOnce": "Vous ne pourrez voir ce lien qu'une seule fois. N'oubliez pas de le copier.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.", "shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Sous-domaine : {subdomain}", "domainPickerSubdomain": "Sous-domaine : {subdomain}",
"domainPickerNamespace": "Espace de noms : {namespace}", "domainPickerNamespace": "Espace de noms : {namespace}",
"domainPickerShowMore": "Afficher plus", "domainPickerShowMore": "Afficher plus",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Sélectionner Région", "regionSelectorTitle": "Sélectionner Région",
"domainPickerRemoteExitNodeWarning": "Les domaines fournis ne sont pas pris en charge lorsque les sites se connectent à des nœuds de sortie distants. Pour que les ressources soient disponibles sur des nœuds distants, utilisez un domaine personnalisé à la place.", "domainPickerRemoteExitNodeWarning": "Les domaines fournis ne sont pas pris en charge lorsque les sites se connectent à des nœuds de sortie distants. Pour que les ressources soient disponibles sur des nœuds distants, utilisez un domaine personnalisé à la place.",
"regionSelectorInfo": "Sélectionner une région nous aide à offrir de meilleures performances pour votre localisation. Vous n'avez pas besoin d'être dans la même région que votre serveur.", "regionSelectorInfo": "Sélectionner une région nous aide à offrir de meilleures performances pour votre localisation. Vous n'avez pas besoin d'être dans la même région que votre serveur.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Adresse source", "sourceAddress": "Adresse source",
"destinationAddress": "Adresse de destination", "destinationAddress": "Adresse de destination",
"duration": "Durée", "duration": "Durée",
"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": "Une <enterpriseLicenseLink>licence Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> est requise pour utiliser cette fonctionnalité. <bookADemoLink>Réservez une démonstration ou une évaluation de 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>", "ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Réservez une démo ou un essai POC</bookADemoLink>.",
"certResolver": "Résolveur de certificat", "certResolver": "Résolveur de certificat",
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.", "certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
"selectCertResolver": "Sélectionnez le résolveur de certificat", "selectCertResolver": "Sélectionnez le résolveur de certificat",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"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,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Sottodominio: {subdomain}", "domainPickerSubdomain": "Sottodominio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}", "domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostra Altro", "domainPickerShowMore": "Mostra Altro",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Seleziona regione", "regionSelectorTitle": "Seleziona regione",
"domainPickerRemoteExitNodeWarning": "I domini forniti non sono supportati quando i siti si connettono a nodi di uscita remoti. Affinché le risorse siano disponibili su nodi remoti, utilizza invece un dominio personalizzato.", "domainPickerRemoteExitNodeWarning": "I domini forniti non sono supportati quando i siti si connettono a nodi di uscita remoti. Affinché le risorse siano disponibili su nodi remoti, utilizza invece un dominio personalizzato.",
"regionSelectorInfo": "Selezionare una regione ci aiuta a fornire migliori performance per la tua posizione. Non devi necessariamente essere nella stessa regione del tuo server.", "regionSelectorInfo": "Selezionare una regione ci aiuta a fornire migliori performance per la tua posizione. Non devi necessariamente essere nella stessa regione del tuo server.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Indirizzo Di Origine", "sourceAddress": "Indirizzo Di Origine",
"destinationAddress": "Indirizzo Di Destinazione", "destinationAddress": "Indirizzo Di Destinazione",
"duration": "Durata", "duration": "Durata",
"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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "공유 가능한 링크 삭제 확인", "shareDeleteConfirm": "공유 가능한 링크 삭제 확인",
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?", "shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.", "shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
"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.", "shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
"accessToken": "액세스 토큰", "accessToken": "액세스 토큰",
"usageExamples": "사용 예", "usageExamples": "사용 예",
"tokenId": "토큰 ID", "tokenId": "토큰 ID",
@@ -196,14 +196,8 @@
"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": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.",
@@ -1517,10 +1511,6 @@
"actionListInvitations": "초대 목록", "actionListInvitations": "초대 목록",
"actionExportLogs": "로그 내보내기", "actionExportLogs": "로그 내보내기",
"actionViewLogs": "로그 보기", "actionViewLogs": "로그 보기",
"actionCreateSiteProvisioningKey": "사이트 프로비저닝 키 생성",
"actionListSiteProvisioningKeys": "사이트 프로비저닝 키 목록",
"actionUpdateSiteProvisioningKey": "사이트 프로비저닝 키 업데이트",
"actionDeleteSiteProvisioningKey": "사이트 프로비저닝 키 삭제",
"noneSelected": "선택된 항목 없음", "noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.", "orgNotFound2": "조직이 없습니다.",
"search": "검색…", "search": "검색…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "서브도메인: {subdomain}", "domainPickerSubdomain": "서브도메인: {subdomain}",
"domainPickerNamespace": "이름 공간: {namespace}", "domainPickerNamespace": "이름 공간: {namespace}",
"domainPickerShowMore": "더보기", "domainPickerShowMore": "더보기",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "지역 선택", "regionSelectorTitle": "지역 선택",
"domainPickerRemoteExitNodeWarning": "제공된 도메인은 원격 종료 노드에 연결된 사이트에서 지원되지 않습니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.", "domainPickerRemoteExitNodeWarning": "제공된 도메인은 원격 종료 노드에 연결된 사이트에서 지원되지 않습니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.",
"regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.", "regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "소스 주소", "sourceAddress": "소스 주소",
"destinationAddress": "대상 주소", "destinationAddress": "대상 주소",
"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": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <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>", "ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이(가) 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"certResolver": "인증서 해결사", "certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.", "certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택", "selectCertResolver": "인증서 해결사 선택",
@@ -3824,6 +3811,5 @@
"rdpUploadFailed": "업로드 실패", "rdpUploadFailed": "업로드 실패",
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드", "rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
"sessionToolbarShow": "툴바 보기", "sessionToolbarShow": "툴바 보기",
"sessionToolbarHide": "툴바 숨기기", "sessionToolbarHide": "툴바 숨기기"
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
} }
+4 -18
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": "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.", "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.",
"accessToken": "Tilgangsnøkkel", "accessToken": "Tilgangsnøkkel",
"usageExamples": "Brukseksempler", "usageExamples": "Brukseksempler",
"tokenId": "Token-ID", "tokenId": "Token-ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Underdomene: {subdomain}", "domainPickerSubdomain": "Underdomene: {subdomain}",
"domainPickerNamespace": "Navnerom: {namespace}", "domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mer", "domainPickerShowMore": "Vis mer",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Velg Region", "regionSelectorTitle": "Velg Region",
"domainPickerRemoteExitNodeWarning": "Tilbudte domener støttes ikke når sider kobles til eksterne avkjøringsnoder. For ressurser som skal være tilgjengelige på eksterne noder, brukes et egendefinert domene i stedet.", "domainPickerRemoteExitNodeWarning": "Tilbudte domener støttes ikke når sider kobles til eksterne avkjøringsnoder. For ressurser som skal være tilgjengelige på eksterne noder, brukes et egendefinert domene i stedet.",
"regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.", "regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Kilde adresse", "sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)", "destinationAddress": "Måladresse (Automatic Translation)",
"duration": "Varighet", "duration": "Varighet",
"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": "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>.",
"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": "<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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"accessToken": "Toegangs-token", "accessToken": "Toegangs-token",
"usageExamples": "Voorbeelden van gebruik", "usageExamples": "Voorbeelden van gebruik",
"tokenId": "Token ID", "tokenId": "Token ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdomein: {subdomain}", "domainPickerSubdomain": "Subdomein: {subdomain}",
"domainPickerNamespace": "Naamruimte: {namespace}", "domainPickerNamespace": "Naamruimte: {namespace}",
"domainPickerShowMore": "Meer weergeven", "domainPickerShowMore": "Meer weergeven",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Selecteer Regio", "regionSelectorTitle": "Selecteer Regio",
"domainPickerRemoteExitNodeWarning": "Opgegeven domeinen worden niet ondersteund wanneer websites verbinding maken met externe sluitnodes. Gebruik in plaats daarvan een aangepast domein. Om bronnen beschikbaar te maken op externe nodes.", "domainPickerRemoteExitNodeWarning": "Opgegeven domeinen worden niet ondersteund wanneer websites verbinding maken met externe sluitnodes. Gebruik in plaats daarvan een aangepast domein. Om bronnen beschikbaar te maken op externe nodes.",
"regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.", "regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Bron adres", "sourceAddress": "Bron adres",
"destinationAddress": "Adres bestemming", "destinationAddress": "Adres bestemming",
"duration": "Duur", "duration": "Duur",
"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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
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": "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.", "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.",
"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,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdomena: {subdomain}", "domainPickerSubdomain": "Subdomena: {subdomain}",
"domainPickerNamespace": "Przestrzeń nazw: {namespace}", "domainPickerNamespace": "Przestrzeń nazw: {namespace}",
"domainPickerShowMore": "Pokaż więcej", "domainPickerShowMore": "Pokaż więcej",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Wybierz region", "regionSelectorTitle": "Wybierz region",
"domainPickerRemoteExitNodeWarning": "Podane domeny nie są obsługiwane, gdy witryny łączą się ze zdalnymi węzłami wyjścia. Aby zasoby były dostępne w węzłach zdalnych, użyj domeny niestandardowej.", "domainPickerRemoteExitNodeWarning": "Podane domeny nie są obsługiwane, gdy witryny łączą się ze zdalnymi węzłami wyjścia. Aby zasoby były dostępne w węzłach zdalnych, użyj domeny niestandardowej.",
"regionSelectorInfo": "Wybór regionu pomaga nam zapewnić lepszą wydajność dla Twojej lokalizacji. Nie musisz być w tym samym regionie co Twój serwer.", "regionSelectorInfo": "Wybór regionu pomaga nam zapewnić lepszą wydajność dla Twojej lokalizacji. Nie musisz być w tym samym regionie co Twój serwer.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Adres źródłowy", "sourceAddress": "Adres źródłowy",
"destinationAddress": "Adres docelowy", "destinationAddress": "Adres docelowy",
"duration": "Czas trwania", "duration": "Czas trwania",
"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": "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>.",
"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": "<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>.",
"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",
@@ -3824,6 +3811,5 @@
"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ę"
} }
+4 -18
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": "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.", "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.",
"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,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Subdomínio: {subdomain}", "domainPickerSubdomain": "Subdomínio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}", "domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostrar Mais", "domainPickerShowMore": "Mostrar Mais",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Selecionar Região", "regionSelectorTitle": "Selecionar Região",
"domainPickerRemoteExitNodeWarning": "Domínios fornecidos não são suportados quando os sites se conectam a nós de saída remota. Para recursos disponíveis em nós remotos, use um domínio personalizado.", "domainPickerRemoteExitNodeWarning": "Domínios fornecidos não são suportados quando os sites se conectam a nós de saída remota. Para recursos disponíveis em nós remotos, use um domínio personalizado.",
"regionSelectorInfo": "Selecionar uma região nos ajuda a fornecer melhor desempenho para sua localização. Você não precisa estar na mesma região que seu servidor.", "regionSelectorInfo": "Selecionar uma região nos ajuda a fornecer melhor desempenho para sua localização. Você não precisa estar na mesma região que seu servidor.",
@@ -3098,8 +3085,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": "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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Подтвердить удаление общей ссылки", "shareDeleteConfirm": "Подтвердить удаление общей ссылки",
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?", "shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.", "shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
"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.", "shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
"accessToken": "Токен доступа", "accessToken": "Токен доступа",
"usageExamples": "Примеры использования", "usageExamples": "Примеры использования",
"tokenId": "ID токена", "tokenId": "ID токена",
@@ -196,14 +196,8 @@
"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": "Любой, у кого есть эта ссылка, может получить доступ к ресурсу. Делитесь ею с осторожностью.",
@@ -1517,10 +1511,6 @@
"actionListInvitations": "Список приглашений", "actionListInvitations": "Список приглашений",
"actionExportLogs": "Экспорт журналов", "actionExportLogs": "Экспорт журналов",
"actionViewLogs": "Просмотр журналов", "actionViewLogs": "Просмотр журналов",
"actionCreateSiteProvisioningKey": "Создать ключ конфигурации сайта",
"actionListSiteProvisioningKeys": "Список ключей конфигурации сайтов",
"actionUpdateSiteProvisioningKey": "Обновить ключ конфигурации сайта",
"actionDeleteSiteProvisioningKey": "Удалить ключ конфигурации сайта",
"noneSelected": "Ничего не выбрано", "noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.", "orgNotFound2": "Организации не найдены.",
"search": "Поиск…", "search": "Поиск…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Поддомен: {subdomain}", "domainPickerSubdomain": "Поддомен: {subdomain}",
"domainPickerNamespace": "Пространство имен: {namespace}", "domainPickerNamespace": "Пространство имен: {namespace}",
"domainPickerShowMore": "Показать еще", "domainPickerShowMore": "Показать еще",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Выберите регион", "regionSelectorTitle": "Выберите регион",
"domainPickerRemoteExitNodeWarning": "Предоставленные домены не поддерживаются при подключении сайтов к удаленным узлам. Для доступа к ресурсам на удаленных узлах используйте пользовательский домен.", "domainPickerRemoteExitNodeWarning": "Предоставленные домены не поддерживаются при подключении сайтов к удаленным узлам. Для доступа к ресурсам на удаленных узлах используйте пользовательский домен.",
"regionSelectorInfo": "Выбор региона помогает нам обеспечить лучшее качество обслуживания для вашего расположения. Вам необязательно находиться в том же регионе, что и ваш сервер.", "regionSelectorInfo": "Выбор региона помогает нам обеспечить лучшее качество обслуживания для вашего расположения. Вам необязательно находиться в том же регионе, что и ваш сервер.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Адрес источника", "sourceAddress": "Адрес источника",
"destinationAddress": "Адрес назначения", "destinationAddress": "Адрес назначения",
"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": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <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>", "ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.",
"certResolver": "Резольвер сертификата", "certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.", "certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата", "selectCertResolver": "Выберите резолвер сертификата",
@@ -3824,6 +3811,5 @@
"rdpUploadFailed": "Ошибка загрузки", "rdpUploadFailed": "Ошибка загрузки",
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode", "rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
"sessionToolbarShow": "Показать панель инструментов", "sessionToolbarShow": "Показать панель инструментов",
"sessionToolbarHide": "Скрыть панель инструментов", "sessionToolbarHide": "Скрыть панель инструментов"
"actionUpdateSiteApprovals": "Обновить утверждения сайта"
} }
+4 -18
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": "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.", "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.",
"accessToken": "Erişim Jetonu", "accessToken": "Erişim Jetonu",
"usageExamples": "Kullanım Örnekleri", "usageExamples": "Kullanım Örnekleri",
"tokenId": "Jeton ID", "tokenId": "Jeton ID",
@@ -196,14 +196,8 @@
"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.",
@@ -1517,10 +1511,6 @@
"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…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "Alt Alan: {subdomain}", "domainPickerSubdomain": "Alt Alan: {subdomain}",
"domainPickerNamespace": "Ad Alanı: {namespace}", "domainPickerNamespace": "Ad Alanı: {namespace}",
"domainPickerShowMore": "Daha Fazla Göster", "domainPickerShowMore": "Daha Fazla Göster",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Bölge Seç", "regionSelectorTitle": "Bölge Seç",
"domainPickerRemoteExitNodeWarning": "Belirtilen alan adları, siteler uzak çıkış düğümlerine bağlandığında desteklenmez. Kaynakların uzak düğümlerde kullanılabilir olması için özel bir alan adı kullanın.", "domainPickerRemoteExitNodeWarning": "Belirtilen alan adları, siteler uzak çıkış düğümlerine bağlandığında desteklenmez. Kaynakların uzak düğümlerde kullanılabilir olması için özel bir alan adı kullanın.",
"regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.", "regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.",
@@ -3098,8 +3085,8 @@
"sourceAddress": "Kaynak Adresi", "sourceAddress": "Kaynak Adresi",
"destinationAddress": "Hedef Adresi", "destinationAddress": "Hedef Adresi",
"duration": "Süre", "duration": "Süre",
"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": "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>.",
"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": "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>.",
"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",
@@ -3824,6 +3811,5 @@
"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"
} }
+4 -18
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "确认删除共享链接", "shareDeleteConfirm": "确认删除共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?", "shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。", "shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"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.", "shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
"accessToken": "访问令牌", "accessToken": "访问令牌",
"usageExamples": "用法示例", "usageExamples": "用法示例",
"tokenId": "令牌 ID", "tokenId": "令牌 ID",
@@ -196,14 +196,8 @@
"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": "任何具有此链接的人都可以访问该资源。小心地分享它。",
@@ -1517,10 +1511,6 @@
"actionListInvitations": "邀请列表", "actionListInvitations": "邀请列表",
"actionExportLogs": "导出日志", "actionExportLogs": "导出日志",
"actionViewLogs": "查看日志", "actionViewLogs": "查看日志",
"actionCreateSiteProvisioningKey": "创建站点预配密钥",
"actionListSiteProvisioningKeys": "列出站点预配密钥",
"actionUpdateSiteProvisioningKey": "更新站点预配密钥",
"actionDeleteSiteProvisioningKey": "删除站点预配密钥",
"noneSelected": "未选择", "noneSelected": "未选择",
"orgNotFound2": "未找到组织。", "orgNotFound2": "未找到组织。",
"search": "搜索…", "search": "搜索…",
@@ -1995,9 +1985,6 @@
"domainPickerSubdomain": "子域:{subdomain}", "domainPickerSubdomain": "子域:{subdomain}",
"domainPickerNamespace": "命名空间:{namespace}", "domainPickerNamespace": "命名空间:{namespace}",
"domainPickerShowMore": "显示更多", "domainPickerShowMore": "显示更多",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "选择区域", "regionSelectorTitle": "选择区域",
"domainPickerRemoteExitNodeWarning": "当站点连接到远程退出节点时不支持所提供的域。为了资源可在远程节点上使用,请使用自定义域名。", "domainPickerRemoteExitNodeWarning": "当站点连接到远程退出节点时不支持所提供的域。为了资源可在远程节点上使用,请使用自定义域名。",
"regionSelectorInfo": "选择区域以帮助提升您所在地的性能。您不必与服务器在相同的区域。", "regionSelectorInfo": "选择区域以帮助提升您所在地的性能。您不必与服务器在相同的区域。",
@@ -3098,8 +3085,8 @@
"sourceAddress": "源地址", "sourceAddress": "源地址",
"destinationAddress": "目的地址", "destinationAddress": "目的地址",
"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": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<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>", "ossEnterpriseEditionRequired": "需要 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 才能使用此功能。 此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。 <bookADemoLink>预订演示或POC 试用</bookADemoLink>",
"certResolver": "证书解决器", "certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。", "certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析", "selectCertResolver": "选择证书解析",
@@ -3824,6 +3811,5 @@
"rdpUploadFailed": "上传失败", "rdpUploadFailed": "上传失败",
"rdpUnicodeKeyboardMode": "Unicode 键盘模式", "rdpUnicodeKeyboardMode": "Unicode 键盘模式",
"sessionToolbarShow": "显示工具栏", "sessionToolbarShow": "显示工具栏",
"sessionToolbarHide": "隐藏工具栏", "sessionToolbarHide": "隐藏工具栏"
"actionUpdateSiteApprovals": "更新站点审批"
} }
-5
View File
@@ -107,7 +107,6 @@ 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"),
@@ -906,16 +905,12 @@ 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,7 +118,6 @@ 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"),
@@ -1126,18 +1125,12 @@ 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,31 +239,6 @@ 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)
@@ -505,27 +480,6 @@ 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({
+18 -49
View File
@@ -8,42 +8,26 @@ 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}:${tzOffsetMinutes}`; return `statusHistory:${entityType}:${entityId}:${days}`;
}
// 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( const cacheKey = statusHistoryCacheKey(entityType, entityId, days);
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 local midnight (UTC when tzOffsetMinutes is 0) so the query // Anchor to UTC midnight so the query window aligns with stable calendar days
// window aligns with stable calendar days for the requesting client const utcToday = new Date();
const todayMidnightSec = localMidnightSec(tzOffsetMinutes); utcToday.setUTCHours(0, 0, 0, 0);
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
@@ -79,8 +63,7 @@ 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 =
@@ -116,19 +99,11 @@ 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)
}) })
); );
@@ -158,15 +133,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 local midnight (UTC when tzOffsetMinutes is // Anchor bucket boundaries to UTC midnight so dates are stable calendar days
// 0) so dates are stable calendar days for the requesting client and // and don't drift as the cache expires and is recomputed
// don't drift as the cache expires and is recomputed const utcToday = new Date();
const todayMidnightSec = localMidnightSec(tzOffsetMinutes); utcToday.setUTCHours(0, 0, 0, 0);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
const buckets: StatusHistoryDayBucket[] = []; const buckets: StatusHistoryDayBucket[] = [];
let totalDowntime = 0; let totalDowntime = 0;
@@ -262,13 +237,7 @@ export function computeBuckets(
) )
: 100; : 100;
// Shift by the client's offset before formatting so the label reflects const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
// 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,7 +2,6 @@ import {
db, db,
Org, Org,
orgs, orgs,
resourceAccessToken,
resources, resources,
siteResources, siteResources,
sites, sites,
@@ -84,15 +83,6 @@ 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,4 +17,3 @@ export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess"; export * from "./verifyApiKeyIdpAccess";
export * from "./verifyApiKeyDomainAccess"; export * from "./verifyApiKeyDomainAccess";
export * from "./verifyApiKeyResourcePolicyAccess"; export * from "./verifyApiKeyResourcePolicyAccess";
export * from "./verifyApiKeySiteProvisioningKeyAccess";
@@ -1,111 +0,0 @@
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,7 +18,6 @@ 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,14 +55,9 @@ export async function getHealthCheckStatusHistory(
const entityType = "health_check"; const entityType = "health_check";
const entityId = parsedParams.data.healthCheckId; const entityId = parsedParams.data.healthCheckId;
const { days, tzOffsetMinutes } = parsedQuery.data; const { days } = parsedQuery.data;
const data = await getCachedStatusHistory( const data = await getCachedStatusHistory(entityType, entityId, days);
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, { return response<StatusHistoryResponse>(res, {
data, data,
+2 -317
View File
@@ -58,8 +58,7 @@ 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";
@@ -82,16 +81,11 @@ 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";
@@ -184,73 +178,6 @@ 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[])
@@ -1902,23 +1829,8 @@ 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: { data: result,
...result,
userData
},
success: true, success: true,
error: false, error: false,
message: result.valid message: result.valid
@@ -1938,233 +1850,6 @@ 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,7 +16,6 @@ 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,
@@ -25,7 +24,6 @@ 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";
@@ -217,45 +215,3 @@ 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,8 +26,6 @@ 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()
@@ -36,17 +34,10 @@ 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 maxBatchSize: z.union([
.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)
}) })
@@ -66,49 +57,6 @@ 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,46 +13,23 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import {
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,7 +11,11 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import {
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";
@@ -21,8 +25,6 @@ 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()
@@ -43,55 +45,11 @@ 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: siteProvisioningKeys.siteProvisioningKeyId, siteProvisioningKeyId:
siteProvisioningKeys.siteProvisioningKeyId,
orgId: siteProvisioningKeyOrg.orgId, orgId: siteProvisioningKeyOrg.orgId,
lastChars: siteProvisioningKeys.lastChars, lastChars: siteProvisioningKeys.lastChars,
createdAt: siteProvisioningKeys.createdAt, createdAt: siteProvisioningKeys.createdAt,
@@ -13,7 +13,11 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import {
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";
@@ -21,8 +25,6 @@ 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(),
@@ -36,13 +38,7 @@ 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()
}) })
@@ -54,8 +50,7 @@ const bodySchema = z
) { ) {
ctx.addIssue({ ctx.addIssue({
code: "custom", code: "custom",
message: message: "Provide maxBatchSize and/or validUntil and/or approveNewSites",
"Provide maxBatchSize and/or validUntil and/or approveNewSites",
path: ["maxBatchSize"] path: ["maxBatchSize"]
}); });
} }
@@ -74,48 +69,6 @@ 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,3 +1,4 @@
import { hash } from "@node-rs/argon2";
import { import {
generateId, generateId,
generateIdFromEntropySize, generateIdFromEntropySize,
@@ -7,18 +8,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 { and, eq } from "drizzle-orm"; import { 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";
@@ -27,9 +28,7 @@ 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({
@@ -102,14 +101,7 @@ export async function generateAccessToken(
} }
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
const { const { validForSeconds, title, path, description } = parsedBody.data;
validForSeconds,
title,
path,
description,
persistSession,
userId
} = parsedBody.data;
const [resource] = await db const [resource] = await db
.select() .select()
@@ -120,28 +112,6 @@ 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
@@ -163,27 +133,23 @@ 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();
+2 -10
View File
@@ -6,13 +6,12 @@ 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, gt } from "drizzle-orm"; import { sql, eq, or, inArray, and, count, isNull, lt, 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";
@@ -56,16 +55,11 @@ 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,
@@ -81,7 +75,6 @@ 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(
@@ -104,7 +97,6 @@ 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(
+10 -15
View File
@@ -16,26 +16,18 @@ 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 with missing or invalid session. IP: ${req.ip}.` `Log out failed because missing or invalid session. IP: ${req.ip}.`
); );
} }
return next(
return response<null>(res, { createHttpError(
data: null, HttpCode.BAD_REQUEST,
success: true, "You must be logged in to sign out"
error: false, )
message: "Logged out successfully", );
status: HttpCode.OK
});
} }
try { try {
@@ -45,6 +37,9 @@ 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=${encodeURIComponent(email)}&token=${token}`; const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
if (!config.getRawConfig().email) { if (!config.getRawConfig().email) {
logger.info( logger.info(
+40 -273
View File
@@ -1,9 +1,4 @@
import { import { validateResourceSessionToken } from "@server/auth/sessions/resource";
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,
@@ -18,7 +13,6 @@ import {
LoginPage, LoginPage,
Org, Org,
Resource, Resource,
ResourceAccessToken,
ResourceHeaderAuth, ResourceHeaderAuth,
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
ResourcePassword, ResourcePassword,
@@ -27,10 +21,7 @@ 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";
@@ -50,13 +41,11 @@ 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(),
@@ -361,15 +350,22 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
return await allowAccessToken( logRequestAudit(
res, {
resource, action: true,
tokenItem, reason: 102, // valid access token
sessions, resourceId: resource.resourceId,
dontStripSession, orgId: resource.orgId,
parsedBody.data, location: ipCC,
ipCC apiKey: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
); );
return allowed(res, undefined, dontStripSession);
} }
} }
@@ -405,15 +401,22 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
return await allowAccessToken( logRequestAudit(
res, {
resource, action: true,
tokenItem, reason: 102, // valid access token
sessions, resourceId: resource.resourceId,
dontStripSession, orgId: resource.orgId,
parsedBody.data, location: ipCC,
ipCC apiKey: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
); );
return allowed(res, undefined, dontStripSession);
} }
} }
@@ -663,37 +666,22 @@ export async function verifyResourceSession(
"Resource allowed because access token session is valid" "Resource allowed because access token session is valid"
); );
const [tokenItem] = await db logRequestAudit(
.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,
accessTokenId: resourceSession.accessTokenId, apiKey: {
tokenTitle: tokenItem?.title ?? null, name: null,
userData apiKeyId: resourceSession.accessTokenId
}
}, },
parsedBody.data parsedBody.data
); );
return allowed(res, userData, dontStripSession); return allowed(res, undefined, dontStripSession);
} }
if (resourceSession.userSessionId && sso) { if (resourceSession.userSessionId && sso) {
@@ -904,227 +892,6 @@ 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,6 +809,16 @@ 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, localEndpoints, chainId } = message.data; const { publicKey, port, chainId } = message.data;
const siteId = newt.siteId; const siteId = newt.siteId;
// Get the current site data // Get the current site data
@@ -69,10 +69,7 @@ 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,7 +30,6 @@ 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;
@@ -207,9 +206,6 @@ 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,
@@ -1,74 +0,0 @@
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 newt // update the peer on the exit node
await newtUpdatePeer(siteId, client.pubKey, { await newtUpdatePeer(siteId, client.pubKey, {
endpoint: "" // this removes the endpoint so the newt knows to relay endpoint: "" // this removes the endpoint so the exit node knows to relay
}); });
return { return {
@@ -3,15 +3,24 @@ 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 { clients, clientSitesAssociationsCache, Olm, sites } from "@server/db"; import {
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 { generateAliasConfig } from "@server/lib/ip"; import {
generateAliasConfig,
} from "@server/lib/ip";
import { generateRemoteSubnets } from "@server/lib/ip"; import { generateRemoteSubnets } from "@server/lib/ip";
import { addPeer as newtAddPeer } from "@server/routers/newt/peers"; import {
addPeer as newtAddPeer,
} from "@server/routers/newt/peers";
export const handleOlmServerPeerAddMessage: MessageHandler = async ( export const handleOlmServerPeerAddMessage: MessageHandler = async (
context context
@@ -126,7 +135,10 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
clientSiteResourcesAssociationsCache.siteResourceId clientSiteResourcesAssociationsCache.siteResourceId
) )
) )
.innerJoin(networks, eq(siteResources.networkId, networks.networkId)) .innerJoin(
networks,
eq(siteResources.networkId, networks.networkId)
)
.innerJoin( .innerJoin(
siteNetworks, siteNetworks,
and( and(
@@ -135,7 +147,10 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
) )
) )
.where( .where(
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId) eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
); );
// Return connect message with all site configurations // Return connect message with all site configurations
@@ -146,9 +161,6 @@ 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,
@@ -158,7 +170,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,
@@ -1,95 +0,0 @@
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 newt // update the peer on the exit node
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 newt endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node
}); });
return { return {
-2
View File
@@ -13,5 +13,3 @@ 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,7 +18,6 @@ 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
@@ -45,7 +44,6 @@ 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,
+1 -32
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, users } from "@server/db"; import { Resource, resources } 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,42 +156,11 @@ 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
}); });
@@ -0,0 +1,133 @@
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")
);
}
}
+2 -7
View File
@@ -42,14 +42,9 @@ export async function getResourceStatusHistory(
const entityType = "resource"; const entityType = "resource";
const entityId = parsedParams.data.resourceId; const entityId = parsedParams.data.resourceId;
const { days, tzOffsetMinutes } = parsedQuery.data; const { days } = parsedQuery.data;
const data = await getCachedStatusHistory( const data = await getCachedStatusHistory(entityType, entityId, days);
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, { return response<StatusHistoryResponse>(res, {
data, data,
+1
View File
@@ -17,6 +17,7 @@ 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";
+2 -7
View File
@@ -42,14 +42,9 @@ export async function getSiteStatusHistory(
const entityType = "site"; const entityType = "site";
const entityId = parsedParams.data.siteId; const entityId = parsedParams.data.siteId;
const { days, tzOffsetMinutes } = parsedQuery.data; const { days } = parsedQuery.data;
const data = await getCachedStatusHistory( const data = await getCachedStatusHistory(entityType, entityId, days);
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=${encodeURIComponent(existingUser[0].email!)}&token=${token}`; const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${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=${encodeURIComponent(email)}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${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=${encodeURIComponent(email)}`; const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
if (doEmail) { if (doEmail) {
await sendEmail( await sendEmail(
+1 -5
View File
@@ -20,9 +20,7 @@ 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";
@@ -34,8 +32,6 @@ 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,39 +248,6 @@ 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
+4 -99
View File
@@ -52,6 +52,7 @@ 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,
@@ -62,26 +63,11 @@ 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: CreatedShareLink) => void; onCreated?: (result: ShareLinkRow) => void;
}; };
export default function CreateShareLinkForm({ export default function CreateShareLinkForm({
@@ -99,8 +85,6 @@ 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();
@@ -191,9 +175,7 @@ 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) => {
@@ -223,11 +205,7 @@ 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
}); });
} }
@@ -242,9 +220,6 @@ 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();
}} }}
> >
@@ -369,48 +344,6 @@ 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>
@@ -504,34 +437,6 @@ 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>
+8 -20
View File
@@ -93,20 +93,14 @@ 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=${inviteRedirect}&email=${encodeURIComponent(email)}` ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/signup?redirect=${inviteRedirect}`; : `/auth/signup?redirect=/invite?token=${tokenParam}`;
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=${inviteRedirect}&user=${encodeURIComponent(email)}` ? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
: `/auth/login?redirect=${inviteRedirect}`; : `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl); router.push(redirectUrl);
} else { } else {
setLoading(false); setLoading(false);
@@ -118,23 +112,17 @@ 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=${inviteRedirect}&user=${encodeURIComponent(email)}` ? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
: `/auth/login?redirect=${inviteRedirect}`; : `/auth/login?redirect=/invite?token=${tokenParam}`;
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=${inviteRedirect}&email=${encodeURIComponent(email)}` ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/signup?redirect=${inviteRedirect}`; : `/auth/signup?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl); router.push(redirectUrl);
} }
+9 -12
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 { useState } from "react"; import { createApiClient } from "@app/lib/api";
import { logoutProxy } from "@app/actions/server"; import { useEnvContext } from "@app/hooks/useEnvContext";
type OrgPolicyRequiredProps = { type OrgPolicyRequiredProps = {
orgId: string; orgId: string;
@@ -40,23 +40,21 @@ 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;
async function reauthenticate() { function reauthenticate() {
setLoading(true); api.post("/auth/logout")
try { .catch(() => {})
await logoutProxy(); .then(() => {
} 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) {
@@ -78,7 +76,6 @@ 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,13 +143,6 @@ 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,7 +35,6 @@ 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;
@@ -45,10 +44,6 @@ 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 = {
@@ -160,41 +155,6 @@ 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
@@ -1,106 +0,0 @@
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>
);
}
+5 -12
View File
@@ -650,13 +650,9 @@ 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;
} }
}), }),
@@ -671,13 +667,11 @@ 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}`, {
`/resource/${resourceId}/status-history?days=${days}&tzOffsetMinutes=${tzOffsetMinutes}`, signal
{ signal } });
);
return res.data.data; return res.data.data;
} }
}), }),
@@ -699,11 +693,10 @@ 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}&tzOffsetMinutes=${tzOffsetMinutes}`, `/org/${orgId}/health-check/${healthCheckId}/status-history?days=${days}`,
{ signal } { signal }
); );
return res.data.data; return res.data.data;