Compare commits

...

170 Commits

Author SHA1 Message Date
Fred KISSIE e7098963d6 ♻️ Code Refactoring 2026-07-30 17:56:15 +01:00
Fred KISSIE f015fb592b ♻️ Prioritize variant in place of idp.type if available 2026-07-30 17:55:58 +01:00
Fred KISSIE c099167905 Merge branch 'dev' into feat/show-idp-icon-in-smart-login-form 2026-07-30 17:37:47 +01:00
Owen Schwartz 95ee56217c Merge pull request #3504 from fosrl/crowdin_dev
New Crowdin updates
2026-07-29 21:25:59 -04:00
Owen Schwartz 7625cc208d New translations en-us.json (French)
[ci skip]
2026-07-29 18:14:07 -04:00
Owen Schwartz f2dfd939bc New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-29 18:14:05 -04:00
Owen Schwartz 699bc8ddb4 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-29 18:14:03 -04:00
Owen Schwartz e658e007e1 New translations en-us.json (Turkish)
[ci skip]
2026-07-29 18:14:01 -04:00
Owen Schwartz 3103265450 New translations en-us.json (Russian)
[ci skip]
2026-07-29 18:13:59 -04:00
Owen Schwartz 97789d9e2e New translations en-us.json (Portuguese)
[ci skip]
2026-07-29 18:13:57 -04:00
Owen Schwartz a1aa3d96d4 New translations en-us.json (Polish)
[ci skip]
2026-07-29 18:13:56 -04:00
Owen Schwartz ae39bc0ecd New translations en-us.json (Dutch)
[ci skip]
2026-07-29 18:13:54 -04:00
Owen Schwartz e4d81aa610 New translations en-us.json (Korean)
[ci skip]
2026-07-29 18:13:52 -04:00
Owen Schwartz f59b524266 New translations en-us.json (Italian)
[ci skip]
2026-07-29 18:13:50 -04:00
Owen Schwartz 58c499acae New translations en-us.json (German)
[ci skip]
2026-07-29 18:13:48 -04:00
Owen Schwartz f22301a1eb New translations en-us.json (Danish)
[ci skip]
2026-07-29 18:13:46 -04:00
Owen Schwartz d6264fb39a New translations en-us.json (Czech)
[ci skip]
2026-07-29 18:13:44 -04:00
Owen Schwartz 08b5a9b34b New translations en-us.json (Bulgarian)
[ci skip]
2026-07-29 18:13:42 -04:00
Owen Schwartz 263976bf41 New translations en-us.json (Spanish)
[ci skip]
2026-07-29 18:13:40 -04:00
Owen 1b89c328de Merge branch 'main' into dev 2026-07-29 17:54:59 -04:00
miloschwartz e7f147d4ca fix site status stuck loading sometimes 2026-07-29 17:52:55 -04:00
miloschwartz 41dbc7d5ed remove time logs 2026-07-29 17:45:19 -04:00
miloschwartz 9f297317f2 bump version 2026-07-29 17:33:49 -04:00
Fred KISSIE e0a8721207 🚧 wip: idp type icon in last used idp 2026-07-29 22:29:56 +01:00
Owen Schwartz a69b310109 Merge pull request #3460 from shubhamsinnh/fix/idn-punycode-domain-validation
Fix Punycode domain validation
2026-07-29 17:29:25 -04:00
miloschwartz 864a6a39cd Merge branch 'main' into dev 2026-07-29 17:27:22 -04:00
Milo Schwartz 1882d4df88 Merge pull request #3469 from fosrl/refactor/batch-status-requests
refactor: batch status histories
2026-07-29 17:27:11 -04:00
miloschwartz ef3f71e9b5 Merge branch 'main' into dev 2026-07-29 17:09:54 -04:00
Milo Schwartz 9981c4c035 Merge pull request #3410 from Adityakk9031/#3408
fix: redirect to /auth/initial-setup after hitting auth rate limit (#…
2026-07-29 17:09:40 -04:00
Fred KISSIE bc267b7107 ♻️ refactor 2026-07-29 19:11:16 +01:00
Fred KISSIE 5cbb767e5f Set default certificates for private resources 2026-07-29 19:11:03 +01:00
Fred KISSIE ac79621cae ♻️ Adjust the polling interval to 30s for valid certs 2026-07-29 19:05:33 +01:00
miloschwartz f47c94d05b check idp org ownership on save policy closes #3290 2026-07-29 09:38:15 -04:00
Fred KISSIE 10773432bb ♻️ Batch certificate queries 2026-07-28 20:59:42 +01:00
Owen 9a9ae649ef Add default to path
Fix #3484
2026-07-27 10:02:32 -04:00
Owen aa6dc67015 Pull the version on the info page and use api 2026-07-27 10:01:12 -04:00
Fred KISSIE 49854f31a5 🚧 WIP: refactor useCertificate to use tanstack qeury 2026-07-24 21:03:52 +01:00
Fred KISSIE 762c79511b 🚧 Add batchedCertificate queries 2026-07-24 20:59:29 +01:00
Fred KISSIE 6e29ebd649 ♻️ refactor getbatchedcertificate 2026-07-24 20:58:47 +01:00
Fred KISSIE e128b8c282 get batched certificates endpoint 2026-07-24 19:07:37 +01:00
Fred KISSIE 832e382888 🚧 WIP: batch certificates 2026-07-23 22:00:31 +01:00
Owen Schwartz 71f45b8a80 Merge pull request #3479 from fosrl/dev
1.21.0-s.1
2026-07-22 17:27:34 -04:00
Owen ecf008a8d9 Improve cert retreival and new newt combined certs 2026-07-22 17:22:51 -04:00
Fred KISSIE b3880e5c02 ♻️ refactor 2026-07-22 21:34:13 +01:00
Fred KISSIE 58004a8ec9 ♻️ little refactor 2026-07-22 21:25:31 +01:00
Fred KISSIE b0ff7d2707 ♻️ batch healthcheck status history 2026-07-22 21:06:40 +01:00
Fred KISSIE 262f7bd090 resource status histories 2026-07-22 20:57:32 +01:00
Fred KISSIE b1558b09b1 ♻️ refactor imports 2026-07-22 19:33:00 +01:00
Fred KISSIE a33de8268b 🐛 Get last known events working with sqlite 2026-07-22 19:32:53 +01:00
Fred KISSIE 9867d3c876 ♻️ const instead of let 2026-07-21 20:31:57 +01:00
Fred KISSIE 70bddba55b Make batched status history work 2026-07-21 20:29:16 +01:00
Fred KISSIE 23181f4019 🚧 WIP: batched site status histories 2026-07-21 19:57:09 +01:00
Owen Schwartz 19c1c2042b Merge pull request #3468 from fosrl/fix/labels-dropdown-flicker
fix labels dropdown flicker if no changes applied
2026-07-20 20:28:37 -04:00
Fred KISSIE 9cc3190e3a 🚧 WIP: batched status 2026-07-20 20:37:52 +01:00
Fred KISSIE 4c873e7c48 Merge branch 'dev' into refactor/batch-status-requests 2026-07-20 18:38:00 +01:00
Fred KISSIE 56fcb80b23 💄 fix labels dropdown flicker if no changes applied 2026-07-20 17:36:45 +01:00
Owen Schwartz 41c68148a9 Merge pull request #3467 from fosrl/dev
1.21.0
2026-07-20 11:46:48 -04:00
Owen Schwartz 5d38059b95 Merge pull request #3466 from fosrl/api-improvements
api improvements
2026-07-20 11:44:25 -04:00
Owen df0198df9c Update internal -> private 2026-07-20 11:44:07 -04:00
Owen 26713b53f6 Fix #3462 2026-07-20 11:44:07 -04:00
Owen 2bc6b28978 Unify put vs post 2026-07-20 11:44:07 -04:00
Owen f1ed4da8a4 Move to legacy 2026-07-20 11:44:07 -04:00
Owen adeefb9dbd Add missing endpoints to api 2026-07-20 11:44:05 -04:00
Owen bf1cc705a5 Update tags 2026-07-20 11:43:10 -04:00
Owen 0b82dae01e More route cleanup 2026-07-20 11:43:09 -04:00
Owen 75afe6ece2 Order more api routes 2026-07-20 11:43:09 -04:00
Owen 70e2fe1e4e Just add ordering 2026-07-20 11:43:06 -04:00
Owen 17e03457e1 Recategorize 2026-07-20 11:42:37 -04:00
Owen 4d8cb7e231 Add aliaes for public/private resources 2026-07-20 11:42:35 -04:00
Owen 9561d23f1e Resovle endcoding issue 2026-07-19 14:41:07 -04:00
Shubham Singh 0275f44056 Fix Punycode domain validation 2026-07-19 13:45:44 +05:30
Owen a2e1c7b751 Go to domains if no domains 2026-07-18 16:30:29 -04:00
Owen 9e2ec72ced Fix missing resource id in cache 2026-07-18 16:15:30 -04:00
Owen 02fe1f3abd Claify that this is only for the cloud 2026-07-18 11:53:56 -04:00
Milo Schwartz c30fe5b574 Merge pull request #3457 from fosrl/dev
1.21.0
2026-07-17 17:17:35 -04:00
miloschwartz a0b9ea76a3 add badger to migration 2026-07-17 16:22:15 -04:00
miloschwartz eb3a3eac98 add 1.21 migrations 2026-07-17 15:58:13 -04:00
Owen 02d2e09709 Merge branch 'local-connection' into dev 2026-07-17 14:07:30 -04:00
miloschwartz 6e8283957c update remote node routes to support access token updates 2026-07-17 14:06:35 -04:00
miloschwartz bb9b94a983 add persistent session and users to access tokens 2026-07-17 13:39:09 -04:00
Owen 36f807ddbc Merge branch 'dev' into local-connection 2026-07-17 11:15:51 -04:00
miloschwartz cb31546c6b fix spelling in installer 2026-07-17 10:15:11 -04:00
miloschwartz 2d48adc9f9 use proxy for logout on org auth page session expire 2026-07-16 22:06:59 -04:00
Owen Schwartz 113b7a0b84 Merge pull request #3453 from fosrl/dev
1.20.0-s.3
2026-07-16 21:39:00 -04:00
Owen Schwartz ca279ee3e0 Merge pull request #3452 from fosrl/crowdin_dev
New Crowdin updates
2026-07-16 21:37:08 -04:00
Owen Schwartz a5808200f0 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-16 21:34:54 -04:00
Owen Schwartz aa9de4d2ac New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-16 21:34:52 -04:00
Owen Schwartz 50821e972d New translations en-us.json (Turkish)
[ci skip]
2026-07-16 21:34:50 -04:00
Owen Schwartz e5b9dfee66 New translations en-us.json (Russian)
[ci skip]
2026-07-16 21:34:48 -04:00
Owen Schwartz 8e09070bc1 New translations en-us.json (Portuguese)
[ci skip]
2026-07-16 21:34:46 -04:00
Owen Schwartz 3d9c413bbb New translations en-us.json (Polish)
[ci skip]
2026-07-16 21:34:45 -04:00
Owen Schwartz 86670c1d60 New translations en-us.json (Dutch)
[ci skip]
2026-07-16 21:34:43 -04:00
Owen Schwartz 5f2ee77a73 New translations en-us.json (Korean)
[ci skip]
2026-07-16 21:34:41 -04:00
Owen Schwartz 714e3a8fb1 New translations en-us.json (Italian)
[ci skip]
2026-07-16 21:34:40 -04:00
Owen Schwartz 672555f051 New translations en-us.json (German)
[ci skip]
2026-07-16 21:34:38 -04:00
Owen Schwartz 7853f2b096 New translations en-us.json (Danish)
[ci skip]
2026-07-16 21:34:36 -04:00
Owen Schwartz 61a7dda29a New translations en-us.json (Czech)
[ci skip]
2026-07-16 21:34:35 -04:00
Owen Schwartz f9bcb25eaa New translations en-us.json (Bulgarian)
[ci skip]
2026-07-16 21:34:33 -04:00
Owen Schwartz 6df804ab32 New translations en-us.json (Spanish)
[ci skip]
2026-07-16 21:34:31 -04:00
Owen Schwartz 9e50735800 New translations en-us.json (French)
[ci skip]
2026-07-16 21:34:29 -04:00
miloschwartz a30119f5e8 encode email in url param 2026-07-16 21:28:36 -04:00
miloschwartz 3c9f0946d2 add site provisioning keys to integration api 2026-07-16 17:56:31 -04:00
Owen 21861a6dcd Merge branch 'local-connection' of github.com:fosrl/pangolin into local-connection 2026-07-16 17:25:58 -04:00
Owen 6135f2b727 Local endpoints from the get config 2026-07-16 17:25:51 -04:00
Owen 3a616cc804 Add handlers 2026-07-16 17:25:51 -04:00
Owen d5eb67a8fc Send localEndpoints on sites to olm 2026-07-16 17:25:51 -04:00
Owen 3ac7ef23ae Save the local endpoints 2026-07-16 17:25:51 -04:00
Owen 08c5ec2be7 Add new column 2026-07-16 17:25:51 -04:00
Owen a88b79e066 Add timezone offset to fix bad display 2026-07-16 16:09:35 -04:00
Owen a48ef77ee5 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-07-16 15:49:52 -04:00
Owen 55f3807491 Enforce uniqueness of aliases in blueprints 2026-07-16 15:10:38 -04:00
Owen dbfb8e83e8 Local endpoints from the get config 2026-07-16 14:43:45 -04:00
Owen 515afc14a5 Add handlers 2026-07-16 11:23:53 -04:00
Owen 903e8c0fa1 Send localEndpoints on sites to olm 2026-07-16 10:47:24 -04:00
Owen 7f9c760380 Save the local endpoints 2026-07-16 09:59:11 -04:00
Owen 530a1a5350 Add new column 2026-07-16 09:59:07 -04:00
miloschwartz 3fb36c1434 add missing translation 2026-07-15 22:03:28 -04:00
Owen Schwartz 5d20956a0e Merge pull request #3450 from fosrl/dev
1.20.0-s.2
2026-07-15 20:55:40 -04:00
Owen Schwartz 29f91e8276 Merge pull request #3449 from fosrl/crowdin_dev
New Crowdin updates
2026-07-15 20:54:40 -04:00
Owen Schwartz 4d9e38d022 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-15 20:54:17 -04:00
Owen Schwartz d7d31b78f0 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-15 20:54:15 -04:00
Owen Schwartz e8ed8fc7e9 New translations en-us.json (Turkish)
[ci skip]
2026-07-15 20:54:13 -04:00
Owen Schwartz 5ee406c92e New translations en-us.json (Russian)
[ci skip]
2026-07-15 20:54:12 -04:00
Owen Schwartz 89f4f4ba6f New translations en-us.json (Portuguese)
[ci skip]
2026-07-15 20:54:10 -04:00
Owen Schwartz 4e5fd2f2b0 New translations en-us.json (French)
[ci skip]
2026-07-15 20:54:08 -04:00
Owen Schwartz 22a5743817 New translations en-us.json (Polish)
[ci skip]
2026-07-15 20:54:06 -04:00
Owen Schwartz 40e05f903a New translations en-us.json (Dutch)
[ci skip]
2026-07-15 20:54:04 -04:00
Owen Schwartz 6c1e1bc0ca New translations en-us.json (Korean)
[ci skip]
2026-07-15 20:54:02 -04:00
Owen Schwartz cebbf9ca65 New translations en-us.json (Italian)
[ci skip]
2026-07-15 20:54:00 -04:00
Owen Schwartz e6646e5833 New translations en-us.json (German)
[ci skip]
2026-07-15 20:53:59 -04:00
Owen Schwartz 00f7510195 New translations en-us.json (Danish)
[ci skip]
2026-07-15 20:53:57 -04:00
Owen Schwartz 8d1f5e1096 New translations en-us.json (Czech)
[ci skip]
2026-07-15 20:53:55 -04:00
Owen Schwartz e497fcc3b9 New translations en-us.json (Bulgarian)
[ci skip]
2026-07-15 20:53:53 -04:00
Owen Schwartz a7010b2788 New translations en-us.json (Spanish)
[ci skip]
2026-07-15 20:53:51 -04:00
Owen abf58a70ee Merge branch 'provisioning-resources' into dev 2026-07-15 20:45:39 -04:00
Owen d4c602cf91 Update the counts when deleting resources 2026-07-15 17:22:12 -04:00
miloschwartz d1b0bbb6d9 update translations 2026-07-15 16:05:53 -04:00
miloschwartz ae0be3a4bc add resources overview column to pending sites table 2026-07-12 17:53:57 -04:00
miloschwartz a8f3f71021 add reject endpoint 2026-07-12 17:48:48 -04:00
miloschwartz 933ca71c16 add approve site endpoint 2026-07-12 17:16:08 -04:00
miloschwartz 591cb9cdc1 dont use strict query params in list alises 2026-07-10 18:05:16 -04:00
miloschwartz 34b18bdb53 only show approved resources in launcher 2026-07-10 17:24:10 -04:00
miloschwartz 7d475f5e91 filter out pending resources 2026-07-10 17:18:33 -04:00
miloschwartz 39c35fa539 reorganize components nad hooks for consistency 2026-07-10 17:05:45 -04:00
Milo Schwartz bada1fdb97 Merge pull request #3394 from fosrl/feat/remember-last-idp-on-smart-login-form
feat: Remember last used idp in login forms
2026-07-10 16:34:22 -04:00
miloschwartz d9303f87c8 set last used cookie in smart login lookup 2026-07-10 16:34:02 -04:00
Owen e1bc0b7efd Make sure the enabled gets set to false 2026-07-10 15:36:05 -04:00
Owen 5ef068c8dc Set the resource from the site 2026-07-10 15:31:37 -04:00
miloschwartz 14680df160 suse tiermatrix in server component susbcribed check 2026-07-10 15:23:09 -04:00
Owen 94c01a23a9 Merge branch 'private-resource-enable' into provisioning-resources 2026-07-10 15:17:06 -04:00
Owen 609fb357bb Support enable in the blueprints 2026-07-10 15:16:50 -04:00
Owen cf9a17cc2e Add status filter 2026-07-10 15:04:28 -04:00
Owen 538b57941c Makes sure patch works
z#
2026-07-10 11:32:27 -04:00
Owen f4bee6406a Support partial updates 2026-07-10 10:11:47 -04:00
Owen 0b2693a317 Add toggle to the ui for enabled 2026-07-10 09:59:11 -04:00
Fred KISSIE 1580b7abff 🚧 wip: batch status histories 2026-07-10 07:02:22 +02:00
Owen 3be2d928f6 Filter out disabled 2026-07-09 21:42:59 -04:00
Milo Schwartz e5398d441e Merge pull request #3425 from fosrl/dev
Dev
2026-07-09 21:33:17 -04:00
miloschwartz d38f9ac2bb update readme 2026-07-09 21:32:35 -04:00
Owen 8d018fe47d Clean up 2026-07-09 21:28:48 -04:00
Fred KISSIE 34d5c9535d ♻️ delete last used IDP coookie when logging in with email/password on smart login form 2026-07-10 03:22:28 +02:00
Fred KISSIE dc60ef712f Merge branch 'dev' into feat/remember-last-idp-on-smart-login-form 2026-07-10 02:38:38 +02:00
Owen dc7eb630c3 Add missing action 2026-07-09 11:16:15 -04:00
Aditya kumar singh bb5547a157 fix: redirect to /auth/initial-setup after hitting auth rate limit (#3408) 2026-07-08 23:33:57 +05:30
Fred KISSIE 2fe85ebda2 ♻️ set last IDP to null if login with email + password 2026-07-08 03:11:12 +02:00
Fred KISSIE 3d13e9105c 🏷️ fix types 2026-07-03 22:33:58 +02:00
Fred KISSIE 289be30e6b show last used login idp in smart login form 2026-07-03 22:33:51 +02:00
Fred KISSIE a74c0c227c 🌐 text 2026-07-03 21:05:53 +02:00
Fred KISSIE 05bf77da29 ♻️ remove cache on verifySession calls as it's already wrapped in cache 2026-07-03 21:05:47 +02:00
Fred KISSIE bb4deb1ae9 remember last used idp in dashboard login form 2026-07-03 21:05:16 +02:00
234 changed files with 8104 additions and 1445 deletions
+39 -3
View File
@@ -41,7 +41,7 @@
</strong>
</p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control.
## Installation
@@ -63,11 +63,26 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
Pangolin's site connectors provide gateways into networks so you can access any networked resources. Sites use outbound tunnels and intelligent NAT traversal to make networks behind restrictive firewalls available for authorized access without public IPs or open ports. Easily deploy a site as a binary or container on any platform.
* Lightweight user-space connector runs anywhere
* Punches through any firewall
* Doesn't require open ports or a public IP
* Strict network segmentation
* WireGuard-based
* Get alerts when a device or network resource goes down
<img src="public/screenshots/sites.png" alt="Sites" width="100%" />
### Browser-based reverse proxy access
Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
Expose HTTPS web applications and connect to VNC, RDP, and SSH entirely in the browser through identity and context-aware tunneled reverse proxies. Users access resources with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
* Expose a web panel anywhere
* Access via any web browser
* Single sign-on across all resources
* HTTPS resources
* Remote desktop in the browser with VNC and RDP
* In-browser SSH terminal with privileged access management (PAM)
* PIN codes, passcodes, email OTP, geoblocking, allow-lists, and more
<img src="public/clip.gif" alt="Reverse proxy access" width="100%" />
@@ -75,14 +90,35 @@ Expose web applications through identity and context-aware tunneled reverse prox
Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
* Peer-to-peer with intelligent NAT traversal
* Hosts/IPs and port ranges
* Network ranges/CIDRs
* Friendly DNS aliases for network addresses
* Privileged access management (PAM) with SSH resources
* Private HTTPS resources only accessible on the private network
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
### Give users and roles access to resources
Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
* Bring your existing identity provider (IdP) or use Pangolin identities
* Sync users and roles from your IdP
* User- and role-based access control
* Full network audit and access logs
<img src="public/screenshots/users.png" alt="Users from identity provider with roles" width="100%" />
### Find and launch resources from a personalized home page
Give users a landing page to quickly find and open the resources they can access. Resources are grouped by site or label, searchable, and filterable, with grid or list views. Saved views capture filters, grouping, and layout as personal or organization-wide defaults.
* Single place for admins and non-admins to see accessible resources
* Create reusable views for common access patterns
<img src="public/screenshots/resource-launcher.png" alt="Resource Launcher" width="100%" />
## Download Clients
Download the Pangolin client for your platform:
+1 -1
View File
@@ -76,7 +76,7 @@ var redisFlag *bool
func main() {
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as cacheing solution. Required for HA. Not required for the Enterprise version.")
redisFlag = flag.Bool("redis", false, "Install Redis as caching solution. Required for HA. Not required for the Enterprise version.")
flag.Parse()
// 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
+39 -15
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Потвърдете изтриването на споделима връзка",
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
"shareTokenDescription": "Токен за достъп може да бъде предаден като параметър на заявка или в хедърите на заявката. По подразбиране трябва да се изпраща при всяка заявка. Ако е активирано задържане на сесията, първата заявка го заменя за сесийна бисквитка.",
"accessToken": "Достъп Токен",
"usageExamples": "Примери за използване",
"tokenId": "Токен ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Заглавие (по избор)",
"sharePathOptional": "Път (по избор)",
"sharePathDescription": "След удостоверяване, линкът ще препрати потребителите на този път.",
"shareAssociateUserOptional": "Асоцииране на потребител (по избор)",
"shareAssociateUserDescription": "Когато е настроен, заявките използващи този линк се свързват с потребителя в достъпните логове и заглавия за идентичност. Линкът се премахва, ако потребителят напусне организацията.",
"userSelect": "Изберете потребител",
"usersNotFound": "Няма намерени потребители",
"expireIn": "Изтече",
"neverExpire": "Никога не изтича",
"sharePersistSession": "Задръж сесия след първо използване",
"sharePersistSessionDescription": "Когато е активирано, първата заявка с този токен чрез параметър на заявка или хедър създава сесийна бисквитка, така че следващите заявки не се нуждаят от токена. Оставете го изключено за клиенти на API, които трябва да изпращат токена при всяка заявка.",
"shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.",
"shareSeeOnce": "Ще можете да видите този линк само веднъж. Уверете се, че го копирате.",
"shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.",
@@ -449,8 +455,14 @@
"provisioningManage": "Осигуряване",
"provisioningDescription": "Управление на ключовете за осигуряване и преглед на чаканещите сайтове за одобрение.",
"pendingSites": "Чаканещи сайтове",
"siteApproveSuccess": "Сайтът е одобрен успешно",
"siteApproveSuccess": "Сайтът и свързаните ресурси са одобрени успешно",
"siteApproveError": "Грешка при одобряването на сайта",
"siteReject": "Отказ на сайт",
"siteQuestionReject": "Сигурни ли сте, че искате да откажете този сайт?",
"siteMessageReject": "Това ще изтрие окончателно сайта и всички свързани ресурси, които все още са на изчакване.",
"siteConfirmReject": "Потвърдете отказ на сайт",
"siteRejectSuccess": "Сайтът беше успешно отхвърлен",
"siteRejectError": "Грешка при отхвърляне на сайта",
"provisioningKeys": "Ключове за осигуряване",
"searchProvisioningKeys": "Търсене на ключове за осигуряване...",
"provisioningKeysAdd": "Генериране на ключ за осигуряване",
@@ -466,8 +478,8 @@
"provisioningKeysSave": "Запазете ключа за осигуряване",
"provisioningKeysSaveDescription": "Ще можете да видите това само веднъж. Копирайте го на сигурно място.",
"provisioningKeysErrorCreate": "Грешка при създаване на ключ за осигуряване",
"provisioningKeysList": "Нов ключ за осигуряване",
"provisioningKeysMaxBatchSize": "Максимален размер на пакет",
"provisioningKeysList": "Нов ключ за разпределяне",
"provisioningKeysMaxBatchSize": "Максимален размер на пакета",
"provisioningKeysUnlimitedBatchSize": "Неограничен размер на партида (без лимит)",
"provisioningKeysMaxBatchUnlimited": "Неограничено",
"provisioningKeysMaxBatchSizeInvalid": "Въведете валиден максимален размер на партида (11,000,000).",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Никога",
"provisioningKeysEdit": "Редактиране на ключ за осигуряване",
"provisioningKeysEditDescription": "Актуализирайте максималния размер на партида и времето на изтичане за този ключ.",
"provisioningKeysApproveNewSites": "Одобрете нови сайтове",
"provisioningKeysApproveNewSites": "Одобряване на нови сайтове",
"provisioningKeysApproveNewSitesDescription": "Автоматично одобряване на сайтове, които се регистрират с този ключ.",
"provisioningKeysUpdateError": "Грешка при актуализирането на ключа за осигуряване",
"provisioningKeysUpdated": "Ключът за осигуряване е актуализиран",
@@ -922,7 +934,7 @@
"newtVersion": "Версия",
"architecture": "Архитектура",
"sites": "Сайтове",
"siteWgAnyClients": "Използвайте клиент на WireGuard, за да се свържете. Ще трябва да използвате вътрешните ресурси чрез IP адреса на връстника.",
"siteWgAnyClients": "Използвайте всеки WireGuard клиент, за да се свържете. Ще трябва да адресирате частни ресурси, използвайки IP на връстника.",
"siteWgCompatibleAllClients": "Съвместим с всички WireGuard клиенти",
"siteWgManualConfigurationRequired": "Необходима е ръчна конфигурация",
"userErrorNotAdminOrOwner": "Потребителят не е администратор или собственик",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Създаване на Организационна Потребител",
"actionUpdateOrg": "Актуализиране на организацията",
"actionRemoveInvitation": "Премахване на поканата.",
"actionRemoveUserRole": "Премахване на роля на потребител",
"actionUpdateUser": "Актуализиране на потребител",
"actionGetUser": "Получаване на потребител",
"actionGetOrgUser": "Вземете потребител на организация",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
"actionUpdateSite": "Актуализиране на сайт",
"actionApproveSite": "Одобряване на сайт",
"actionRejectSite": "Отказ на сайт",
"actionResetSiteBandwidth": "Нулиране на честотната лента на организацията",
"actionListSiteRoles": "Изброяване на позволените роли за сайта",
"actionCreateResource": "Създаване на ресурс",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Списък с покани",
"actionExportLogs": "Експортиране на дневници",
"actionViewLogs": "Преглед на дневници",
"actionCreateSiteProvisioningKey": "Създаване на ключ за предоставяне на сайт",
"actionListSiteProvisioningKeys": "Списък на ключовете за предоставяне на сайт",
"actionUpdateSiteProvisioningKey": "Актуализиране на ключ за предоставяне на сайт",
"actionDeleteSiteProvisioningKey": "Изтриване на ключ за предоставяне на сайт",
"noneSelected": "Нищо не е избрано",
"orgNotFound2": "Няма намерени организации.",
"search": "Търси…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Въведете кода от приложението за удостоверяване или един от вашите резервни кодове за еднократна употреба.",
"otpAuthSubmit": "Изпрати код",
"idpContinue": "Или продължете със",
"idpLastUsed": "Последно използвано",
"otpAuthBack": "Назад към парола",
"navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Поддомейн: {subdomain}",
"domainPickerNamespace": "Име на пространство: {namespace}",
"domainPickerShowMore": "Покажи повече",
"domainPickerNoDomainsAvailableTitle": "Няма налични домейни",
"domainPickerNoDomainsAvailableDescription": "Все още нямате конфигурирани домейни. Създайте домейн, за да продължите.",
"domainPickerNoDomainsAvailableAction": "Отидете на Домейни",
"regionSelectorTitle": "Избор на регион",
"domainPickerRemoteExitNodeWarning": "Предоставените домейни не се поддържат, когато сайтовете се свързват към отдалечени крайни възли. За да бъдат ресурсите налични на отдалечени възли, използвайте персонализиран домейн вместо това.",
"regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Публичен",
"resourcesTableClientResources": "Частен",
"resourcesTableNoProxyResourcesFound": "Не са намерени ресурсни проксита.",
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
"resourcesTableNoInternalResourcesFound": "Не са намерени частни ресурси.",
"resourcesTableDestination": "Дестинация",
"resourcesTableAlias": "Псевдоним",
"resourcesTableAliasAddress": "Адрес на псевдоним.",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Отмяна",
"editInternalResourceDialogSaveResource": "Запазване на ресурс",
"editInternalResourceDialogSuccess": "Успех",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Вътрешният ресурс успешно актуализиран",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Частният ресурс е успешно обновен",
"editInternalResourceDialogError": "Грешка",
"editInternalResourceDialogFailedToUpdateInternalResource": "Неуспешно актуализиране на вътрешен ресурс",
"editInternalResourceDialogFailedToUpdateInternalResource": "Неуспешен опит за обновяване на частен ресурс",
"editInternalResourceDialogNameRequired": "Името е задължително",
"editInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа",
"editInternalResourceDialogProxyPortMin": "Прокси портът трябва да бъде поне 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Псевдоним",
"editInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
"createInternalResourceDialogNoSitesAvailable": "Няма достъпни сайтове",
"createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един сайт на Newt с конфигурирана мрежа, за да създадете вътрешни ресурси.",
"createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един Newt сайт с конфигуриран сабнет, за да създадете частни ресурси.",
"createInternalResourceDialogClose": "Затвори",
"createInternalResourceDialogCreateClientResource": "Създаване на частен ресурс",
"createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен само за клиенти, свързани към организацията",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Отмяна",
"createInternalResourceDialogCreateResource": "Създаване на ресурс",
"createInternalResourceDialogSuccess": "Успех",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Вътрешният ресурс създаден успешно",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Частният ресурс е създаден успешно",
"createInternalResourceDialogError": "Грешка",
"createInternalResourceDialogFailedToCreateInternalResource": "Неуспешно създаване на вътрешен ресурс",
"createInternalResourceDialogFailedToCreateInternalResource": "Неуспешен опит за създаване на частен ресурс",
"createInternalResourceDialogNameRequired": "Името е задължително",
"createInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа",
"createInternalResourceDialogPleaseSelectSite": "Моля, изберете сайт",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Източен адрес",
"destinationAddress": "Адрес на дестинация",
"duration": "Продължителност",
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> за използване на тази функция. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>, за да използвате тази функция. <bookADemoLink>Резервирайте безплатна демонстрация или пробен POC, за да научите повече.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте безплатна демонстрация или пробен POC, за да научите повече.</bookADemoLink>",
"certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Качването неуспешно",
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
"sessionToolbarShow": "Показване на лентата с инструменти",
"sessionToolbarHide": "Скриване на лентата с инструменти"
"sessionToolbarHide": "Скриване на лентата с инструменти",
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
}
+38 -14
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Potvrdit odstranění sdílného odkazu",
"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.",
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v hlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.",
"shareTokenDescription": "Přístupový token může být předán jako dotazový parametr nebo v hlavičkách žádostí. Ve výchozím nastavení musí být odesílán s každou žádostí. Pokud je povolena perzistence relace, první žádost jej vymění za relaci cookie.",
"accessToken": "Přístupový token",
"usageExamples": "Příklady použití",
"tokenId": "ID tokenu",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Název (volitelné)",
"sharePathOptional": "Cesta (volitelně)",
"sharePathDescription": "Odkaz přesměruje uživatele na tuto cestu po autentikaci.",
"shareAssociateUserOptional": "Přiřadit uživatele (volitelné)",
"shareAssociateUserDescription": "Pokud je nastaveno, žádosti pomocí tohoto odkazu jsou v přístupových protokolech a hlavičkách identity přidružené k uživateli. Odkaz je odstraněn, pokud uživatel opustí organizaci.",
"userSelect": "Vyberte uživatele",
"usersNotFound": "Nebyl nalezen žádný uživatel",
"expireIn": "Platnost vyprší za",
"neverExpire": "Nikdy nevyprší",
"sharePersistSession": "Udržet relaci po prvním použití",
"sharePersistSessionDescription": "Pokud je povoleno, první žádost s tímto tokenem prostřednictvím dotazu nebo hlavičky nastaví relaci cookie, takže pozdější žádosti nepotřebují token. Nechcete-li, aby API klienti posílali token s každou žádostí, vypněte to.",
"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.",
"shareAccessHint": "Kdokoli s tímto odkazem může přistupovat ke zdroji. Sdílejte jej s rozvahou.",
@@ -449,8 +455,14 @@
"provisioningManage": "Zajištění",
"provisioningDescription": "Spravovat klíče pro nastavení a zkontrolovat čekající stránky čekající na schválení.",
"pendingSites": "Nevyřízené weby",
"siteApproveSuccess": "Web byl úspěšně schválen",
"siteApproveSuccess": "Stránka a přidružené zdroje byly úspěšně schváleny",
"siteApproveError": "Chyba při schvalování webu",
"siteReject": "Odmítnout stránku",
"siteQuestionReject": "Opravdu chcete tuto stránku odmítnout?",
"siteMessageReject": "Toto trvale odstraní stránku a všechny přidružené zdroje, které jsou stále nevyřízené.",
"siteConfirmReject": "Potvrdit odmítnutí stránky",
"siteRejectSuccess": "Stránka byla úspěšně odmítnuta",
"siteRejectError": "Chyba při odmítání stránky",
"provisioningKeys": "Poskytovací klíče",
"searchProvisioningKeys": "Hledat klíče k zajišťování...",
"provisioningKeysAdd": "Generovat zajišťovací klíč",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Uložit konfigurační klíč",
"provisioningKeysSaveDescription": "Toto můžete vidět pouze jednou. Zkopírujte ho na bezpečné místo.",
"provisioningKeysErrorCreate": "Chyba při vytváření doplňovacího klíče",
"provisioningKeysList": "Nový klíč pro poskytování informací",
"provisioningKeysList": "Nový provisioning klíč",
"provisioningKeysMaxBatchSize": "Maximální velikost dávky",
"provisioningKeysUnlimitedBatchSize": "Neomezená velikost šarže (bez omezení)",
"provisioningKeysMaxBatchUnlimited": "Bez omezení",
"provisioningKeysMaxBatchSizeInvalid": "Zadejte platnou maximální velikost šarže (11,000,000).",
"provisioningKeysValidUntil": "Platné do",
"provisioningKeysValidUntil": "Platná do",
"provisioningKeysValidUntilHint": "Ponechte prázdné, pokud vyprší platnost.",
"provisioningKeysValidUntilInvalid": "Zadejte platné datum a čas.",
"provisioningKeysNumUsed": "Časy použití",
@@ -922,7 +934,7 @@
"newtVersion": "Verze",
"architecture": "Architektura",
"sites": "Stránky",
"siteWgAnyClients": "K připojení použijte jakéhokoli klienta WireGuard. Budete muset řešit interní zdroje pomocí klientské IP adresy.",
"siteWgAnyClients": "Pro připojení použijte jakéhokoli klienta WireGuard. Budete muset adresovat privátní zdroje pomocí IP protějšku.",
"siteWgCompatibleAllClients": "Kompatibilní se všemi klienty aplikace WireGuard",
"siteWgManualConfigurationRequired": "Je vyžadována ruční konfigurace",
"userErrorNotAdminOrOwner": "Uživatel není administrátor nebo vlastník",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Vytvořit Org uživatele",
"actionUpdateOrg": "Aktualizovat organizaci",
"actionRemoveInvitation": "Odstranit pozvání",
"actionRemoveUserRole": "Odstranit uživatelskou roli",
"actionUpdateUser": "Aktualizovat uživatele",
"actionGetUser": "Získat uživatele",
"actionGetOrgUser": "Získat uživatele organizace",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Zadejte nastavovací token z konzole serveru.",
"setupTokenRequired": "Je vyžadován token nastavení",
"actionUpdateSite": "Aktualizovat stránku",
"actionApproveSite": "Schválit stránku",
"actionRejectSite": "Odmítnout stránku",
"actionResetSiteBandwidth": "Resetovat šířku pásma organizace",
"actionListSiteRoles": "Seznam povolených rolí webu",
"actionCreateResource": "Vytvořit zdroj",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Seznam pozvánek",
"actionExportLogs": "Exportovat protokoly",
"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",
"orgNotFound2": "Nebyly nalezeny žádné organizace.",
"search": "Vyhledávání…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Zadejte kód z vaší autentizační aplikace nebo jeden z vlastních záložních kódů.",
"otpAuthSubmit": "Odeslat kód",
"idpContinue": "Nebo pokračovat s",
"idpLastUsed": "Naposled použito",
"otpAuthBack": "Zpět na heslo",
"navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdoména: {subdomain}",
"domainPickerNamespace": "Jmenný prostor: {namespace}",
"domainPickerShowMore": "Zobrazit více",
"domainPickerNoDomainsAvailableTitle": "Nejsou k dispozici žádné domény",
"domainPickerNoDomainsAvailableDescription": "Zatím nemáte žádné nastavené domény. Vytvořte doménu, abyste mohli pokračovat.",
"domainPickerNoDomainsAvailableAction": "Přejít na domény",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Veřejnost",
"resourcesTableClientResources": "Soukromé",
"resourcesTableNoProxyResourcesFound": "Nebyly nalezeny žádné zdroje proxy",
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné privátní zdroje.",
"resourcesTableDestination": "Místo určení",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adresa aliasu",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Zrušit",
"editInternalResourceDialogSaveResource": "Uložit dokument",
"editInternalResourceDialogSuccess": "Úspěšně",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interní zdroj byl úspěšně aktualizován",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Privátní zdroj byl úspěšně aktualizován",
"editInternalResourceDialogError": "Chyba",
"editInternalResourceDialogFailedToUpdateInternalResource": "Aktualizace interního zdroje se nezdařila",
"editInternalResourceDialogFailedToUpdateInternalResource": "Nepodařilo se aktualizovat privátní zdroj",
"editInternalResourceDialogNameRequired": "Název je povinný",
"editInternalResourceDialogNameMaxLength": "Název musí mít méně než 255 znaků",
"editInternalResourceDialogProxyPortMin": "Port proxy serveru musí být alespoň 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
"createInternalResourceDialogNoSitesAvailable": "Nejsou k dispozici žádné weby",
"createInternalResourceDialogNoSitesAvailableDescription": "Musíte mít alespoň jeden Newt web s podsítí nakonfigurovanou pro vytvoření vnitřních zdrojů.",
"createInternalResourceDialogNoSitesAvailableDescription": "K vytvoření privátních zdrojů potřebujete mít alespoň jednu lokalitu Newt s nastavenou podsítí.",
"createInternalResourceDialogClose": "Zavřít",
"createInternalResourceDialogCreateClientResource": "Vytvořit soukromý zdroj",
"createInternalResourceDialogCreateClientResourceDescription": "Vytvořte nový zdroj, který bude přístupný pouze klientům připojeným k organizaci",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Zrušit",
"createInternalResourceDialogCreateResource": "Vytvořit zdroj",
"createInternalResourceDialogSuccess": "Úspěšně",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interní zdroj byl úspěšně vytvořen",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Privátní zdroj byl úspěšně vytvořen",
"createInternalResourceDialogError": "Chyba",
"createInternalResourceDialogFailedToCreateInternalResource": "Nepodařilo se vytvořit interní zdroj",
"createInternalResourceDialogFailedToCreateInternalResource": "Nepodařilo se vytvořit privátní zdroj",
"createInternalResourceDialogNameRequired": "Název je povinný",
"createInternalResourceDialogNameMaxLength": "Název musí mít méně než 255 znaků",
"createInternalResourceDialogPleaseSelectSite": "Vyberte prosím web",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Zdrojová adresa",
"destinationAddress": "Cílová adresa",
"duration": "Doba trvání",
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> nebo <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezervujte si demo nebo POC zkušební verzi</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> nebo <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezervujte si bezplatné demo nebo POC zkušební verzi, abyste se dozvěděli více.</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 zdarma, abyste se dozvěděli více.</bookADemoLink>",
"certResolver": "Oddělovač certifikátů",
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
"selectCertResolver": "Vyberte řešič certifikátů",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Nahrání selhalo",
"rdpUnicodeKeyboardMode": "Režim Unicode klávesnice",
"sessionToolbarShow": "Zobrazit panel nástrojů",
"sessionToolbarHide": "Skrýt panel nástrojů"
"sessionToolbarHide": "Skrýt panel nástrojů",
"actionUpdateSiteApprovals": "Aktualizovat schválení webu"
}
+37 -13
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bekræft sletning af delbar link",
"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.",
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en queryparameter eller i request-headers. Disse skal sendes fra klienten på hver forespørgsel om autentificeret adgang.",
"shareTokenDescription": "Adgangstokenet kan sendes som en forespørgselsparameter eller i anmodningsoverskrifter. Som standard skal det sendes ved hver anmodning. Hvis sessionsvedholdelse er aktiveret, byttes den første anmodning for en sessionscookie.",
"accessToken": "Adgangstoken",
"usageExamples": "Brukseksempler",
"tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (valgfrit)",
"sharePathOptional": "Sti (valgfrit)",
"sharePathDescription": "Linket vil videresende brugere til denne stien efter autentificering.",
"shareAssociateUserOptional": "Tilknyt brugeren (valgfrit)",
"shareAssociateUserDescription": "Når det er indstillet, tilskrives anmodninger ved hjælp af dette link brugeren i adgangslogfiler og identitetsoverskrifter. Linket fjernes, hvis brugeren forlader organisationen.",
"userSelect": "Vælg bruger",
"usersNotFound": "Ingen brugere fundet",
"expireIn": "Udløber om",
"neverExpire": "Udløber aldrig",
"sharePersistSession": "Bevar session efter første brug",
"sharePersistSessionDescription": "Når det er aktiveret, sætter den første anmodning med dette token via en forespørgselsparameter eller overskrift en sessionscookie, så senere anmodninger ikke behøver tokenet. Lad være med det til API-klienter, der ønsker at sende tokenet ved hver anmodning.",
"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.",
"shareAccessHint": "Alle med denne linket kan få adgang til ressourcen. Del forsiktig.",
@@ -449,8 +455,14 @@
"provisioningManage": "Provisionering",
"provisioningDescription": "Administrer provisioneringsnøgler og gennemgå ventende sites som venter på godkendelse.",
"pendingSites": "Ventende sites",
"siteApproveSuccess": "Med succes godkendelse af site",
"siteApproveSuccess": "Site og tilknyttede ressourcer godkendt med succes",
"siteApproveError": "Fejl ved godkendelse af side",
"siteReject": "Afvis Site",
"siteQuestionReject": "Er du sikker på, at du vil afvise dette site?",
"siteMessageReject": "Dette vil permanent slette sitet og enhver tilknyttet ressource, der stadig er i afventning.",
"siteConfirmReject": "Bekræft Afvis Site",
"siteRejectSuccess": "Site afvist med succes",
"siteRejectError": "Fejl ved afvisning af sitet",
"provisioningKeys": "Provisioneringsnøgler",
"searchProvisioningKeys": "Søg varer i provisioneringsnøgler...",
"provisioningKeysAdd": "Generer provisioneringsnøgle",
@@ -467,7 +479,7 @@
"provisioningKeysSaveDescription": "Du kan kun se denne én gang. Kopiér det til et sikkert sted.",
"provisioningKeysErrorCreate": "Fejl under oprettelse af provisioneringsnøgle",
"provisioningKeysList": "Ny provisioneringsnøgle",
"provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
"provisioningKeysMaxBatchSize": "Maksimum Batch Størrelse",
"provisioningKeysUnlimitedBatchSize": "Ubegrænset mengde bunt (ingen begrænsning)",
"provisioningKeysMaxBatchUnlimited": "Ubegrænset",
"provisioningKeysMaxBatchSizeInvalid": "Angiv en gyldig sjakkstørrelse (11 000.000).",
@@ -922,7 +934,7 @@
"newtVersion": "Version",
"architecture": "Arkitektur",
"sites": "Websteder",
"siteWgAnyClients": "Brug hvilken som helst WireGuard klient til at oprette forbindelse til. Du skal adressere interne ressourcer ved hjælp af peer IP.",
"siteWgAnyClients": "Brug enhver WireGuard-klient til at oprette forbindelse. Du skal adressere private ressourcer ved hjælp af peer IP.",
"siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter",
"siteWgManualConfigurationRequired": "Manuel konfiguration påkrævet",
"userErrorNotAdminOrOwner": "Bruger er ikke administrator eller ejer",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Opret organisationsbruger",
"actionUpdateOrg": "Opdater organisation",
"actionRemoveInvitation": "Fjern invitation",
"actionRemoveUserRole": "Fjern Brugers Rolle",
"actionUpdateUser": "Opdater bruger",
"actionGetUser": "Hent bruger",
"actionGetOrgUser": "Hent organisationsbruger",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Indtast opsætningstoken fra serverkonsollen.",
"setupTokenRequired": "Opsætningstoken er nødvendig",
"actionUpdateSite": "Opdater site",
"actionApproveSite": "Godkend Site",
"actionRejectSite": "Afvis Site",
"actionResetSiteBandwidth": "Nulstil organisationsbåndbredde",
"actionListSiteRoles": "Vis tilladte områderoller",
"actionCreateResource": "Opret ressource",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Vis invitationer",
"actionExportLogs": "Eksportér 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",
"orgNotFound2": "Ingen organisationer fundet.",
"search": "Søg…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Indtast koden fra autentiseringsappen din eller en af dine engangs reservekoder.",
"otpAuthSubmit": "Send kode",
"idpContinue": "Eller fortsæt med",
"idpLastUsed": "Sidst brugt",
"otpAuthBack": "Tilbage til adgangskode",
"navbar": "Navigationsmenu",
"navbarDescription": "Hovednavigasjonsmeny for applikationen",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Underdomæne: {subdomain}",
"domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mere",
"domainPickerNoDomainsAvailableTitle": "Ingen domæner tilgængelige",
"domainPickerNoDomainsAvailableDescription": "Du har endnu ikke oprettet nogen domæner. Opret et domæne for at fortsætte.",
"domainPickerNoDomainsAvailableAction": "Gå til domæner",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Offentlig",
"resourcesTableClientResources": "Privat",
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressourcer fundet.",
"resourcesTableNoInternalResourcesFound": "Ingen interne ressourcer fundet.",
"resourcesTableNoInternalResourcesFound": "Ingen private ressourcer fundet.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias adresse",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Annuller",
"editInternalResourceDialogSaveResource": "Gem ressource",
"editInternalResourceDialogSuccess": "Succes",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressource opdateret med succes",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Privat ressource opdateret succesfuldt",
"editInternalResourceDialogError": "Fejl",
"editInternalResourceDialogFailedToUpdateInternalResource": "Mislykkedes at opdatere intern ressource",
"editInternalResourceDialogFailedToUpdateInternalResource": "Mislykkedes at opdatere privat ressource",
"editInternalResourceDialogNameRequired": "Navn er påkrævet",
"editInternalResourceDialogNameMaxLength": "Navn kan ikke være længere enn 255 tegn",
"editInternalResourceDialogProxyPortMin": "Proxy-port skal være mindst 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Et valgfrit internt DNS-alias for denne ressource.",
"createInternalResourceDialogNoSitesAvailable": "Ingen tilgængelige steder",
"createInternalResourceDialogNoSitesAvailableDescription": "Du skal have mindst ét Newt-site med et konfigureret subnet for at oprette interne ressourcer.",
"createInternalResourceDialogNoSitesAvailableDescription": "Du skal have mindst ét Newt-site med et konfigureret subnet for at oprette private ressourcer.",
"createInternalResourceDialogClose": "Luk",
"createInternalResourceDialogCreateClientResource": "Opret privat ressource",
"createInternalResourceDialogCreateClientResourceDescription": "Opret en ny ressource som kun vil være tilgængelig for kunder som er forbundet til organisationen",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Annuller",
"createInternalResourceDialogCreateResource": "Opret ressource",
"createInternalResourceDialogSuccess": "Succes",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressource oprettet med succes",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Privat ressource oprettet succesfuldt",
"createInternalResourceDialogError": "Fejl",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke oprette intern ressource",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke oprette privat ressource",
"createInternalResourceDialogNameRequired": "Navn er påkrævet",
"createInternalResourceDialogNameMaxLength": "Navn kan ikke være længere enn 255 tegn",
"createInternalResourceDialogPleaseSelectSite": "Vælg venligst et site",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Kildeadresse",
"destinationAddress": "Måladresse ",
"duration": "Varighed",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>-licens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrævet for at bruge denne funktion. <bookADemoLink>Book en demo eller POC-prøveversion</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>-licens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrævet for at bruge denne funktion. <bookADemoLink>Book en gratis demo eller POC-prøveversion for at lære mere.</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 gratis demo eller POC-prøve for at lære mere.</bookADemoLink>",
"certResolver": "Certifikatløser",
"certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.",
"selectCertResolver": "Vælg certifikatløser",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Uploaden mislykkedes",
"rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
"sessionToolbarShow": "Vis værktøjslinje",
"sessionToolbarHide": "Skjul værktøjslinje"
"sessionToolbarHide": "Skjul værktøjslinje",
"actionUpdateSiteApprovals": "Opdater godkendelser på site"
}
+38 -14
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Löschung des Freigabelinks bestätigen",
"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.",
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.",
"shareTokenDescription": "Der Zugriffstoken kann als Abfrageparameter oder in Anforderungsheadern übergeben werden. Standardmäßig muss er bei jeder Anforderung gesendet werden. Wenn die Sitzungsbeständigkeit aktiviert ist, wird die erste Anfrage gegen ein Sitzungscookie ausgetauscht.",
"accessToken": "Zugriffstoken",
"usageExamples": "Nutzungsbeispiele",
"tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (optional)",
"sharePathOptional": "Pfad (optional)",
"sharePathDescription": "Der Link leitet Benutzer nach der Authentifizierung zu diesem Pfad weiter.",
"shareAssociateUserOptional": "Benutzer zuweisen (optional)",
"shareAssociateUserDescription": "Wenn gesetzt, werden Anfragen mit diesem Link in Zugriffsprotokollen und Identitätsheadern dem Benutzer zugeordnet. Der Link wird entfernt, wenn der Benutzer die Organisation verlässt.",
"userSelect": "Benutzer auswählen",
"usersNotFound": "Keine Benutzer gefunden",
"expireIn": "Läuft ab in",
"neverExpire": "Läuft nie ab",
"sharePersistSession": "Sitzung nach erster Nutzung beibehalten",
"sharePersistSessionDescription": "Wenn aktiviert, setzt die erste Anfrage mit diesem Token über einen Abfrageparameter oder einen Header ein Sitzungscookie, sodass spätere Anfragen das Token nicht benötigen. Deaktivieren Sie dies für API-Clients, die das Token bei jeder Anfrage senden sollen.",
"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.",
"shareAccessHint": "Jeder mit diesem Link kann auf die Ressource zugreifen. Teilen Sie sie mit Vorsicht.",
@@ -449,8 +455,14 @@
"provisioningManage": "Bereitstellung",
"provisioningDescription": "Bereitstellungsschlüssel verwalten und ausstehende Standorte prüfen, die noch auf Genehmigung warten.",
"pendingSites": "Ausstehende Standorte",
"siteApproveSuccess": "Standort erfolgreich freigegeben",
"siteApproveSuccess": "Site und zugehörige Ressourcen erfolgreich genehmigt",
"siteApproveError": "Fehler beim Genehmigen des Standorts",
"siteReject": "Site ablehnen",
"siteQuestionReject": "Sind Sie sicher, dass Sie diese Site ablehnen möchten?",
"siteMessageReject": "Dies wird die Site und alle damit verbundenen, noch ausstehenden Ressourcen dauerhaft löschen.",
"siteConfirmReject": "Ablehnung der Site bestätigen",
"siteRejectSuccess": "Site erfolgreich abgelehnt",
"siteRejectError": "Fehler beim Ablehnen der Site",
"provisioningKeys": "Bereitstellungsschlüssel",
"searchProvisioningKeys": "Bereitstellungsschlüssel suchen...",
"provisioningKeysAdd": "Bereitstellungsschlüssel generieren",
@@ -467,7 +479,7 @@
"provisioningKeysSaveDescription": "Sie können dies nur einmal sehen. Kopieren Sie es an einen sicheren Ort.",
"provisioningKeysErrorCreate": "Fehler beim Erstellen des Bereitstellungsschlüssels",
"provisioningKeysList": "Neuer Bereitstellungsschlüssel",
"provisioningKeysMaxBatchSize": "Max. Batch-Größe",
"provisioningKeysMaxBatchSize": "Maximale Batch-Größe",
"provisioningKeysUnlimitedBatchSize": "Unbegrenzte Batch-Größe (kein Limit)",
"provisioningKeysMaxBatchUnlimited": "Unbegrenzt",
"provisioningKeysMaxBatchSizeInvalid": "Geben Sie eine gültige maximale Batchgröße ein (11.000.000).",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Nie",
"provisioningKeysEdit": "Bereitstellungsschlüssel bearbeiten",
"provisioningKeysEditDescription": "Aktualisieren Sie die maximale Batch-Größe und Ablaufzeit für diesen Schlüssel.",
"provisioningKeysApproveNewSites": "Neuen Standort genehmigen",
"provisioningKeysApproveNewSites": "Neue Sites genehmigen",
"provisioningKeysApproveNewSitesDescription": "Sites, die sich mit diesem Schlüssel registrieren, automatisch freigeben.",
"provisioningKeysUpdateError": "Fehler beim Aktualisieren des Bereitstellungsschlüssels",
"provisioningKeysUpdated": "Bereitstellungsschlüssel aktualisiert",
@@ -922,7 +934,7 @@
"newtVersion": "Version",
"architecture": "Architektur",
"sites": "Standorte",
"siteWgAnyClients": "Verwenden Sie jeden WireGuard-Client um sich zu verbinden. Sie müssen interne Ressourcen über die Peer-IP ansprechen.",
"siteWgAnyClients": "Verwenden Sie jeden beliebigen WireGuard-Client, um sich zu verbinden. Sie müssen die privaten Ressourcen mit der Peer-IP adressieren.",
"siteWgCompatibleAllClients": "Kompatibel mit allen WireGuard-Clients",
"siteWgManualConfigurationRequired": "Manuelle Konfiguration erforderlich",
"userErrorNotAdminOrOwner": "Benutzer ist kein Administrator oder Eigentümer",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Org Benutzer erstellen",
"actionUpdateOrg": "Organisation aktualisieren",
"actionRemoveInvitation": "Einladung entfernen",
"actionRemoveUserRole": "Benutzerrolle entfernen",
"actionUpdateUser": "Benutzer aktualisieren",
"actionGetUser": "Benutzer abrufen",
"actionGetOrgUser": "Organisationsbenutzer abrufen",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
"setupTokenRequired": "Setup-Token ist erforderlich",
"actionUpdateSite": "Standorte aktualisieren",
"actionApproveSite": "Site genehmigen",
"actionRejectSite": "Site ablehnen",
"actionResetSiteBandwidth": "Organisations-Bandbreite zurücksetzen",
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
"actionCreateResource": "Ressource erstellen",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Einladungen auflisten",
"actionExportLogs": "Logs exportieren",
"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",
"orgNotFound2": "Keine Organisationen gefunden.",
"search": "Suche…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Geben Sie den Code aus Ihrer Authenticator-App oder einen Ihrer einmaligen Backup-Codes ein.",
"otpAuthSubmit": "Code absenden",
"idpContinue": "Oder weiter mit",
"idpLastUsed": "Zuletzt verwendet",
"otpAuthBack": "Zurück zum Passwort",
"navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mehr anzeigen",
"domainPickerNoDomainsAvailableTitle": "Keine Domains verfügbar",
"domainPickerNoDomainsAvailableDescription": "Sie haben noch keine Domains eingerichtet. Erstellen Sie eine Domain, um fortzufahren.",
"domainPickerNoDomainsAvailableAction": "Zu Domains wechseln",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Öffentlich",
"resourcesTableClientResources": "Privat",
"resourcesTableNoProxyResourcesFound": "Keine Proxy-Ressourcen gefunden.",
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
"resourcesTableNoInternalResourcesFound": "Keine privaten Ressourcen gefunden.",
"resourcesTableDestination": "Ziel",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias-Adresse",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Abbrechen",
"editInternalResourceDialogSaveResource": "Ressource speichern",
"editInternalResourceDialogSuccess": "Erfolg",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne Ressource erfolgreich aktualisiert",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Private Ressource erfolgreich aktualisiert",
"editInternalResourceDialogError": "Fehler",
"editInternalResourceDialogFailedToUpdateInternalResource": "Interne Ressource konnte nicht aktualisiert werden",
"editInternalResourceDialogFailedToUpdateInternalResource": "Fehler beim Aktualisieren der privaten Ressource",
"editInternalResourceDialogNameRequired": "Name ist erforderlich",
"editInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein",
"editInternalResourceDialogProxyPortMin": "Proxy-Port muss mindestens 1 sein",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Ein optionaler interner DNS-Alias für diese Ressource.",
"createInternalResourceDialogNoSitesAvailable": "Kein Standort verfügbar",
"createInternalResourceDialogNoSitesAvailableDescription": "Sie müssen mindestens ein Newt-Standort mit einem konfigurierten Subnetz haben, um interne Ressourcen zu erstellen.",
"createInternalResourceDialogNoSitesAvailableDescription": "Sie müssen mindestens eine Newt-Site mit einem konfigurierten Subnetz haben, um private Ressourcen zu erstellen.",
"createInternalResourceDialogClose": "Schließen",
"createInternalResourceDialogCreateClientResource": "Private Ressource erstellen",
"createInternalResourceDialogCreateClientResourceDescription": "Erstelle eine neue Ressource, die nur für Clients zugänglich ist, die mit der Organisation verbunden sind",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Abbrechen",
"createInternalResourceDialogCreateResource": "Ressource erstellen",
"createInternalResourceDialogSuccess": "Erfolg",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne Ressource erfolgreich erstellt",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Private Ressource erfolgreich erstellt",
"createInternalResourceDialogError": "Fehler",
"createInternalResourceDialogFailedToCreateInternalResource": "Interne Ressource konnte nicht erstellt werden",
"createInternalResourceDialogFailedToCreateInternalResource": "Fehler beim Erstellen der privaten Ressource",
"createInternalResourceDialogNameRequired": "Name ist erforderlich",
"createInternalResourceDialogNameMaxLength": "Der Name darf nicht länger als 255 Zeichen sein",
"createInternalResourceDialogPleaseSelectSite": "Bitte wählen Sie einen Standort aus",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Quelladresse",
"destinationAddress": "Zieladresse",
"duration": "Dauer",
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion nutzen zu können. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion zu nutzen. <bookADemoLink>Buchen Sie eine kostenlose Demo oder POC Testversion, um mehr zu erfahren.</bookADemoLink>",
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> ist notwendig, um diese Funktion zu nutzen. Diese Funktion ist auch in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar. <bookADemoLink>Buchen Sie eine kostenlose Demo oder POC Testversion, um mehr zu erfahren.</bookADemoLink>",
"certResolver": "Zertifikatsauflöser",
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
"selectCertResolver": "Zertifikatsauflöser auswählen",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Upload fehlgeschlagen",
"rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus",
"sessionToolbarShow": "Werkzeugleiste zeigen",
"sessionToolbarHide": "Werkzeugleiste ausblenden"
"sessionToolbarHide": "Werkzeugleiste ausblenden",
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirm Delete Shareable 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.",
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
"shareTokenDescription": "The access token can be passed as a query parameter or in request headers. By default it must be sent on every request. If session persistence is enabled, the first request exchanges it for a session cookie.",
"accessToken": "Access Token",
"usageExamples": "Usage Examples",
"tokenId": "Token ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Title (optional)",
"sharePathOptional": "Path (optional)",
"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",
"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.",
"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.",
@@ -449,8 +455,14 @@
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveSuccess": "Site and associated resources approved successfully",
"siteApproveError": "Error approving site",
"siteReject": "Reject Site",
"siteQuestionReject": "Are you sure you want to reject this site?",
"siteMessageReject": "This will permanently delete the site and any associated resources that are still pending.",
"siteConfirmReject": "Confirm Reject Site",
"siteRejectSuccess": "Site rejected successfully",
"siteRejectError": "Error rejecting site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysList": "New Provisioning Key",
"provisioningKeysMaxBatchSize": "Max Batch Size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntil": "Valid Until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSites": "Approve New Sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
@@ -922,7 +934,7 @@
"newtVersion": "Version",
"architecture": "Architecture",
"sites": "Sites",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address private resources using the peer IP.",
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
"siteWgManualConfigurationRequired": "Manual configuration required",
"userErrorNotAdminOrOwner": "User is not an admin or owner",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Create Org User",
"actionUpdateOrg": "Update Organization",
"actionRemoveInvitation": "Remove Invitation",
"actionRemoveUserRole": "Remove User Role",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Enter the setup token from the server console.",
"setupTokenRequired": "Setup token is required",
"actionUpdateSite": "Update Site",
"actionApproveSite": "Approve Site",
"actionRejectSite": "Reject Site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "List Allowed Site Roles",
"actionCreateResource": "Create Resource",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "List Invitations",
"actionExportLogs": "Export 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",
"orgNotFound2": "No organizations found.",
"search": "Search…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
"otpAuthSubmit": "Submit Code",
"idpContinue": "Or continue with",
"idpLastUsed": "Last Used",
"otpAuthBack": "Back to Password",
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Show More",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Select Region",
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Public",
"resourcesTableClientResources": "Private",
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableNoInternalResourcesFound": "No private resources found.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias Address",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Cancel",
"editInternalResourceDialogSaveResource": "Save Resource",
"editInternalResourceDialogSuccess": "Success",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Private resource updated successfully",
"editInternalResourceDialogError": "Error",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update private resource",
"editInternalResourceDialogNameRequired": "Name is required",
"editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create private resources.",
"createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Private Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Cancel",
"createInternalResourceDialogCreateResource": "Create Resource",
"createInternalResourceDialogSuccess": "Success",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Private resource created successfully",
"createInternalResourceDialogError": "Error",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create private resource",
"createInternalResourceDialogNameRequired": "Name is required",
"createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"createInternalResourceDialogPleaseSelectSite": "Please select a site",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"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>.",
"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>.",
"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>",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Upload failed",
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
"sessionToolbarShow": "Show toolbar",
"sessionToolbarHide": "Hide toolbar"
"sessionToolbarHide": "Hide toolbar",
"actionUpdateSiteApprovals": "Update Site Approvals"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmar Eliminación de Enlace Compartible",
"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.",
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.",
"shareTokenDescription": "El token de acceso se puede pasar como un parámetro de consulta o en los encabezados de la solicitud. Por defecto, se debe enviar en cada solicitud. Si la persistencia de sesión está habilitada, la primera solicitud lo intercambia por una cookie de sesión.",
"accessToken": "Token de acceso",
"usageExamples": "Ejemplos de uso",
"tokenId": "ID de token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Título (opcional)",
"sharePathOptional": "Ruta (opcional)",
"sharePathDescription": "El enlace redirigirá a los usuarios a esta ruta tras la autenticación.",
"shareAssociateUserOptional": "Asociar Usuario (opcional)",
"shareAssociateUserDescription": "Cuando está configurado, las solicitudes que usan este enlace se atribuyen al usuario en los registros de acceso y encabezados de identidad. El enlace se elimina si el usuario abandona la organización.",
"userSelect": "Seleccione usuario",
"usersNotFound": "No se encontraron usuarios",
"expireIn": "Caduca en",
"neverExpire": "Nunca expirar",
"sharePersistSession": "Persistir sesión después del primer uso",
"sharePersistSessionDescription": "Cuando está habilitado, la primera solicitud con este token mediante un parámetro de consulta o encabezado configura una cookie de sesión, por lo que las solicitudes posteriores no necesitan el token. Dejar desactivado para clientes de API que deben enviar el token en cada solicitud.",
"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.",
"shareAccessHint": "Cualquiera con este enlace puede acceder al recurso. Compártelo con cuidado.",
@@ -449,8 +455,14 @@
"provisioningManage": "Aprovisionamiento",
"provisioningDescription": "Administrar las claves de aprovisionamiento y revisar los sitios pendientes de aprobación.",
"pendingSites": "Sitios pendientes",
"siteApproveSuccess": "Sitio aprobado con éxito",
"siteApproveSuccess": "Sitio y recursos asociados aprobados correctamente",
"siteApproveError": "Error al aprobar el sitio",
"siteReject": "Rechazar Sitio",
"siteQuestionReject": "¿Está seguro de que desea rechazar este sitio?",
"siteMessageReject": "Esto eliminará permanentemente el sitio y cualquier recurso asociado que aún esté pendiente.",
"siteConfirmReject": "Confirmar Rechazo del Sitio",
"siteRejectSuccess": "Sitio rechazado correctamente",
"siteRejectError": "Error al rechazar el sitio",
"provisioningKeys": "Claves de aprovisionamiento",
"searchProvisioningKeys": "Buscar claves de suministro...",
"provisioningKeysAdd": "Generar clave de aprovisionamiento",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Guardar la clave de aprovisionamiento",
"provisioningKeysSaveDescription": "Sólo podrás verlo una vez. Copítalo a un lugar seguro.",
"provisioningKeysErrorCreate": "Error al crear la clave de provisioning",
"provisioningKeysList": "Nueva clave de aprovisionamiento",
"provisioningKeysMaxBatchSize": "Tamaño máximo de lote",
"provisioningKeysList": "Nueva Clave de Provisión",
"provisioningKeysMaxBatchSize": "Tamaño Máximo del Lote",
"provisioningKeysUnlimitedBatchSize": "Tamaño ilimitado del lote (sin límite)",
"provisioningKeysMaxBatchUnlimited": "Ilimitado",
"provisioningKeysMaxBatchSizeInvalid": "Introduzca un tamaño máximo de lote válido (11,000,000).",
"provisioningKeysValidUntil": "Válido hasta",
"provisioningKeysValidUntil": "Válido Hasta",
"provisioningKeysValidUntilHint": "Dejar vacío para no expirar.",
"provisioningKeysValidUntilInvalid": "Introduzca una fecha y hora válidas.",
"provisioningKeysNumUsed": "Tiempos usados",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Nunca",
"provisioningKeysEdit": "Editar clave de aprovisionamiento",
"provisioningKeysEditDescription": "Actualizar el tamaño máximo de lote y el tiempo de caducidad para esta clave.",
"provisioningKeysApproveNewSites": "Aprobar nuevos sitios",
"provisioningKeysApproveNewSites": "Aprobar Nuevos Sitios",
"provisioningKeysApproveNewSitesDescription": "Aprobar automáticamente los sitios que se registran con esta clave.",
"provisioningKeysUpdateError": "Error al actualizar la clave de aprovisionamiento",
"provisioningKeysUpdated": "Clave de aprovisionamiento actualizada",
@@ -922,7 +934,7 @@
"newtVersion": "Versión",
"architecture": "Arquitectura",
"sites": "Sitios",
"siteWgAnyClients": "Usa cualquier cliente de Wirex para conectarte. Tendrás que dirigirte a los recursos internos usando la IP de compañeros.",
"siteWgAnyClients": "Usa cualquier cliente de WireGuard para conectarte. Tendrás que dirigirte a recursos privados usando la IP del par.",
"siteWgCompatibleAllClients": "Compatible con todos los clientes de Wirex Guard",
"siteWgManualConfigurationRequired": "Configuración manual requerida",
"userErrorNotAdminOrOwner": "El usuario no es un administrador o propietario",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Crear usuario Org",
"actionUpdateOrg": "Actualizar organización",
"actionRemoveInvitation": "Eliminar invitación",
"actionRemoveUserRole": "Quitar Rol de Usuario",
"actionUpdateUser": "Actualizar usuario",
"actionGetUser": "Obtener usuario",
"actionGetOrgUser": "Obtener usuario de la organización",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
"setupTokenRequired": "Se requiere el token de configuración",
"actionUpdateSite": "Actualizar sitio",
"actionApproveSite": "Aprobar Sitio",
"actionRejectSite": "Rechazar Sitio",
"actionResetSiteBandwidth": "Restablecer ancho de banda de la organización",
"actionListSiteRoles": "Lista de roles permitidos del sitio",
"actionCreateResource": "Crear Recurso",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Listar invitaciones",
"actionExportLogs": "Exportar 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",
"orgNotFound2": "No se encontraron organizaciones.",
"search": "Buscar…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Introduzca el código de su aplicación de autenticación o uno de sus códigos de copia de seguridad de un solo uso.",
"otpAuthSubmit": "Enviar código",
"idpContinue": "O continuar con",
"idpLastUsed": "Último Uso",
"otpAuthBack": "Volver a la contraseña",
"navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdominio: {subdomain}",
"domainPickerNamespace": "Espacio de nombres: {namespace}",
"domainPickerShowMore": "Mostrar más",
"domainPickerNoDomainsAvailableTitle": "No hay dominios disponibles",
"domainPickerNoDomainsAvailableDescription": "Aún no tiene ningún dominio configurado. Cree un dominio para continuar.",
"domainPickerNoDomainsAvailableAction": "Ir a Dominios",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Público",
"resourcesTableClientResources": "Privado",
"resourcesTableNoProxyResourcesFound": "No se encontraron recursos de proxy.",
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos privados.",
"resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Dirección del alias",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Cancelar",
"editInternalResourceDialogSaveResource": "Guardar recurso",
"editInternalResourceDialogSuccess": "Éxito",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno actualizado con éxito",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso privado actualizado con éxito",
"editInternalResourceDialogError": "Error",
"editInternalResourceDialogFailedToUpdateInternalResource": "Error al actualizar el recurso interno",
"editInternalResourceDialogFailedToUpdateInternalResource": "Error al actualizar el recurso privado",
"editInternalResourceDialogNameRequired": "El nombre es requerido",
"editInternalResourceDialogNameMaxLength": "El nombre no debe tener más de 255 caracteres",
"editInternalResourceDialogProxyPortMin": "El puerto del proxy debe ser al menos 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Un alias DNS interno opcional para este recurso.",
"createInternalResourceDialogNoSitesAvailable": "No hay sitios disponibles",
"createInternalResourceDialogNoSitesAvailableDescription": "Necesita tener al menos un sitio de Newt con una subred configurada para crear recursos internos.",
"createInternalResourceDialogNoSitesAvailableDescription": "Necesita tener al menos un sitio de Newt con una subred configurada para crear recursos privados.",
"createInternalResourceDialogClose": "Cerrar",
"createInternalResourceDialogCreateClientResource": "Crear recurso privado",
"createInternalResourceDialogCreateClientResourceDescription": "Crear un nuevo recurso que sólo será accesible a los clientes conectados a la organización",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Cancelar",
"createInternalResourceDialogCreateResource": "Crear recurso",
"createInternalResourceDialogSuccess": "Éxito",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno creado con éxito",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso privado creado con éxito",
"createInternalResourceDialogError": "Error",
"createInternalResourceDialogFailedToCreateInternalResource": "Error al crear recurso interno",
"createInternalResourceDialogFailedToCreateInternalResource": "Error al crear recurso privado",
"createInternalResourceDialogNameRequired": "El nombre es requerido",
"createInternalResourceDialogNameMaxLength": "El nombre debe ser menor de 255 caracteres",
"createInternalResourceDialogPleaseSelectSite": "Por favor seleccione un sitio",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Dirección de origen",
"destinationAddress": "Dirección de destino",
"duration": "Duración",
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> para usar esta función. <bookADemoLink>Reserve una demostración o prueba POC</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> para usar esta función. <bookADemoLink>Reserve una demostración gratuita o una prueba POC para saber más.</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 gratuita o prueba POC para saber más.</bookADemoLink>",
"certResolver": "Resolver certificado",
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
"selectCertResolver": "Seleccionar Resolver Certificado",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Error de subida",
"rdpUnicodeKeyboardMode": "Modo teclado Unicode",
"sessionToolbarShow": "Mostrar barra de herramientas",
"sessionToolbarHide": "Ocultar barra de herramientas"
"sessionToolbarHide": "Ocultar barra de herramientas",
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio"
}
+38 -14
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmer la suppression du lien partageable",
"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.",
"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é.",
"shareTokenDescription": "Le jeton d'accès peut être transmis comme paramètre de requête ou dans les en-têtes de requête. Par défaut, il doit être envoyé à chaque requête. Si la persistance de session est activée, la première requête l'échange contre un cookie de session.",
"accessToken": "Jeton d'accès",
"usageExamples": "Exemples d'utilisation",
"tokenId": "ID du jeton",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titre (facultatif)",
"sharePathOptional": "Chemin (optionnel)",
"sharePathDescription": "Le lien redirigera les utilisateurs vers ce chemin après l'authentification.",
"shareAssociateUserOptional": "Associer un utilisateur (facultatif)",
"shareAssociateUserDescription": "Lorsqu'il est défini, les requêtes utilisant ce lien sont attribuées à l'utilisateur dans les journaux d'accès et les en-têtes d'identité. Le lien est supprimé si l'utilisateur quitte l'organisation.",
"userSelect": "Sélectionner un utilisateur",
"usersNotFound": "Aucun utilisateur trouvé",
"expireIn": "Expire dans",
"neverExpire": "N'expire jamais",
"sharePersistSession": "Persister la session après la première utilisation",
"sharePersistSessionDescription": "Lorsqu'elle est activée, la première requête avec ce jeton via un paramètre de requête ou un en-tête définit un cookie de session afin que les requêtes ultérieures n'aient pas besoin du jeton. Désactivez-le pour les clients API qui doivent envoyer le jeton à chaque requête.",
"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.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec précaution.",
@@ -449,8 +455,14 @@
"provisioningManage": "Mise en place",
"provisioningDescription": "Gérer les clés de provisioning et examiner les sites en attente d'approbation.",
"pendingSites": "Sites en attente",
"siteApproveSuccess": "Site approuvé avec succès",
"siteApproveSuccess": "Site et ressources associées approuvés avec succès",
"siteApproveError": "Erreur lors de l'approbation du site",
"siteReject": "Rejeter le site",
"siteQuestionReject": "Êtes-vous sûr de vouloir rejeter ce site ?",
"siteMessageReject": "Cela supprimera définitivement le site et toutes les ressources associées qui sont encore en attente.",
"siteConfirmReject": "Confirmer le rejet du site",
"siteRejectSuccess": "Site rejeté avec succès",
"siteRejectError": "Erreur lors du rejet du site",
"provisioningKeys": "Clés de provisionnement",
"searchProvisioningKeys": "Recherche des clés de provision...",
"provisioningKeysAdd": "Générer une clé de provisioning",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Enregistrer la clé de provisioning",
"provisioningKeysSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Copiez-le dans un endroit sécurisé.",
"provisioningKeysErrorCreate": "Erreur lors de la création de la clé de provisioning",
"provisioningKeysList": "Nouvelle clé de provisioning",
"provisioningKeysList": "Nouvelle clé d'approvisionnement",
"provisioningKeysMaxBatchSize": "Taille maximale du lot",
"provisioningKeysUnlimitedBatchSize": "Taille de lot illimitée (sans limite)",
"provisioningKeysMaxBatchUnlimited": "Illimité",
"provisioningKeysMaxBatchSizeInvalid": "Entrez une taille de lot maximale valide (11 000 000).",
"provisioningKeysValidUntil": "Valable jusqu'au",
"provisioningKeysValidUntil": "Valable jusqu'à",
"provisioningKeysValidUntilHint": "Laisser vide pour ne pas expirer.",
"provisioningKeysValidUntilInvalid": "Entrez une date et une heure valides.",
"provisioningKeysNumUsed": "Nombre de fois utilisées",
@@ -922,7 +934,7 @@
"newtVersion": "Version",
"architecture": "Architecture",
"sites": "Nœuds",
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser des ressources internes en utilisant l'adresse IP du pair.",
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser les ressources privées en utilisant l'IP du pair.",
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
"userErrorNotAdminOrOwner": "L'utilisateur n'est pas un administrateur ou un propriétaire",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Créer un utilisateur Org",
"actionUpdateOrg": "Mettre à jour l'organisation",
"actionRemoveInvitation": "Supprimer l'invitation",
"actionRemoveUserRole": "Supprimer le rôle d'utilisateur",
"actionUpdateUser": "Mettre à jour l'utilisateur",
"actionGetUser": "Obtenir l'utilisateur",
"actionGetOrgUser": "Obtenir l'utilisateur de l'organisation",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
"actionUpdateSite": "Mettre à jour un site",
"actionApproveSite": "Approuver le site",
"actionRejectSite": "Rejeter le site",
"actionResetSiteBandwidth": "Réinitialiser la bande passante de l'organisation",
"actionListSiteRoles": "Lister les rôles autorisés du site",
"actionCreateResource": "Créer une ressource",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Lister les invitations",
"actionExportLogs": "Exporter les journaux",
"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",
"orgNotFound2": "Aucune organisation trouvée.",
"search": "Rechercher…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Entrez le code de votre application d'authentification ou l'un de vos codes de secours à usage unique.",
"otpAuthSubmit": "Soumettre le code",
"idpContinue": "Ou continuer avec",
"idpLastUsed": "Dernière utilisation",
"otpAuthBack": "Retour au mot de passe",
"navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Sous-domaine : {subdomain}",
"domainPickerNamespace": "Espace de noms : {namespace}",
"domainPickerShowMore": "Afficher plus",
"domainPickerNoDomainsAvailableTitle": "Aucun domaine disponible",
"domainPickerNoDomainsAvailableDescription": "Vous n'avez pas encore configuré de domaine. Créez un domaine pour continuer.",
"domainPickerNoDomainsAvailableAction": "Aller aux domaines",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Publique",
"resourcesTableClientResources": "Privé",
"resourcesTableNoProxyResourcesFound": "Aucune ressource proxy trouvée.",
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
"resourcesTableNoInternalResourcesFound": "Aucune ressource privée trouvée.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adresse de l'alias",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Abandonner",
"editInternalResourceDialogSaveResource": "Enregistrer la ressource",
"editInternalResourceDialogSuccess": "Succès",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Ressource interne mise à jour avec succès",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Ressource privée mise à jour avec succès",
"editInternalResourceDialogError": "Erreur",
"editInternalResourceDialogFailedToUpdateInternalResource": "Échec de la mise à jour de la ressource interne",
"editInternalResourceDialogFailedToUpdateInternalResource": "Échec de la mise à jour de la ressource privée",
"editInternalResourceDialogNameRequired": "Le nom est requis",
"editInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères",
"editInternalResourceDialogProxyPortMin": "Le port proxy doit être d'au moins 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Un alias DNS interne optionnel pour cette ressource.",
"createInternalResourceDialogNoSitesAvailable": "Aucun site disponible",
"createInternalResourceDialogNoSitesAvailableDescription": "Vous devez avoir au moins un site Newt avec un sous-réseau configuré pour créer des ressources internes.",
"createInternalResourceDialogNoSitesAvailableDescription": "Vous devez avoir au moins un site Newt avec un sous-réseau configuré pour créer des ressources privées.",
"createInternalResourceDialogClose": "Fermer",
"createInternalResourceDialogCreateClientResource": "Créer une ressource privée",
"createInternalResourceDialogCreateClientResourceDescription": "Créer une nouvelle ressource qui ne sera accessible qu'aux clients connectés à l'organisation",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Abandonner",
"createInternalResourceDialogCreateResource": "Créer une ressource",
"createInternalResourceDialogSuccess": "Succès",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Ressource interne créée avec succès",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Ressource privée créée avec succès",
"createInternalResourceDialogError": "Erreur",
"createInternalResourceDialogFailedToCreateInternalResource": "Échec de la création de la ressource interne",
"createInternalResourceDialogFailedToCreateInternalResource": "Échec de la création de la ressource privée",
"createInternalResourceDialogNameRequired": "Le nom est requis",
"createInternalResourceDialogNameMaxLength": "Le nom doit être inférieur à 255 caractères",
"createInternalResourceDialogPleaseSelectSite": "Veuillez sélectionner un site",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Adresse source",
"destinationAddress": "Adresse de destination",
"duration": "Durée",
"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": "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>.",
"licenseRequiredToUse": "Une <enterpriseLicenseLink>licence Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> est requise pour utiliser cette fonctionnalité. <bookADemoLink>Réservez une démo gratuite ou un essai POC pour en savoir plus.</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 gratuite ou un essai POC pour en savoir plus.</bookADemoLink>",
"certResolver": "Résolveur de certificat",
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
"selectCertResolver": "Sélectionnez le résolveur de certificat",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Échec du téléchargement",
"rdpUnicodeKeyboardMode": "Mode clavier Unicode",
"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"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Conferma Eliminazione Collegamento Condivisibile",
"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.",
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.",
"shareTokenDescription": "Il token di accesso può essere passato come parametro di query o nei header delle richieste. Per impostazione predefinita deve essere inviato a ogni richiesta. Se la persistenza della sessione è abilitata, la prima richiesta lo scambia per un cookie di sessione.",
"accessToken": "Token Di Accesso",
"usageExamples": "Esempi Di Utilizzo",
"tokenId": "ID del Token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titolo (facoltativo)",
"sharePathOptional": "Percorso (opzionale)",
"sharePathDescription": "Il link reindirizzerà gli utenti a questo percorso dopo l'autenticazione.",
"shareAssociateUserOptional": "Associa utente (opzionale)",
"shareAssociateUserDescription": "Quando impostato, le richieste utilizzando questo link sono attribuite all'utente nei log di accesso e negli header di identità. Il link viene rimosso se l'utente lascia l'organizzazione.",
"userSelect": "Seleziona utente",
"usersNotFound": "Nessun utente trovato",
"expireIn": "Scadenza In",
"neverExpire": "Nessuna scadenza",
"sharePersistSession": "Mantieni la sessione dopo il primo utilizzo",
"sharePersistSessionDescription": "Quando abilitato, la prima richiesta con questo token tramite un parametro di query o header imposta un cookie di sessione quindi le richieste successive non hanno bisogno del token. Disattivare per i client API che devono inviare il token a ogni richiesta.",
"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.",
"shareAccessHint": "Chiunque abbia questo link può accedere alla risorsa. Condividilo con cura.",
@@ -449,8 +455,14 @@
"provisioningManage": "Accantonamento",
"provisioningDescription": "Gestire le chiavi di provisioning e rivedere i siti in attesa di approvazione.",
"pendingSites": "Siti In Attesa",
"siteApproveSuccess": "Sito approvato con successo",
"siteApproveSuccess": "Sito e risorse associate approvate con successo",
"siteApproveError": "Errore nell'approvazione del sito",
"siteReject": "Rifiuta Sito",
"siteQuestionReject": "Sei sicuro di voler rifiutare questo sito?",
"siteMessageReject": "Questo eliminerà permanentemente il sito e tutte le risorse associate ancora in sospeso.",
"siteConfirmReject": "Conferma Rifiuto Sito",
"siteRejectSuccess": "Sito rifiutato con successo",
"siteRejectError": "Errore nel rifiutare il sito",
"provisioningKeys": "Chiavi Di Provvedimento",
"searchProvisioningKeys": "Cerca le chiavi di provisioning...",
"provisioningKeysAdd": "Genera Chiave di provisioning",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Salva la chiave di provisioning",
"provisioningKeysSaveDescription": "Sarai in grado di vedere solo una volta. Copiarlo in un posto sicuro.",
"provisioningKeysErrorCreate": "Errore nella creazione della chiave di provisioning",
"provisioningKeysList": "Nuova chiave di provisioning",
"provisioningKeysMaxBatchSize": "Dimensione massima batch",
"provisioningKeysList": "Nuova Chiave di Provisioning",
"provisioningKeysMaxBatchSize": "Dimensione Massima Batch",
"provisioningKeysUnlimitedBatchSize": "Dimensione illimitata del batch (nessun limite)",
"provisioningKeysMaxBatchUnlimited": "Illimitato",
"provisioningKeysMaxBatchSizeInvalid": "Inserisci una dimensione massima valida del batch (11.000.000).",
"provisioningKeysValidUntil": "Valido fino al",
"provisioningKeysValidUntil": "Valido Fino a",
"provisioningKeysValidUntilHint": "Lasciare vuoto per nessuna scadenza.",
"provisioningKeysValidUntilInvalid": "Inserisci una data e ora valide.",
"provisioningKeysNumUsed": "Volte usate",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Mai",
"provisioningKeysEdit": "Modifica Chiave di provisioning",
"provisioningKeysEditDescription": "Aggiorna la dimensione massima del batch e il tempo di scadenza per questa chiave.",
"provisioningKeysApproveNewSites": "Approva nuovi siti",
"provisioningKeysApproveNewSites": "Approva Nuovi Siti",
"provisioningKeysApproveNewSitesDescription": "Approvare automaticamente i siti che si registrano con questa chiave.",
"provisioningKeysUpdateError": "Errore nell'aggiornamento della chiave di provisioning",
"provisioningKeysUpdated": "Chiave di provisioning aggiornata",
@@ -922,7 +934,7 @@
"newtVersion": "Versione",
"architecture": "Architettura",
"sites": "Siti",
"siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le risorse interne utilizzando l'IP del peer.",
"siteWgAnyClients": "Usa qualsiasi client WireGuard per connetterti. Dovrai indirizzare le risorse private utilizzando l'IP del peer.",
"siteWgCompatibleAllClients": "Compatibile con tutti i client WireGuard",
"siteWgManualConfigurationRequired": "Configurazione manuale richiesta",
"userErrorNotAdminOrOwner": "L'utente non è un amministratore o proprietario",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Crea Utente Org",
"actionUpdateOrg": "Aggiorna Organizzazione",
"actionRemoveInvitation": "Rimuovi Invito",
"actionRemoveUserRole": "Rimuovi Ruolo Utente",
"actionUpdateUser": "Aggiorna Utente",
"actionGetUser": "Ottieni Utente",
"actionGetOrgUser": "Ottieni Utente Organizzazione",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
"setupTokenRequired": "Il token di configurazione è richiesto",
"actionUpdateSite": "Aggiorna Sito",
"actionApproveSite": "Approva Sito",
"actionRejectSite": "Rifiuta Sito",
"actionResetSiteBandwidth": "Reimposta Larghezza Banda Dell'Organizzazione",
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
"actionCreateResource": "Crea Risorsa",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Elenco Inviti",
"actionExportLogs": "Esporta 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",
"orgNotFound2": "Nessuna organizzazione trovata.",
"search": "Cerca…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Inserisci il codice dalla tua app di autenticazione o uno dei tuoi codici di backup monouso.",
"otpAuthSubmit": "Invia Codice",
"idpContinue": "O continua con",
"idpLastUsed": "Ultimo Utilizzo",
"otpAuthBack": "Torna alla Password",
"navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Sottodominio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostra Altro",
"domainPickerNoDomainsAvailableTitle": "Nessun dominio disponibile",
"domainPickerNoDomainsAvailableDescription": "Non hai ancora configurato alcun dominio. Crea un dominio per continuare.",
"domainPickerNoDomainsAvailableAction": "Vai ai Domini",
"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.",
"regionSelectorInfo": "Selezionare una regione ci aiuta a fornire migliori performance per la tua posizione. Non devi necessariamente essere nella stessa regione del tuo server.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Pubblico",
"resourcesTableClientResources": "Privato",
"resourcesTableNoProxyResourcesFound": "Nessuna risorsa proxy trovata.",
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa privata trovata.",
"resourcesTableDestination": "Destinazione",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Indirizzo Alias",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Annulla",
"editInternalResourceDialogSaveResource": "Salva Risorsa",
"editInternalResourceDialogSuccess": "Successo",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Risorsa interna aggiornata con successo",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Risorsa privata aggiornata con successo",
"editInternalResourceDialogError": "Errore",
"editInternalResourceDialogFailedToUpdateInternalResource": "Impossibile aggiornare la risorsa interna",
"editInternalResourceDialogFailedToUpdateInternalResource": "Impossibile aggiornare la risorsa privata",
"editInternalResourceDialogNameRequired": "Il nome è obbligatorio",
"editInternalResourceDialogNameMaxLength": "Il nome deve essere inferiore a 255 caratteri",
"editInternalResourceDialogProxyPortMin": "La porta proxy deve essere almeno 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Un alias DNS interno opzionale per questa risorsa.",
"createInternalResourceDialogNoSitesAvailable": "Nessun Sito Disponibile",
"createInternalResourceDialogNoSitesAvailableDescription": "Devi avere almeno un sito Newt con una subnet configurata per creare risorse interne.",
"createInternalResourceDialogNoSitesAvailableDescription": "Devi avere almeno un sito Newt con una subnet configurata per creare risorse private.",
"createInternalResourceDialogClose": "Chiudi",
"createInternalResourceDialogCreateClientResource": "Crea Risorsa Privata",
"createInternalResourceDialogCreateClientResourceDescription": "Crea una nuova risorsa che sarà accessibile solo ai client connessi all'organizzazione",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Annulla",
"createInternalResourceDialogCreateResource": "Crea Risorsa",
"createInternalResourceDialogSuccess": "Successo",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Risorsa interna creata con successo",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Risorsa privata creata con successo",
"createInternalResourceDialogError": "Errore",
"createInternalResourceDialogFailedToCreateInternalResource": "Impossibile creare la risorsa interna",
"createInternalResourceDialogFailedToCreateInternalResource": "Impossibile creare la risorsa privata",
"createInternalResourceDialogNameRequired": "Il nome è obbligatorio",
"createInternalResourceDialogNameMaxLength": "Il nome non deve superare i 255 caratteri",
"createInternalResourceDialogPleaseSelectSite": "Si prega di selezionare un sito",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Indirizzo Di Origine",
"destinationAddress": "Indirizzo Di Destinazione",
"duration": "Durata",
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Prenota una demo o una prova POC</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Prenota una demo gratuita o una prova POC per saperne di più.</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 gratuita o una prova POC per saperne di più.</bookADemoLink>",
"certResolver": "Risolutore Di Certificato",
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
"selectCertResolver": "Seleziona Risolutore Di Certificato",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Caricamento fallito",
"rdpUnicodeKeyboardMode": "Modalità tastiera Unicode",
"sessionToolbarShow": "Mostra barra degli strumenti",
"sessionToolbarHide": "Nascondi barra degli strumenti"
"sessionToolbarHide": "Nascondi barra degli strumenti",
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito"
}
+37 -13
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "공유 가능한 링크 삭제 확인",
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더 전달될 수 있습니다. 기본적으로 모든 요청에 포함되어야 합니다. 세션 지속이 활성화된 경우, 첫 번째 요청이 세션 쿠키로 교환됩니다.",
"accessToken": "액세스 토큰",
"usageExamples": "사용 예",
"tokenId": "토큰 ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "제목 (선택 사항)",
"sharePathOptional": "경로 (선택 사항)",
"sharePathDescription": "링크는 인증 후 이 경로로 사용자를 리디렉션합니다.",
"shareAssociateUserOptional": "사용자 연관 (선택 사항)",
"shareAssociateUserDescription": "설정 시, 이 링크를 사용하는 요청은 액세스 로그와 ID 헤더에서 사용자로 기록됩니다. 사용자가 조직을 떠나면 링크가 제거됩니다.",
"userSelect": "사용자 선택",
"usersNotFound": "사용자를 찾을 수 없습니다",
"expireIn": "만료됨",
"neverExpire": "만료되지 않음",
"sharePersistSession": "첫 사용 후 세션 지속",
"sharePersistSessionDescription": "활성화된 경우, 쿼리 매개변수나 헤더를 통해 이 토큰으로 첫 요청 시 세션 쿠키가 설정되어 이후 요청에는 토큰이 필요하지 않습니다. 모든 요청에 토큰을 포함해야 하는 API 클라이언트의 경우, 이 옵션을 해제하세요.",
"shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.",
"shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.",
"shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.",
@@ -449,8 +455,14 @@
"provisioningManage": "프로비저닝",
"provisioningDescription": "프로비저닝 키를 관리하고 승인을 기다리는 사이트를 검토합니다.",
"pendingSites": "대기중인 사이트",
"siteApproveSuccess": "사이트가 성공적으로 승인되었습니다",
"siteApproveSuccess": "사이트 및 관련 리소스가 성공적으로 승인되었습니다",
"siteApproveError": "사이트 승인 오류",
"siteReject": "사이트 거부",
"siteQuestionReject": "이 사이트를 거부하시겠습니까?",
"siteMessageReject": "이렇게 하면 펜딩 중인 사이트 및 관련 리소스가 영구적으로 삭제됩니다.",
"siteConfirmReject": "사이트 거부 확인",
"siteRejectSuccess": "사이트가 성공적으로 거부되었습니다",
"siteRejectError": "사이트 거부 오류",
"provisioningKeys": "프로비저닝 키",
"searchProvisioningKeys": "프로비저닝 키 검색...",
"provisioningKeysAdd": "프로비저닝 키 생성",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "절대",
"provisioningKeysEdit": "프로비저닝 키 수정",
"provisioningKeysEditDescription": "이 키의 최대 배치 크기 및 만료 시간을 업데이트하세요.",
"provisioningKeysApproveNewSites": "새로운 사이트 승인",
"provisioningKeysApproveNewSites": "새 사이트 승인",
"provisioningKeysApproveNewSitesDescription": "이 키를 등록하는 사이트를 자동으로 승인합니다.",
"provisioningKeysUpdateError": "프로비저닝 키 업데이트 오류",
"provisioningKeysUpdated": "프로비저닝 키가 업데이트되었습니다",
@@ -922,7 +934,7 @@
"newtVersion": "버전",
"architecture": "아키텍처",
"sites": "사이트",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 개인 리소스에 접근해야 합니다.",
"siteWgCompatibleAllClients": "모든 WireGuard 클라이언트와 호환",
"siteWgManualConfigurationRequired": "수동 구성이 필요합니다.",
"userErrorNotAdminOrOwner": "사용자는 관리자 또는 소유자가 아닙니다.",
@@ -1397,6 +1409,7 @@
"createOrgUser": "조직 사용자 생성",
"actionUpdateOrg": "조직 업데이트",
"actionRemoveInvitation": "초대 제거",
"actionRemoveUserRole": "사용자 역할 제거",
"actionUpdateUser": "사용자 업데이트",
"actionGetUser": "사용자 조회",
"actionGetOrgUser": "조직 사용자 가져오기",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
"actionUpdateSite": "사이트 업데이트",
"actionApproveSite": "사이트 승인",
"actionRejectSite": "사이트 거부",
"actionResetSiteBandwidth": "조직 대역폭 재설정",
"actionListSiteRoles": "허용된 사이트 역할 목록",
"actionCreateResource": "리소스 생성",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "초대 목록",
"actionExportLogs": "로그 내보내기",
"actionViewLogs": "로그 보기",
"actionCreateSiteProvisioningKey": "사이트 프로비저닝 키 생성",
"actionListSiteProvisioningKeys": "사이트 프로비저닝 키 목록",
"actionUpdateSiteProvisioningKey": "사이트 프로비저닝 키 업데이트",
"actionDeleteSiteProvisioningKey": "사이트 프로비저닝 키 삭제",
"noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.",
"search": "검색…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "인증 앱에서 코드를 입력하거나 단일 사용 백업 코드 중 하나를 입력하세요.",
"otpAuthSubmit": "코드 제출",
"idpContinue": "또는 계속 진행하십시오.",
"idpLastUsed": "마지막 사용",
"otpAuthBack": "비밀번호로 돌아가기",
"navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "서브도메인: {subdomain}",
"domainPickerNamespace": "이름 공간: {namespace}",
"domainPickerShowMore": "더보기",
"domainPickerNoDomainsAvailableTitle": "사용 가능한 도메인이 없습니다",
"domainPickerNoDomainsAvailableDescription": "설정된 도메인이 아직 없습니다. 계속하려면 도메인을 생성하세요.",
"domainPickerNoDomainsAvailableAction": "도메인으로 이동",
"regionSelectorTitle": "지역 선택",
"domainPickerRemoteExitNodeWarning": "제공된 도메인은 원격 종료 노드에 연결된 사이트에서 지원되지 않습니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.",
"regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "공유",
"resourcesTableClientResources": "비공개",
"resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.",
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
"resourcesTableNoInternalResourcesFound": "개인 리소스를 찾을 수 없습니다.",
"resourcesTableDestination": "대상지",
"resourcesTableAlias": "별칭",
"resourcesTableAliasAddress": "별칭 주소",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "취소",
"editInternalResourceDialogSaveResource": "리소스 저장",
"editInternalResourceDialogSuccess": "성공",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "내부 리소스가 성공적으로 업데이트되었습니다",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "개인 리소스가 성공적으로 업데이트되었습니다",
"editInternalResourceDialogError": "오류",
"editInternalResourceDialogFailedToUpdateInternalResource": "내부 리소스 업데이트 실패",
"editInternalResourceDialogFailedToUpdateInternalResource": "개인 리소스 업데이트 실패",
"editInternalResourceDialogNameRequired": "이름은 필수입니다.",
"editInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
"editInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "별칭",
"editInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
"createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.",
"createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
"createInternalResourceDialogNoSitesAvailableDescription": "개인 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
"createInternalResourceDialogClose": "닫기",
"createInternalResourceDialogCreateClientResource": "사이트 리소스 생성",
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "취소",
"createInternalResourceDialogCreateResource": "리소스 생성",
"createInternalResourceDialogSuccess": "성공",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "내부 리소스가 성공적으로 생성되었습니다.",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "개인 리소스가 성공적으로 생성되었습니다",
"createInternalResourceDialogError": "오류",
"createInternalResourceDialogFailedToCreateInternalResource": "내부 리소스 생성 실패",
"createInternalResourceDialogFailedToCreateInternalResource": "개인 리소스 생성 실패",
"createInternalResourceDialogNameRequired": "이름은 필수입니다.",
"createInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
"createInternalResourceDialogPleaseSelectSite": "사이트를 선택하세요",
@@ -3075,8 +3098,8 @@
"sourceAddress": "소스 주소",
"destinationAddress": "대상 주소",
"duration": "지속 시간",
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이(가) 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>",
"certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "업로드 실패",
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
"sessionToolbarShow": "툴바 보기",
"sessionToolbarHide": "툴바 숨기기"
"sessionToolbarHide": "툴바 숨기기",
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bekreft sletting av delbar lenke",
"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.",
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
"shareTokenDescription": "Tilgangstokenn kan sendes som en spørringsparameter eller i forespørselshoder. Som standard må det sendes med hver forespørsel. Hvis sesjonsvedholdenhet er aktivert, byttes den første forespørselen mot en sesjons-cookie.",
"accessToken": "Tilgangsnøkkel",
"usageExamples": "Brukseksempler",
"tokenId": "Token-ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Tittel (valgfritt)",
"sharePathOptional": "Bane (valgfritt)",
"sharePathDescription": "Lenken vil videresende brukere til denne stien etter autentisering.",
"shareAssociateUserOptional": "Tilknytt bruker (valgfritt)",
"shareAssociateUserDescription": "Når den er satt, blir forespørsler som bruker denne koblingen tilskrevet brukeren i tilgangslogger og identitetshoder. Koblingen fjernes hvis brukeren forlater organisasjonen.",
"userSelect": "Velg bruker",
"usersNotFound": "Ingen brukere funnet",
"expireIn": "Utløper om",
"neverExpire": "Utløper aldri",
"sharePersistSession": "Behold sesjonen etter første bruk",
"sharePersistSessionDescription": "Når den er aktivert, setter den første forespørselen med dette tokenet via en spørringsparameter eller et hode en sesjons-cookie slik at senere forespørsler ikke trenger tokenet. La det være av for API-klienter som skal sende tokenet ved hver forespørsel.",
"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.",
"shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.",
@@ -449,8 +455,14 @@
"provisioningManage": "Levering",
"provisioningDescription": "Administrer foreløpig nøkler og gjennomgå ventende nettsteder som venter på godkjenning.",
"pendingSites": "Ventende nettsteder",
"siteApproveSuccess": "Vellykket godkjenning av nettsted",
"siteApproveSuccess": "Område og tilknyttede ressurser godkjent",
"siteApproveError": "Feil ved godkjenning av side",
"siteReject": "Avvis Område",
"siteQuestionReject": "Er du sikker på at du vil avvise dette området?",
"siteMessageReject": "Dette vil permanent slette området og eventuelle tilknyttede ressurser som fortsatt venter.",
"siteConfirmReject": "Bekreft Avvisning av Område",
"siteRejectSuccess": "Område avvist",
"siteRejectError": "Feil ved avvisning av området",
"provisioningKeys": "Foreløpig nøkler",
"searchProvisioningKeys": "Søk varer i lagrings nøkler...",
"provisioningKeysAdd": "Generer fremvisende nøkkel",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Lagre den midlertidig nøkkelen",
"provisioningKeysSaveDescription": "Du kan bare se denne én gang. Kopier det til et sikkert sted.",
"provisioningKeysErrorCreate": "Feil under oppretting av foreløpig nøkkel",
"provisioningKeysList": "Ny provisorisk nøkkel",
"provisioningKeysMaxBatchSize": "Maks størrelse på bunt",
"provisioningKeysList": "Ny Forsyningsnøkkel",
"provisioningKeysMaxBatchSize": "Maks Batch Størrelse",
"provisioningKeysUnlimitedBatchSize": "Ubegrenset mengde bunt (ingen begrensning)",
"provisioningKeysMaxBatchUnlimited": "Ubegrenset",
"provisioningKeysMaxBatchSizeInvalid": "Angi en gyldig sjakkstørrelse (11 000.000).",
"provisioningKeysValidUntil": "Gyldig til",
"provisioningKeysValidUntil": "Gyldig Til",
"provisioningKeysValidUntilHint": "La stå tomt for ingen utløp.",
"provisioningKeysValidUntilInvalid": "Angi en gyldig dato og klokkeslett.",
"provisioningKeysNumUsed": "Antall ganger brukt",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Aldri",
"provisioningKeysEdit": "Rediger bestemmelsesnøkkel",
"provisioningKeysEditDescription": "Oppdater maksimal størrelse for bunt og utløpstid for denne nøkkelen.",
"provisioningKeysApproveNewSites": "Godkjenn nye nettsteder",
"provisioningKeysApproveNewSites": "Godkjenn Nye Områder",
"provisioningKeysApproveNewSitesDescription": "Godkjenn automatisk nettsteder som registrerer deg med denne nøkkelen.",
"provisioningKeysUpdateError": "Feil under oppdatering av foreløpig nøkkel",
"provisioningKeysUpdated": "Foreslå nøkkel oppdatert",
@@ -922,7 +934,7 @@
"newtVersion": "Versjon",
"architecture": "Arkitektur",
"sites": "Områder",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard-klient for å koble til. Du må adressere private ressurser ved å bruke peer-IP.",
"siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter",
"siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd",
"userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Opprett Org bruker",
"actionUpdateOrg": "Oppdater organisasjon",
"actionRemoveInvitation": "Fjern invitasjon",
"actionRemoveUserRole": "Fjern Brukerrolle",
"actionUpdateUser": "Oppdater bruker",
"actionGetUser": "Hent bruker",
"actionGetOrgUser": "Hent organisasjonsbruker",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig",
"actionUpdateSite": "Oppdater område",
"actionApproveSite": "Godkjenn Område",
"actionRejectSite": "Avvis Område",
"actionResetSiteBandwidth": "Tilbakestill organisasjons-båndbredde",
"actionListSiteRoles": "List opp tillatte områderoller",
"actionCreateResource": "Opprett ressurs",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Liste invitasjoner",
"actionExportLogs": "Eksportlogger",
"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",
"orgNotFound2": "Ingen organisasjoner funnet.",
"search": "Søk…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Skriv inn koden fra autentiseringsappen din eller en av dine engangs reservekoder.",
"otpAuthSubmit": "Send inn kode",
"idpContinue": "Eller fortsett med",
"idpLastUsed": "Sist brukt",
"otpAuthBack": "Tilbake til passord",
"navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Underdomene: {subdomain}",
"domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mer",
"domainPickerNoDomainsAvailableTitle": "Ingen domener tilgjengelig",
"domainPickerNoDomainsAvailableDescription": "Du har ikke satt opp noen domener ennå. Opprett et domene for å fortsette.",
"domainPickerNoDomainsAvailableAction": "Gå til domener",
"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.",
"regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Offentlig",
"resourcesTableClientResources": "Privat",
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.",
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
"resourcesTableNoInternalResourcesFound": "Ingen private ressurser funnet.",
"resourcesTableDestination": "Destinasjon",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias adresse",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Avbryt",
"editInternalResourceDialogSaveResource": "Lagre ressurs",
"editInternalResourceDialogSuccess": "Suksess",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressurs oppdatert vellykket",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Privat ressurs oppdatert vellykket",
"editInternalResourceDialogError": "Feil",
"editInternalResourceDialogFailedToUpdateInternalResource": "Mislyktes å oppdatere intern ressurs",
"editInternalResourceDialogFailedToUpdateInternalResource": "Feil ved oppdatering av privat ressurs",
"editInternalResourceDialogNameRequired": "Navn er påkrevd",
"editInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
"editInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
"createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder",
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et konfigureret delnett for å lage interne ressurser.",
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et subnett konfigurert for å opprette private ressurser.",
"createInternalResourceDialogClose": "Lukk",
"createInternalResourceDialogCreateClientResource": "Opprett privat ressurs",
"createInternalResourceDialogCreateClientResourceDescription": "Opprett en ny ressurs som bare vil være tilgjengelig for kunder som er koblet til organisasjonen",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Avbryt",
"createInternalResourceDialogCreateResource": "Opprett ressurs",
"createInternalResourceDialogSuccess": "Suksess",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressurs opprettet vellykket",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Privat ressurs opprettet vellykket",
"createInternalResourceDialogError": "Feil",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette intern ressurs",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette privat ressurs",
"createInternalResourceDialogNameRequired": "Navn er påkrevd",
"createInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
"createInternalResourceDialogPleaseSelectSite": "Vennligst velg et område",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)",
"duration": "Varighet",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrevd for å bruke denne funksjonen. <bookADemoLink>Bestill en demo eller POC prøveversjon</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er nødvendig for å bruke denne funksjonen. <bookADemoLink>Bestill en gratis demo eller POC prøve for å lære mer.</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 gratis demo eller POC prøve for å lære mer.</bookADemoLink>",
"certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Opplastningen mislyktes",
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
"sessionToolbarShow": "Vis verktøylinje",
"sessionToolbarHide": "Skjul verktøylinje"
"sessionToolbarHide": "Skjul verktøylinje",
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger"
}
+80 -56
View File
@@ -13,7 +13,7 @@
"orgDisplayName": "Dit is de weergavenaam van de organisatie.",
"orgId": "Organisatie ID",
"setupIdentifierMessage": "Dit is de unieke identificatie voor de organisatie.",
"setupErrorIdentifier": "Organisatie-ID is al in gebruik. Kies een andere.",
"setupErrorIdentifier": "Organisatie ID is al in gebruik, verzin iets anders.",
"componentsErrorNoMemberCreate": "U bent momenteel geen lid van een organisatie. Maak een organisatie aan om aan de slag te gaan.",
"componentsErrorNoMember": "U bent momenteel geen lid van een organisatie.",
"welcome": "Welkom bij Pangolin!",
@@ -78,7 +78,7 @@
"siteManageSites": "Sites beheren",
"siteDescription": "Maak en beheer sites om verbinding met privénetwerken in te schakelen",
"sitesBannerTitle": "Verbind elk netwerk",
"sitesBannerDescription": "Een site is een verbinding met een extern netwerk waarmee Pangolin toegang biedt tot bronnen, zowel openbaar als privé, aan gebruikers overal. Installeer de sitedatacenterconnector (Newt) overal waar je een binaire of container kunt uitvoeren om de verbinding tot stand te brengen.",
"sitesBannerDescription": "Een site is een verbinding met een extern netwerk waarmee Pangolin toegang biedt voor gebruikers voor bronnen, zowel openbaar als privé. Installeer de sitedatacenterconnector (Newt) overal waar je een binaire of container kunt uitvoeren om de verbinding tot stand te brengen.",
"sitesBannerButtonText": "Site installeren",
"approvalsBannerTitle": "Toegang tot het apparaat goedkeuren of weigeren",
"approvalsBannerDescription": "Bekijk en keur toestelverzoeken goed of weiger toegang van gebruikers. Wanneer apparaatgoedkeuringen vereist zijn, moeten gebruikers de goedkeuring van beheerders krijgen voordat hun apparaten verbinding kunnen maken met de bronnen van uw organisatie.",
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Bevestig Verwijdering Deelbare Link",
"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.",
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.",
"shareTokenDescription": "Het toegangstoken kan als queryparameter of in aanvraagheaders worden meegegeven. Standaard moet het bij elke aanvraag worden verzonden. Als sessie-persistentie is ingeschakeld, ruilt de eerste aanvraag deze in voor een sessiecookie.",
"accessToken": "Toegangs-token",
"usageExamples": "Voorbeelden van gebruik",
"tokenId": "Token ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Titel (optioneel)",
"sharePathOptional": "Pad (optioneel)",
"sharePathDescription": "De link zal gebruikers naar dit pad doorsturen na authenticatie.",
"shareAssociateUserOptional": "Gebruiker koppelen (optioneel)",
"shareAssociateUserDescription": "Wanneer ingesteld, worden aanvragen met deze link toegeschreven aan de gebruiker in toegangslogboeken en identiteitsheaders. De link wordt verwijderd als de gebruiker de organisatie verlaat.",
"userSelect": "Selecteer gebruiker",
"usersNotFound": "Geen gebruikers gevonden",
"expireIn": "Vervalt in",
"neverExpire": "Nooit verlopen",
"sharePersistSession": "Sessie behouden na eerste gebruik",
"sharePersistSessionDescription": "Wanneer ingeschakeld, zet de eerste aanvraag met dit token via een queryparameter of header een sessiecookie zodat latere aanvragen het token niet nodig hebben. Laat dit uit voor API-clients die het token bij elke aanvraag moeten verzenden.",
"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.",
"shareAccessHint": "Iedereen met deze link heeft toegang tot de bron. Deel deze met zorg.",
@@ -219,12 +225,12 @@
"shareErrorSelectResource": "Selecteer een bron",
"proxyResourceTitle": "Openbare bronnen beheren",
"proxyResourceDescription": "Creëer en beheer bronnen die openbaar toegankelijk zijn via een webbrowser",
"publicResourcesBannerTitle": "Web-gebaseerde Openbare Toegang",
"publicResourcesBannerDescription": "Openbare bronnen zijn HTTPS-proxies die toegankelijk zijn voor iedereen op het internet via een webbrowser. In tegenstelling tot privébronnen hoeven ze geen client-software te hebben en kunnen ze identiteit- en context bewuste toegangsmiddelen bevatten.",
"publicResourcesBannerTitle": "Webgebaseerde openbare bronnen",
"publicResourcesBannerDescription": "Openbare bronnen zijn HTTPS proxy's die te bereiken zijn voor iedereen op het internet via een webbrowser. Het is mogelijk om deze te beveiligen door identiteitscontrole of een toegangsbeleid. In tegenstelling tot privébronnen hoeven ze geen clientsoftware te installeren.",
"clientResourceTitle": "Privébronnen beheren",
"clientResourceDescription": "Creëer en beheer bronnen die alleen toegankelijk zijn via een verbonden client",
"privateResourcesBannerTitle": "Zero-Trust Private Access",
"privateResourcesBannerDescription": "Privé bronnen maken gebruik van zero-trust-beveiliging, wat ervoor zorgt dat gebruikers en machines alleen toegang kunnen krijgen tot middelen die jij specifiek toestaat. Verbind gebruikersapparaten of machineclients om deze middelen te benaderen via een veilig virtueel privnetwerk.",
"privateResourcesBannerTitle": "Privébronnen via Zero Trust",
"privateResourcesBannerDescription": "Privébronnen maken gebruik van zero trust beveiliging, wat ervoor zorgt dat gebruikers en machines alleen toegang kunnen krijgen tot resources die je specifiek toestaat. Verbind gebruikersapparaten of machines om deze middelen te benaderen via een veilig, virtueel privénetwerk.",
"resourcesSearch": "Zoek bronnen...",
"resourceAdd": "Bron toevoegen",
"resourceErrorDelte": "Fout bij verwijderen document",
@@ -253,7 +259,7 @@
"resourcePolicyUsersRolesDescription": "Bepaal welke gebruikers en rollen geassocieerde bronnen kunnen bezoeken",
"rulesResourcePolicyDescription": "Stel regels in om toegang te regelen tot bronnen die zijn gekoppeld aan dit beleid",
"authentication": "Authenticatie",
"protected": "Beschermd",
"protected": "Beveiligd",
"notProtected": "Niet beveiligd",
"resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.",
"resourceQuestionRemove": "Weet u zeker dat u het document van de organisatie wilt verwijderen?",
@@ -324,7 +330,7 @@
"labelEdit": "Label bewerken",
"editLabelDialogTitle": "Label bijwerken",
"editLabelDialogDescription": "Bewerk een nieuw label dat aan deze organisatie kan worden gekoppeld",
"labelDeleteConfirm": "Bevestigen Verwijderen Label",
"labelDeleteConfirm": "Label verwijderen",
"labelErrorDelete": "Kan label niet verwijderen",
"labelMessageRemove": "Deze handeling is definitief. Alle sites, bronnen en klanten met dit label zullen worden onttakeld.",
"labelQuestionRemove": "Weet u zeker dat u het label uit de organisatie wilt verwijderen?",
@@ -409,48 +415,54 @@
"years": "Jaar",
"day": "{count, plural, one {# dag} other {# dagen}}",
"apiKeysTitle": "API Key Informatie",
"apiKeysConfirmCopy2": "Bevestig dat u de API-sleutel hebt gekopieerd.",
"apiKeysErrorCreate": "Fout bij maken API-sleutel",
"apiKeysConfirmCopy2": "Bevestig dat u de API sleutel hebt gekopieerd.",
"apiKeysErrorCreate": "Fout bij maken API sleutel",
"apiKeysErrorSetPermission": "Fout instellen permissies",
"apiKeysCreate": "API-sleutel genereren",
"apiKeysCreateDescription": "Een nieuwe API-sleutel voor de organisatie genereren",
"apiKeysCreate": "API sleutel genereren",
"apiKeysCreateDescription": "Een nieuwe API sleutel voor de organisatie genereren",
"apiKeysGeneralSettings": "Machtigingen",
"apiKeysGeneralSettingsDescription": "Bepaal wat deze API-sleutel kan doen",
"apiKeysList": "Nieuwe API-sleutel",
"apiKeysSave": "De API-sleutel opslaan",
"apiKeysGeneralSettingsDescription": "Bepaal wat deze API sleutel kan doen",
"apiKeysList": "Nieuwe API sleutel",
"apiKeysSave": "De API sleutel opslaan",
"apiKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plek.",
"apiKeysInfo": "De API-sleutel is:",
"apiKeysConfirmCopy": "Ik heb de API-sleutel gekopieerd",
"apiKeysInfo": "De API sleutel is:",
"apiKeysConfirmCopy": "Ik heb de API sleutel gekopieerd",
"generate": "Genereren",
"done": "Voltooid",
"apiKeysSeeAll": "Alle API-sleutels bekijken",
"apiKeysSeeAll": "Alle API sleutels bekijken",
"apiKeysPermissionsErrorLoadingActions": "Fout bij het laden van API key acties",
"apiKeysPermissionsErrorUpdate": "Fout instellen permissies",
"apiKeysPermissionsUpdated": "Permissies bijgewerkt",
"apiKeysPermissionsUpdatedDescription": "De bevoegdheden zijn bijgewerkt.",
"apiKeysPermissionsGeneralSettings": "Machtigingen",
"apiKeysPermissionsGeneralSettingsDescription": "Bepaal wat deze API-sleutel kan doen",
"apiKeysPermissionsGeneralSettingsDescription": "Bepaal wat deze API sleutel kan doen",
"apiKeysPermissionsSave": "Rechten opslaan",
"apiKeysPermissionsTitle": "Machtigingen",
"apiKeys": "API sleutels",
"searchApiKeys": "API-sleutels zoeken...",
"apiKeysAdd": "API-sleutel genereren",
"apiKeysErrorDelete": "Fout bij verwijderen API-sleutel",
"apiKeysErrorDeleteMessage": "Fout bij verwijderen API-sleutel",
"apiKeysQuestionRemove": "Weet u zeker dat u de API-sleutel van de organisatie wilt verwijderen?",
"apiKeysMessageRemove": "Eenmaal verwijderd, kan de API-sleutel niet meer worden gebruikt.",
"apiKeysDeleteConfirm": "Bevestig Verwijderen API-sleutel",
"apiKeysDelete": "API-sleutel verwijderen",
"apiKeysManage": "API-sleutels beheren",
"apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API",
"searchApiKeys": "API sleutels zoeken...",
"apiKeysAdd": "API sleutel genereren",
"apiKeysErrorDelete": "Fout bij verwijderen API sleutel",
"apiKeysErrorDeleteMessage": "Fout bij verwijderen API sleutel",
"apiKeysQuestionRemove": "Weet u zeker dat u de API sleutel van de organisatie wilt verwijderen?",
"apiKeysMessageRemove": "Eenmaal verwijderd, kan de API sleutel niet meer worden gebruikt.",
"apiKeysDeleteConfirm": "Bevestig Verwijderen API sleutel",
"apiKeysDelete": "API sleutel verwijderen",
"apiKeysManage": "API sleutels beheren",
"apiKeysDescription": "API sleutels worden gebruikt om toegang te verifiëren met de integratie API ",
"provisioningKeysTitle": "Vertrekkende sleutel",
"provisioningKeysManage": "Beheren van Provisioning Sleutels",
"provisioningKeysDescription": "Provisionerende sleutels worden gebruikt om geautomatiseerde sitebepaling voor uw organisatie te verifiëren.",
"provisioningManage": "Provisie",
"provisioningDescription": "Voorzieningssleutels beheren en sites beoordelen in afwachting van goedkeuring.",
"pendingSites": "Openstaande sites",
"siteApproveSuccess": "Site succesvol goedgekeurd",
"siteApproveSuccess": "Site en bijbehorende middelen succesvol goedgekeurd",
"siteApproveError": "Fout bij goedkeuren website",
"siteReject": "Afwijzen Site",
"siteQuestionReject": "Weet u zeker dat u deze site wilt afwijzen?",
"siteMessageReject": "Hiermee wordt de site en alle bijbehorende middelen die nog in behandeling zijn permanent verwijderd.",
"siteConfirmReject": "Afwijzing van site bevestigen",
"siteRejectSuccess": "Site succesvol afgewezen",
"siteRejectError": "Fout bij het afwijzen van site",
"provisioningKeys": "Verhelderende sleutels",
"searchProvisioningKeys": "Zoek provisioningsleutels ...",
"provisioningKeysAdd": "Genereer Provisioning Sleutel",
@@ -466,7 +478,7 @@
"provisioningKeysSave": "Sla de provisioning sleutel op",
"provisioningKeysSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een veilige plaats.",
"provisioningKeysErrorCreate": "Fout bij aanmaken provisioning sleutel",
"provisioningKeysList": "Nieuwe provisioning sleutel",
"provisioningKeysList": "Nieuwe provisioning-sleutel",
"provisioningKeysMaxBatchSize": "Maximale batchgrootte",
"provisioningKeysUnlimitedBatchSize": "Onbeperkte batchgrootte (geen limiet)",
"provisioningKeysMaxBatchUnlimited": "Onbeperkt",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Nooit",
"provisioningKeysEdit": "Wijzig Provisioning Sleutel",
"provisioningKeysEditDescription": "Werk de maximale batchgrootte en verlooptijd voor deze sleutel bij.",
"provisioningKeysApproveNewSites": "Goedkeuren van nieuwe sites",
"provisioningKeysApproveNewSites": "Nieuwe sites goedkeuren",
"provisioningKeysApproveNewSitesDescription": "Automatisch sites goedkeuren die zich registreren met deze sleutel.",
"provisioningKeysUpdateError": "Fout tijdens bijwerken provisioning sleutel",
"provisioningKeysUpdated": "Provisie sleutel bijgewerkt",
@@ -900,7 +912,7 @@
"resourcesUsersRolesAccess": "Gebruiker en rol-gebaseerde toegangsbeheer",
"resourcesErrorUpdate": "Bron wisselen mislukt",
"resourcesErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document",
"access": "Toegangsrechten",
"access": "Link",
"accessControl": "Toegangs controle",
"shareLink": "{resource} Deelbare Link",
"resourceSelect": "Selecteer resource",
@@ -922,7 +934,7 @@
"newtVersion": "Versie",
"architecture": "Architectuur",
"sites": "Sites",
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je zult interne bronnen moeten aanspreken met behulp van de peer IP.",
"siteWgAnyClients": "Gebruik een willekeurige WireGuard-client om verbinding te maken. U moet privébronnen benaderen met de peer IP.",
"siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients",
"siteWgManualConfigurationRequired": "Handmatige configuratie vereist",
"userErrorNotAdminOrOwner": "Gebruiker is geen beheerder of eigenaar",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Org gebruiker aanmaken",
"actionUpdateOrg": "Organisatie bijwerken",
"actionRemoveInvitation": "Verwijder uitnodiging",
"actionRemoveUserRole": "Gebruikersrol verwijderen",
"actionUpdateUser": "Gebruiker bijwerken",
"actionGetUser": "Gebruiker ophalen",
"actionGetOrgUser": "Krijg organisatie-gebruiker",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist",
"actionUpdateSite": "Site bijwerken",
"actionApproveSite": "Verleen site goedkeuring",
"actionRejectSite": "Afwijzen Site",
"actionResetSiteBandwidth": "Reset organisatieschandbreedte",
"actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken",
@@ -1471,11 +1486,11 @@
"actionCheckOrgId": "ID controleren",
"actionCreateOrg": "Nieuwe organisatie aanmaken",
"actionDeleteOrg": "Verwijder organisatie",
"actionListApiKeys": "API-sleutels weergeven",
"actionListApiKeys": "API sleutels weergeven",
"actionListApiKeyActions": "Lijst van API Key Acties",
"actionSetApiKeyActions": "Stel API Key Toegestane Acties",
"actionCreateApiKey": "API-sleutel aanmaken",
"actionDeleteApiKey": "API-sleutel verwijderen",
"actionCreateApiKey": "API sleutel aanmaken",
"actionDeleteApiKey": "API sleutel verwijderen",
"actionCreateIdp": "IDP aanmaken",
"actionUpdateIdp": "IDP bijwerken",
"actionDeleteIdp": "Verwijder IDP",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Toon uitnodigingen",
"actionExportLogs": "Logboeken exporteren",
"actionViewLogs": "Logboeken bekijken",
"actionCreateSiteProvisioningKey": "Een sitevoorzie-ningssleutel aanmaken",
"actionListSiteProvisioningKeys": "Sitevoorzie-ningssleutels weergeven",
"actionUpdateSiteProvisioningKey": "Sitevoorzie-ningssleutel bijwerken",
"actionDeleteSiteProvisioningKey": "Sitevoorzie-ningssleutel verwijderen",
"noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.",
"search": "Zoeken…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Voer de code van je authenticator-app of een van je reservekopiecodes voor het eenmalig gebruik in.",
"otpAuthSubmit": "Code indienen",
"idpContinue": "Of ga verder met",
"idpLastUsed": "Laatst gebruikt",
"otpAuthBack": "Terug naar wachtwoord",
"navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
@@ -1536,7 +1556,7 @@
"commandPaletteCreateProxyResource": "Maak Openbare Bron",
"commandPaletteCreatePrivateResource": "Maak Privébron",
"commandPaletteCreateUser": "Gebruiker aanmaken",
"commandPaletteCreateApiKey": "API-sleutel aanmaken",
"commandPaletteCreateApiKey": "API sleutel aanmaken",
"commandPaletteCreateMachineClient": "Machineclient aanmaken",
"commandPaletteCreateAlertRule": "Alarmregel aanmaken",
"commandPaletteCreateIdentityProvider": "Identiteitsprovider aanmaken",
@@ -1584,10 +1604,10 @@
"authErrorNoUpdate": "Geen authenticatie informatie om bij te werken",
"orgErrorNoUpdate": "Geen org om bij te werken",
"orgErrorNoProvided": "Geen org opgegeven",
"apiKeysErrorNoUpdate": "Geen API-sleutel om bij te werken",
"apiKeysErrorNoUpdate": "Geen API sleutel om bij te werken",
"sidebarOverview": "Overzicht.",
"sidebarHome": "Startpagina",
"sidebarSites": "Werkruimtes",
"sidebarSites": "Sites",
"sidebarApprovals": "Goedkeuringsverzoeken",
"sidebarResources": "Bronnen",
"sidebarProxyResources": "Openbaar",
@@ -1657,8 +1677,8 @@
"commandBilling": "Facturering",
"commandEnterpriseLicenses": "Licentie",
"commandSettings": "Instellingen",
"commandLauncher": "Startprogramma",
"commandResourceLauncher": "Bron Startprogramma",
"commandLauncher": "Startpagina",
"commandResourceLauncher": "Bron startpagina",
"commandSearchResults": "Zoekresultaten",
"alertingTitle": "Waarschuwingen",
"alertingDescription": "Definieer bronnen, triggers en acties voor meldingen",
@@ -1816,7 +1836,7 @@
"standaloneHcFilterSiteIdFallback": "Site {id}",
"standaloneHcFilterResourceIdFallback": "Bron {id}",
"blueprints": "Blauwdrukken",
"blueprintsLog": "Log Blueprints",
"blueprintsLog": "Log blauwafdrukken",
"blueprintsDescription": "Bekijk eerdere blauwdruktoepassingen en hun resultaten of pas een nieuwe blauwdruk toe",
"blueprintAdd": "Blauwdruk toevoegen",
"blueprintGoBack": "Bekijk alle Blauwdrukken",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdomein: {subdomain}",
"domainPickerNamespace": "Naamruimte: {namespace}",
"domainPickerShowMore": "Meer weergeven",
"domainPickerNoDomainsAvailableTitle": "Geen domeinen beschikbaar",
"domainPickerNoDomainsAvailableDescription": "Je hebt nog geen domeinen ingesteld. Maak een domein aan om door te gaan.",
"domainPickerNoDomainsAvailableAction": "Ga naar Domeinen",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Openbaar",
"resourcesTableClientResources": "Privé",
"resourcesTableNoProxyResourcesFound": "Geen proxybronnen gevonden.",
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
"resourcesTableNoInternalResourcesFound": "Geen privébronnen gevonden.",
"resourcesTableDestination": "Bestemming",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias adres",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Annuleren",
"editInternalResourceDialogSaveResource": "Sla bron op",
"editInternalResourceDialogSuccess": "Succes",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Interne bron succesvol bijgewerkt",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Privébron succesvol bijgewerkt",
"editInternalResourceDialogError": "Fout",
"editInternalResourceDialogFailedToUpdateInternalResource": "Het bijwerken van de interne bron is mislukt",
"editInternalResourceDialogFailedToUpdateInternalResource": "Fout bij het bijwerken van de privébron",
"editInternalResourceDialogNameRequired": "Naam is verplicht",
"editInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens",
"editInternalResourceDialogProxyPortMin": "Proxy poort moet minstens 1 zijn",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Een optionele interne DNS-alias voor dit document.",
"createInternalResourceDialogNoSitesAvailable": "Geen sites beschikbaar",
"createInternalResourceDialogNoSitesAvailableDescription": "U moet ten minste één Newt-site hebben met een geconfigureerd subnet om interne bronnen aan te maken.",
"createInternalResourceDialogNoSitesAvailableDescription": "U moet ten minste één Newt-site hebben met een geconfigureerd subnet om privébronnen aan te maken.",
"createInternalResourceDialogClose": "Sluiten",
"createInternalResourceDialogCreateClientResource": "Privé bron maken",
"createInternalResourceDialogCreateClientResourceDescription": "Maak een nieuwe bron aan die alleen toegankelijk is voor klanten die verbonden zijn met de organisatie",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Annuleren",
"createInternalResourceDialogCreateResource": "Bron aanmaken",
"createInternalResourceDialogSuccess": "Succes",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Interne bron succesvol aangemaakt",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Privébron succesvol aangemaakt",
"createInternalResourceDialogError": "Fout",
"createInternalResourceDialogFailedToCreateInternalResource": "Het aanmaken van de interne bron is mislukt",
"createInternalResourceDialogFailedToCreateInternalResource": "Fout bij het aanmaken van de privébron",
"createInternalResourceDialogNameRequired": "Naam is verplicht",
"createInternalResourceDialogNameMaxLength": "Naam mag niet langer zijn dan 255 tekens",
"createInternalResourceDialogPleaseSelectSite": "Selecteer alstublieft een site",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Bron adres",
"destinationAddress": "Adres bestemming",
"duration": "Duur",
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.",
"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>.",
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een gratis demo of POC trial om meer te leren.</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 gratis demo of POC trial om meer te leren.</bookADemoLink>",
"certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren",
@@ -3297,7 +3320,7 @@
"internalResourceAuthDaemonPort": "Daemon poort (optioneel)",
"orgAuthWhatsThis": "Waar kan ik mijn organisatie-ID vinden?",
"learnMore": "Meer informatie",
"backToHome": "Ga terug naar startpagina",
"backToHome": "Ga terug naar de startpagina",
"needToSignInToOrg": "Moet u de identiteit provider van uw organisatie gebruiken?",
"maintenanceMode": "Onderhoudspagina",
"maintenanceModeDescription": "Toon een onderhoudspagina aan bezoekers",
@@ -3510,7 +3533,7 @@
"httpDestAuthNoneDescription": "Stuurt verzoeken zonder toestemmingskop.",
"httpDestAuthBearerTitle": "Betere Token",
"httpDestAuthBearerDescription": "Voegt een Authorization: Bearer '<token>' header toe aan elk verzoek.",
"httpDestAuthBearerPlaceholder": "Uw API-sleutel of -token",
"httpDestAuthBearerPlaceholder": "Uw API sleutel of token",
"httpDestAuthBasicTitle": "Basis authenticatie",
"httpDestAuthBasicDescription": "Voegt een Authorization: Basic '<credentials>' header toe. Verstrek inloggegevens als gebruikersnaam:wachtwoord.",
"httpDestAuthBasicPlaceholder": "Gebruikersnaam:wachtwoord",
@@ -3664,7 +3687,7 @@
"memberPortalResourceDisabled": "Bron Uitgeschakeld",
"memberPortalShowingResources": "Toont {start}-{end} van {total} bronnen",
"resourceLauncherTitle": "Bron Launcher",
"resourceSidebarLauncherTitle": "Startprogramma",
"resourceSidebarLauncherTitle": "Startpagina",
"resourceLauncherDescription": "Bekijk alle beschikbare bronnen en start ze vanuit één centraal punt",
"resourceLauncherSearchPlaceholder": "Zoek naar bronnen...",
"resourceLauncherDefaultView": "Standaard",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Upload mislukt",
"rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus",
"sessionToolbarShow": "Toon werkbalk",
"sessionToolbarHide": "Verberg werkbalk"
"sessionToolbarHide": "Verberg werkbalk",
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken"
}
+37 -13
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Potwierdź usunięcie linku do udostępnienia",
"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.",
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.",
"shareTokenDescription": "Token dostępu można przekaz jako parametr zapytania lub w nagłówkach żądania. Domyślnie musi być wysyłany w każdym żądaniu. Jeśli trwałość sesji jest włączona, pierwsze żądanie wymienia go na ciasteczko sesji.",
"accessToken": "Token dostępu",
"usageExamples": "Przykłady użycia",
"tokenId": "Identyfikator tokena",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Tytuł (opcjonalnie)",
"sharePathOptional": "Ścieżka (opcjonalnie)",
"sharePathDescription": "Link przekieruje użytkowników do tej ścieżki po uwierzytelnieniu.",
"shareAssociateUserOptional": "Powiąż użytkownika (opcjonalnie)",
"shareAssociateUserDescription": "Po ustawieniu, żądania korzystające z tego linku są przypisywane do użytkownika w logach dostępu i nagłówkach tożsamości. Link jest usuwany, jeśli użytkownik opuszcza organizację.",
"userSelect": "Wybierz użytkownika",
"usersNotFound": "Nie znaleziono użytkowników",
"expireIn": "Wygasa za",
"neverExpire": "Nigdy nie wygasa",
"sharePersistSession": "Utrzymaj sesję po pierwszym użyciu",
"sharePersistSessionDescription": "Gdy ta opcja jest włączona, pierwsze żądanie z tym tokenem przez parametr zapytania lub nagłówek ustawia ciasteczko sesji, dzięki czemu późniejsze żądania nie wymagają tokena. Pomijaj dla klienta API, który powinien wysyłać token w każdym żądaniu.",
"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ć.",
"shareAccessHint": "Każdy z tym linkiem może uzyskać dostęp do zasobu. Podziel się nim ostrożnie.",
@@ -449,8 +455,14 @@
"provisioningManage": "Dostarczanie",
"provisioningDescription": "Zarządzaj kluczami rezerwacji i sprawdzaj oczekujące strony oczekujące na zatwierdzenie.",
"pendingSites": "Witryny oczekujące",
"siteApproveSuccess": "Witryna została pomyślnie zatwierdzona",
"siteApproveSuccess": "Witryna i powiązane zasoby zostały zatwierdzone pomyślnie",
"siteApproveError": "Błąd zatwierdzania witryny",
"siteReject": "Odrzuć witrynę",
"siteQuestionReject": "Czy na pewno chcesz odrzucić tę witrynę?",
"siteMessageReject": "Spowoduje to trwałe usunięcie witryny i wszelkich powiązanych, nadal oczekujących zasobów.",
"siteConfirmReject": "Potwierdź odrzucenie witryny",
"siteRejectSuccess": "Witryna została odrzucona pomyślnie",
"siteRejectError": "Błąd podczas odrzucania witryny",
"provisioningKeys": "Klucze Zaopatrzenia",
"searchProvisioningKeys": "Szukaj kluczy zaopatrzenia...",
"provisioningKeysAdd": "Wygeneruj klucz zaopatrzenia",
@@ -466,7 +478,7 @@
"provisioningKeysSave": "Zapisz klucz zaopatrzenia",
"provisioningKeysSaveDescription": "Możesz to zobaczyć tylko raz. Skopiuj je do bezpiecznego miejsca.",
"provisioningKeysErrorCreate": "Błąd podczas tworzenia klucza zaopatrzenia",
"provisioningKeysList": "Nowy klucz rezerwacji",
"provisioningKeysList": "Nowy klucz aprowizacyjny",
"provisioningKeysMaxBatchSize": "Maksymalny rozmiar partii",
"provisioningKeysUnlimitedBatchSize": "Nieograniczony rozmiar partii (bez limitu)",
"provisioningKeysMaxBatchUnlimited": "Nieograniczona",
@@ -922,7 +934,7 @@
"newtVersion": "Wersja",
"architecture": "Architektura",
"sites": "Witryny",
"siteWgAnyClients": "Użyj dowolnego klienta WireGuard, aby się połącz. Będziesz musiał przekierować wewnętrzne zasoby za pomocą adresu IP.",
"siteWgAnyClients": "Użyj dowolnego klienta WireGuard do połączenia. Będziesz musiał adresować prywatne zasoby używając IP współpracownika.",
"siteWgCompatibleAllClients": "Kompatybilny ze wszystkimi klientami WireGuard",
"siteWgManualConfigurationRequired": "Wymagana konfiguracja ręczna",
"userErrorNotAdminOrOwner": "Użytkownik nie jest administratorem ani właścicielem",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Utwórz użytkownika Org",
"actionUpdateOrg": "Aktualizuj organizację",
"actionRemoveInvitation": "Usuń zaproszenie",
"actionRemoveUserRole": "Usuń rolę użytkownika",
"actionUpdateUser": "Zaktualizuj użytkownika",
"actionGetUser": "Pobierz użytkownika",
"actionGetOrgUser": "Pobierz użytkownika organizacji",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
"setupTokenRequired": "Wymagany jest token konfiguracji",
"actionUpdateSite": "Aktualizuj witrynę",
"actionApproveSite": "Zatwierdź witrynę",
"actionRejectSite": "Odrzuć witrynę",
"actionResetSiteBandwidth": "Zresetuj przepustowość organizacji",
"actionListSiteRoles": "Lista dozwolonych ról witryny",
"actionCreateResource": "Utwórz zasób",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Lista zaproszeń",
"actionExportLogs": "Eksportuj 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",
"orgNotFound2": "Nie znaleziono organizacji.",
"search": "Szukaj…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Wprowadź kod z aplikacji uwierzytelniającej lub jeden z jednorazowych kodów zapasowych.",
"otpAuthSubmit": "Wyślij kod",
"idpContinue": "Lub kontynuuj z",
"idpLastUsed": "Ostatnio używany",
"otpAuthBack": "Powrót do hasła",
"navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdomena: {subdomain}",
"domainPickerNamespace": "Przestrzeń nazw: {namespace}",
"domainPickerShowMore": "Pokaż więcej",
"domainPickerNoDomainsAvailableTitle": "Brak dostępnych domen",
"domainPickerNoDomainsAvailableDescription": "Nie masz jeszcze skonfigurowanych żadnych domen. Utwórz domenę, aby kontynuować.",
"domainPickerNoDomainsAvailableAction": "Przejdź do Domena",
"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.",
"regionSelectorInfo": "Wybór regionu pomaga nam zapewnić lepszą wydajność dla Twojej lokalizacji. Nie musisz być w tym samym regionie co Twój serwer.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Publiczne",
"resourcesTableClientResources": "Prywatny",
"resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.",
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
"resourcesTableNoInternalResourcesFound": "Nie znaleziono prywatnych zasobów.",
"resourcesTableDestination": "Miejsce docelowe",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adres aliasu",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Anuluj",
"editInternalResourceDialogSaveResource": "Zapisz zasób",
"editInternalResourceDialogSuccess": "Sukces",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Wewnętrzny zasób zaktualizowany pomyślnie",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Prywatny zasób został pomyślnie zaktualizowany",
"editInternalResourceDialogError": "Błąd",
"editInternalResourceDialogFailedToUpdateInternalResource": "Nie udało się zaktualizować wewnętrznego zasobu",
"editInternalResourceDialogFailedToUpdateInternalResource": "Nie udało się zaktualizować prywatnego zasobu",
"editInternalResourceDialogNameRequired": "Nazwa jest wymagana",
"editInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków",
"editInternalResourceDialogProxyPortMin": "Port proxy musi wynosić przynajmniej 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
"createInternalResourceDialogNoSitesAvailable": "Brak dostępnych stron",
"createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną stronę Newt z skonfigurowanym podsiecią, aby tworzyć wewnętrzne zasoby.",
"createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną lokalizację Newt z skonfigurowaną podsiecią, aby tworzyć prywatne zasoby.",
"createInternalResourceDialogClose": "Zamknij",
"createInternalResourceDialogCreateClientResource": "Utwórz zasób prywatny",
"createInternalResourceDialogCreateClientResourceDescription": "Utwórz nowy zasób, który będzie dostępny tylko dla klientów podłączonych do organizacji",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Anuluj",
"createInternalResourceDialogCreateResource": "Utwórz zasób",
"createInternalResourceDialogSuccess": "Sukces",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Wewnętrzny zasób utworzony pomyślnie",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Prywatny zasób został pomyślnie utworzony",
"createInternalResourceDialogError": "Błąd",
"createInternalResourceDialogFailedToCreateInternalResource": "Nie udało się utworzyć wewnętrznego zasobu",
"createInternalResourceDialogFailedToCreateInternalResource": "Nie udało się utworzyć prywatnego zasobu",
"createInternalResourceDialogNameRequired": "Nazwa jest wymagana",
"createInternalResourceDialogNameMaxLength": "Nazwa nie może mieć więcej niż 255 znaków",
"createInternalResourceDialogPleaseSelectSite": "Proszę wybrać stronę",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Adres źródłowy",
"destinationAddress": "Adres docelowy",
"duration": "Czas trwania",
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lub <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezerwuj wersję demonstracyjną lub wersję prób POC</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>.",
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lub <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezerwuj darmową wersję demo lub próbę POC, aby dowiedzieć się więcej.</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 darmową wersję demo lub próbę POC, aby dowiedzieć się więcej.</bookADemoLink>",
"certResolver": "Rozwiązywanie certyfikatów",
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
"selectCertResolver": "Wybierz Resolver certyfikatów",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Niepowodzenie przesyłania",
"rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode",
"sessionToolbarShow": "Pokaż pasek narzędzi",
"sessionToolbarHide": "Ukryj pasek narzędzi"
"sessionToolbarHide": "Ukryj pasek narzędzi",
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Confirmar exclusão do Link Compartilhável",
"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.",
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.",
"shareTokenDescription": "O token de acesso pode ser passado como um parâmetro de consulta ou nos cabeçalhos da solicitação. Por padrão, ele deve ser enviado em todas as solicitações. Se a persistência da sessão estiver ativada, a primeira solicitação o troca por um cookie de sessão.",
"accessToken": "Token de acesso",
"usageExamples": "Exemplos de uso",
"tokenId": "ID do Token",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Título (opcional)",
"sharePathOptional": "Caminho (opcional)",
"sharePathDescription": "O link redirecionará os usuários para este caminho após a autenticação.",
"shareAssociateUserOptional": "Associar Usuário (opcional)",
"shareAssociateUserDescription": "Quando definido, as solicitações usando este link são atribuídas ao usuário nos registros de acesso e cabeçalhos de identidade. O link é removido se o usuário sair da organização.",
"userSelect": "Selecionar um usuário",
"usersNotFound": "Nenhum usuário encontrado",
"expireIn": "Expira em",
"neverExpire": "Nunca expirar",
"sharePersistSession": "Persistir sessão após o primeiro uso",
"sharePersistSessionDescription": "Quando ativado, a primeira solicitação com este token por meio de um parâmetro de consulta ou cabeçalho define um cookie de sessão, para que solicitações posteriores não precisem do token. Mantenha desativado para clientes de API que devem enviar o token em todas as solicitações.",
"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.",
"shareAccessHint": "Qualquer um com este link pode aceder o recurso. Compartilhe com cuidado.",
@@ -449,8 +455,14 @@
"provisioningManage": "Provisionamento",
"provisioningDescription": "Gerenciar chaves de provisionamento e revisar sites pendentes aguardando aprovação.",
"pendingSites": "Sites pendentes",
"siteApproveSuccess": "Site aprovado com sucesso",
"siteApproveSuccess": "Site e recursos associados aprovados com sucesso",
"siteApproveError": "Erro ao aprovar site",
"siteReject": "Rejeitar Site",
"siteQuestionReject": "Tem certeza de que deseja rejeitar este site?",
"siteMessageReject": "Isto eliminará permanentemente o site e quaisquer recursos associados que ainda estejam pendentes.",
"siteConfirmReject": "Confirmar Rejeição de Site",
"siteRejectSuccess": "Site rejeitado com sucesso",
"siteRejectError": "Erro ao rejeitar site",
"provisioningKeys": "Posicionando chaves",
"searchProvisioningKeys": "Pesquisar chaves de provisionamento...",
"provisioningKeysAdd": "Gerar chave de provisionamento",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Salvar a chave de provisionamento",
"provisioningKeysSaveDescription": "Você só será capaz de ver esta vez. Copiá-lo para um lugar seguro.",
"provisioningKeysErrorCreate": "Erro ao criar chave de provisionamento",
"provisioningKeysList": "Nova chave de aprovisionamento",
"provisioningKeysMaxBatchSize": "Tamanho máximo do lote",
"provisioningKeysList": "Nova Chave de Provisionamento",
"provisioningKeysMaxBatchSize": "Tamanho Máximo do Lote",
"provisioningKeysUnlimitedBatchSize": "Tamanho ilimitado em lote (sem limite)",
"provisioningKeysMaxBatchUnlimited": "Ilimitado",
"provisioningKeysMaxBatchSizeInvalid": "Informe um tamanho máximo válido em lote (11,000,000).",
"provisioningKeysValidUntil": "Valido ate",
"provisioningKeysValidUntil": "Válido Até",
"provisioningKeysValidUntilHint": "Deixe em branco para nenhuma expiração.",
"provisioningKeysValidUntilInvalid": "Informe uma data e hora válidas.",
"provisioningKeysNumUsed": "Use percentual",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "nunca",
"provisioningKeysEdit": "Editar chave de provisionamento",
"provisioningKeysEditDescription": "Atualizar o tamanho máximo do lote e tempo de expiração para esta chave.",
"provisioningKeysApproveNewSites": "Aprovar novos sites",
"provisioningKeysApproveNewSites": "Aprovar Novos Sites",
"provisioningKeysApproveNewSitesDescription": "Aprovar automaticamente sites que se registram com esta chave.",
"provisioningKeysUpdateError": "Erro ao atualizar chave de provisionamento",
"provisioningKeysUpdated": "Chave de provisionamento atualizada",
@@ -922,7 +934,7 @@
"newtVersion": "Versão",
"architecture": "Arquitetura",
"sites": "sites",
"siteWgAnyClients": "Use qualquer cliente do WireGuard para se conectar. Você terá que endereçar recursos internos usando o IP de pares.",
"siteWgAnyClients": "Use qualquer cliente WireGuard para conectar-se. Você terá que endereçar recursos privados usando o IP do par.",
"siteWgCompatibleAllClients": "Compatível com todos os clientes WireGuard",
"siteWgManualConfigurationRequired": "Configuração manual necessária",
"userErrorNotAdminOrOwner": "Usuário não é administrador ou proprietário",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Criar utilizador Org",
"actionUpdateOrg": "Atualizar Organização",
"actionRemoveInvitation": "Remover Convite",
"actionRemoveUserRole": "Remover Função de Utilizador",
"actionUpdateUser": "Atualizar Usuário",
"actionGetUser": "Obter Usuário",
"actionGetOrgUser": "Obter Utilizador da Organização",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
"setupTokenRequired": "Token de configuração é necessário",
"actionUpdateSite": "Atualizar Site",
"actionApproveSite": "Aprovar Site",
"actionRejectSite": "Rejeitar Site",
"actionResetSiteBandwidth": "Redefinir banda da organização",
"actionListSiteRoles": "Listar Funções Permitidas do Site",
"actionCreateResource": "Criar Recurso",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Listar Convites",
"actionExportLogs": "Exportar logs",
"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",
"orgNotFound2": "Nenhuma organização encontrada.",
"search": "Pesquisar…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Insira o código da sua aplicação de autenticação ou um dos seus códigos de backup de uso único.",
"otpAuthSubmit": "Submeter Código",
"idpContinue": "Ou continuar com",
"idpLastUsed": "Última Utilização",
"otpAuthBack": "Voltar à Palavra-passe",
"navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Subdomínio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostrar Mais",
"domainPickerNoDomainsAvailableTitle": "Nenhum domínio disponível",
"domainPickerNoDomainsAvailableDescription": "Você ainda não configurou nenhum domínio. Crie um domínio para continuar.",
"domainPickerNoDomainsAvailableAction": "Ir para Domínios",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Público",
"resourcesTableClientResources": "Privado",
"resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.",
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
"resourcesTableNoInternalResourcesFound": "Nenhum recurso privado encontrado.",
"resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Endereço do Pseudônimo",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Cancelar",
"editInternalResourceDialogSaveResource": "Guardar Recurso",
"editInternalResourceDialogSuccess": "Sucesso",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso interno atualizado com sucesso",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Recurso privado atualizado com sucesso",
"editInternalResourceDialogError": "Erro",
"editInternalResourceDialogFailedToUpdateInternalResource": "Falha ao atualizar recurso interno",
"editInternalResourceDialogFailedToUpdateInternalResource": "Falha ao atualizar recurso privado",
"editInternalResourceDialogNameRequired": "Nome é obrigatório",
"editInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres",
"editInternalResourceDialogProxyPortMin": "Porta de proxy deve ser pelo menos 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.",
"createInternalResourceDialogNoSitesAvailable": "Nenhum Site Disponível",
"createInternalResourceDialogNoSitesAvailableDescription": "Você precisa ter pelo menos um site Newt com uma sub-rede configurada para criar recursos internos.",
"createInternalResourceDialogNoSitesAvailableDescription": "Você precisa ter pelo menos um site Newt com uma sub-rede configurada para criar recursos privados.",
"createInternalResourceDialogClose": "Fechar",
"createInternalResourceDialogCreateClientResource": "Criar Recurso Privado",
"createInternalResourceDialogCreateClientResourceDescription": "Criar um novo recurso que só será acessível para clientes conectados à organização",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Cancelar",
"createInternalResourceDialogCreateResource": "Criar Recurso",
"createInternalResourceDialogSuccess": "Sucesso",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso interno criado com sucesso",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Recurso privado criado com sucesso",
"createInternalResourceDialogError": "Erro",
"createInternalResourceDialogFailedToCreateInternalResource": "Falha ao criar recurso interno",
"createInternalResourceDialogFailedToCreateInternalResource": "Falha ao criar recurso privado",
"createInternalResourceDialogNameRequired": "Nome é obrigatório",
"createInternalResourceDialogNameMaxLength": "Nome deve ser inferior a 255 caracteres",
"createInternalResourceDialogPleaseSelectSite": "Por favor, selecione um site",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Endereço de origem",
"destinationAddress": "Endereço de destino",
"duration": "Duração",
"licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> é necessária para usar este recurso. <bookADemoLink>Reserve um teste de demonstração ou POC</bookADemoLink>.",
"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>.",
"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 para saber mais.</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 para saber mais.</bookADemoLink>",
"certResolver": "Resolvedor de Certificado",
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
"selectCertResolver": "Selecionar solucionador de certificado",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Falha no upload",
"rdpUnicodeKeyboardMode": "Modo de teclado Unicode",
"sessionToolbarShow": "Mostrar barra de ferramentas",
"sessionToolbarHide": "Ocultar barra de ferramentas"
"sessionToolbarHide": "Ocultar barra de ferramentas",
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site"
}
+38 -14
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Подтвердить удаление общей ссылки",
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
"shareTokenDescription": "Токен доступа может быть передан в виде параметра запроса или в заголовках запроса. По умолчанию, он должен отправляться при каждом запросе. Если включена устойчивость сеанса, первый запрос обменяет его на сеансовый файл cookie.",
"accessToken": "Токен доступа",
"usageExamples": "Примеры использования",
"tokenId": "ID токена",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Заголовок (необязательно)",
"sharePathOptional": "Путь (необязательно)",
"sharePathDescription": "Ссылка перенаправит пользователей на этот путь после аутентификации.",
"shareAssociateUserOptional": "Связать пользователя (необязательно)",
"shareAssociateUserDescription": "При установке, запросы с этой ссылкой записываются за пользователем в журналах доступа и заголовках идентификации. Ссылка удаляется, если пользователь покидает организацию.",
"userSelect": "Выберите пользователя",
"usersNotFound": "Пользователи не найдены",
"expireIn": "Срок действия",
"neverExpire": "Бессрочный доступ",
"sharePersistSession": "Сохранять сеанс после первого использования",
"sharePersistSessionDescription": "При включении, первый запрос с этим токеном через параметр запроса или заголовок устанавливает сеансовый файл cookie, так что последующие запросы не требуют токена. Оставьте для API-клиентов, которые должны отправлять токен при каждом запросе.",
"shareExpireDescription": "Срок действия - это период, в течение которого ссылка будет работать и предоставлять доступ к ресурсу. После этого времени ссылка перестанет работать, и пользователи, использовавшие эту ссылку, потеряют доступ к ресурсу.",
"shareSeeOnce": "Вы сможете увидеть эту ссылку только один раз. Обязательно скопируйте ее.",
"shareAccessHint": "Любой, у кого есть эта ссылка, может получить доступ к ресурсу. Делитесь ею с осторожностью.",
@@ -449,8 +455,14 @@
"provisioningManage": "Подготовка",
"provisioningDescription": "Управляйте предоставленными ключами и проверять непроверенные сайты, ожидающие утверждения.",
"pendingSites": "Ожидающие сайты",
"siteApproveSuccess": "Сайт успешно утвержден",
"siteApproveSuccess": "Сайт и связанные ресурсы успешно одобрены",
"siteApproveError": "Ошибка при утверждении сайта",
"siteReject": "Отклонить сайт",
"siteQuestionReject": "Вы уверены, что хотите отклонить этот сайт?",
"siteMessageReject": "Этот процесс окончательно удалит сайт и все связанные с ним ресурсы, которые еще ожидают.",
"siteConfirmReject": "Подтвердите отклонение сайта",
"siteRejectSuccess": "Сайт успешно отклонен",
"siteRejectError": "Ошибка при отклонении сайта",
"provisioningKeys": "Ключи подготовки",
"searchProvisioningKeys": "Поиск подготовительных ключей...",
"provisioningKeysAdd": "Сгенерировать ключ подготовки",
@@ -466,8 +478,8 @@
"provisioningKeysSave": "Сохранить ключ подготовки",
"provisioningKeysSaveDescription": "Вы сможете увидеть это только один раз. Скопируйте его в безопасное место.",
"provisioningKeysErrorCreate": "Ошибка при создании ключа подготовки",
"provisioningKeysList": "Новый подготовительный ключ",
"provisioningKeysMaxBatchSize": "Макс. размер партии",
"provisioningKeysList": "Новый ключ для подготовки",
"provisioningKeysMaxBatchSize": "Максимальный размер партии",
"provisioningKeysUnlimitedBatchSize": "Неограниченный размер партии (без ограничений)",
"provisioningKeysMaxBatchUnlimited": "Неограниченный",
"provisioningKeysMaxBatchSizeInvalid": "Введите максимальный размер пакета (11,000,000).",
@@ -922,7 +934,7 @@
"newtVersion": "Версия",
"architecture": "Архитектура",
"sites": "Сайты",
"siteWgAnyClients": "Для подключения используйте любой клиент WireGuard. Вы должны будете адресовать внутренние ресурсы, используя IP адрес пира.",
"siteWgAnyClients": "Используйте любой клиент WireGuard для подключения. Вам придётся обращаться к частным ресурсам, используя IP узла.",
"siteWgCompatibleAllClients": "Совместим со всеми клиентами WireGuard",
"siteWgManualConfigurationRequired": "Требуется ручная настройка",
"userErrorNotAdminOrOwner": "Пользователь не является администратором или владельцем",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Создать пользователя Org",
"actionUpdateOrg": "Обновить организацию",
"actionRemoveInvitation": "Удалить приглашение",
"actionRemoveUserRole": "Удалить роль пользователя",
"actionUpdateUser": "Обновить пользователя",
"actionGetUser": "Получить пользователя",
"actionGetOrgUser": "Получить пользователя организации",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
"setupTokenRequired": "Токен настройки обязателен",
"actionUpdateSite": "Обновить сайт",
"actionApproveSite": "Одобрить сайт",
"actionRejectSite": "Отклонить сайт",
"actionResetSiteBandwidth": "Сброс пропускной способности организации",
"actionListSiteRoles": "Список разрешенных ролей сайта",
"actionCreateResource": "Создать ресурс",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Список приглашений",
"actionExportLogs": "Экспорт журналов",
"actionViewLogs": "Просмотр журналов",
"actionCreateSiteProvisioningKey": "Создать ключ конфигурации сайта",
"actionListSiteProvisioningKeys": "Список ключей конфигурации сайтов",
"actionUpdateSiteProvisioningKey": "Обновить ключ конфигурации сайта",
"actionDeleteSiteProvisioningKey": "Удалить ключ конфигурации сайта",
"noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.",
"search": "Поиск…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Введите код из вашего приложения-аутентификатора или один из ваших одноразовых резервных кодов.",
"otpAuthSubmit": "Отправить код",
"idpContinue": "Или продолжить с",
"idpLastUsed": "Последнее использование",
"otpAuthBack": "Назад к паролю",
"navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Поддомен: {subdomain}",
"domainPickerNamespace": "Пространство имен: {namespace}",
"domainPickerShowMore": "Показать еще",
"domainPickerNoDomainsAvailableTitle": "Нет доступных доменов",
"domainPickerNoDomainsAvailableDescription": "У вас еще не настроены домены. Создайте домен, чтобы продолжить.",
"domainPickerNoDomainsAvailableAction": "Перейти к доменам",
"regionSelectorTitle": "Выберите регион",
"domainPickerRemoteExitNodeWarning": "Предоставленные домены не поддерживаются при подключении сайтов к удаленным узлам. Для доступа к ресурсам на удаленных узлах используйте пользовательский домен.",
"regionSelectorInfo": "Выбор региона помогает нам обеспечить лучшее качество обслуживания для вашего расположения. Вам необязательно находиться в том же регионе, что и ваш сервер.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Публичный",
"resourcesTableClientResources": "Приватный",
"resourcesTableNoProxyResourcesFound": "Проксированных ресурсов не найдено.",
"resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
"resourcesTableNoInternalResourcesFound": "Частные ресурсы не найдены.",
"resourcesTableDestination": "Пункт назначения",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Псевдоним адреса",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "Отмена",
"editInternalResourceDialogSaveResource": "Сохранить ресурс",
"editInternalResourceDialogSuccess": "Успешно",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Внутренний ресурс успешно обновлен",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Частный ресурс успешно обновлен",
"editInternalResourceDialogError": "Ошибка",
"editInternalResourceDialogFailedToUpdateInternalResource": "Не удалось обновить внутренний ресурс",
"editInternalResourceDialogFailedToUpdateInternalResource": "Не удалось обновить частный ресурс",
"editInternalResourceDialogNameRequired": "Имя обязательно",
"editInternalResourceDialogNameMaxLength": "Имя не должно быть длиннее 255 символов",
"editInternalResourceDialogProxyPortMin": "Порт прокси должен быть не менее 1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
"createInternalResourceDialogNoSitesAvailable": "Нет доступных сайтов",
"createInternalResourceDialogNoSitesAvailableDescription": "Вам необходимо иметь хотя бы один сайт Newt с настроенной подсетью для создания внутреннего ресурса.",
"createInternalResourceDialogNoSitesAvailableDescription": "Вам необходимо иметь хотя бы один сайт Newt с настроенной подсетью для создания частных ресурсов.",
"createInternalResourceDialogClose": "Закрыть",
"createInternalResourceDialogCreateClientResource": "Создать приватный ресурс",
"createInternalResourceDialogCreateClientResourceDescription": "Создать новый ресурс, который будет доступен только клиентам, подключенным к организации",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "Отмена",
"createInternalResourceDialogCreateResource": "Создать ресурс",
"createInternalResourceDialogSuccess": "Успешно",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Внутренний ресурс успешно создан",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Частный ресурс успешно создан",
"createInternalResourceDialogError": "Ошибка",
"createInternalResourceDialogFailedToCreateInternalResource": "Не удалось создать внутренний ресурс",
"createInternalResourceDialogFailedToCreateInternalResource": "Не удалось создать частный ресурс",
"createInternalResourceDialogNameRequired": "Имя обязательно",
"createInternalResourceDialogNameMaxLength": "Имя должно содержать менее 255 символов",
"createInternalResourceDialogPleaseSelectSite": "Пожалуйста, выберите сайт",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Адрес источника",
"destinationAddress": "Адрес назначения",
"duration": "Продолжительность",
"licenseRequiredToUse": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> для использования этой функции. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.",
"licenseRequiredToUse": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> для использования этой функции. <bookADemoLink>Забронируйте демонстрацию или пробный POC, чтобы узнать больше.</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC, чтобы узнать больше.</bookADemoLink>",
"certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Ошибка загрузки",
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
"sessionToolbarShow": "Показать панель инструментов",
"sessionToolbarHide": "Скрыть панель инструментов"
"sessionToolbarHide": "Скрыть панель инструментов",
"actionUpdateSiteApprovals": "Обновить утверждения сайта"
}
+40 -16
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "Paylaşılabilir Bağlantıyı Silmeyi Onayla",
"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.",
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
"shareTokenDescription": "Erişim belirteci bir sorgu parametresi olarak veya istek başlıkları içinden gönderilebilir. Varsayılan olarak her istekte gönderilmelidir. Oturum kalıcılığı etkinse, ilk istek oturum çereziyle değiştirilir.",
"accessToken": "Erişim Jetonu",
"usageExamples": "Kullanım Örnekleri",
"tokenId": "Jeton ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "Başlık (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.",
"shareAssociateUserOptional": "Kullanıcıyla İlişkilendir (isteğe bağlı)",
"shareAssociateUserDescription": "Ayarladığında, bu bağlantıyı kullanan istekler erişim günlüklerinde ve kimlik başlıklarında kullanıcıya atanır. Kullanıcı kuruluşu terk ederse bağlantı kaldırılır.",
"userSelect": "Kullanıcı seçin",
"usersNotFound": "Kullanıcı bulunamadı",
"expireIn": "Süresi Dolacak",
"neverExpire": "Hiçbir Zaman Sona Ermez",
"sharePersistSession": "İlk kullanım sonrası oturumu kalıcı yap",
"sharePersistSessionDescription": "Etkinleştirildiğinde, bu belirteçle yapılan ilk istek, sorgu parametresi veya başlık üzerinden, bir oturum çerezi ayarlayarak sonraki isteklerde belirteç gerektirmeyecek. Her istek için belirteç göndermesi gereken API müşterileri için bırakın.",
"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.",
"shareAccessHint": "Bu bağlantıya sahip olan herkes kaynağa erişebilir. Dikkatle paylaşın.",
@@ -449,8 +455,14 @@
"provisioningManage": "Tedarik",
"provisioningDescription": "Tedarik anahtarlarını yönetin ve onay bekleyen siteleri gözden geçirin.",
"pendingSites": "Bekleyen Siteler",
"siteApproveSuccess": "Site başarıyla onaylandı",
"siteApproveSuccess": "Site ve ilgili kaynaklar başarıyla onaylandı",
"siteApproveError": "Site onaylanırken hata oluştu",
"siteReject": "Siteyi Reddet",
"siteQuestionReject": "Bu siteyi reddetmek istediğinizden emin misiniz?",
"siteMessageReject": "Bu işlem, siteyi ve hala beklemede olan herhangi bir ilgili kaynağı kalıcı olarak silecektir.",
"siteConfirmReject": "Site Reddetmeyi Onayla",
"siteRejectSuccess": "Site başarıyla reddedildi",
"siteRejectError": "Site reddedilirken hata oluştu",
"provisioningKeys": "Tedarik Anahtarları",
"searchProvisioningKeys": "Tedarik anahtarlarını ara...",
"provisioningKeysAdd": "Tedarik Anahtarı Üret",
@@ -466,12 +478,12 @@
"provisioningKeysSave": "Tedarik anahtarını kaydet",
"provisioningKeysSaveDescription": "Bunu yalnızca bir kez görebileceksiniz. Güvenli bir yere kopyalayın.",
"provisioningKeysErrorCreate": "Tedarik anahtarı oluşturulurken hata oluştu",
"provisioningKeysList": "Yeni tedarik anahtarı",
"provisioningKeysMaxBatchSize": "Maksimum toplu iş boyutu",
"provisioningKeysList": "Yeni Sağlama Anahtarı",
"provisioningKeysMaxBatchSize": "Maksimum Toplu İş Boyutu",
"provisioningKeysUnlimitedBatchSize": "Sınırsız toplu iş boyutu (sınırlama yok)",
"provisioningKeysMaxBatchUnlimited": "Sınırsız",
"provisioningKeysMaxBatchSizeInvalid": "Geçerli bir maksimum toplu iş boyutu girin (11,000,000).",
"provisioningKeysValidUntil": "Geçerlilik tarihi",
"provisioningKeysValidUntil": "Geçerli Olma Süresi",
"provisioningKeysValidUntilHint": "Son kullanım tarihi için boş bırakın.",
"provisioningKeysValidUntilInvalid": "Geçerli bir tarih ve saat girin.",
"provisioningKeysNumUsed": "Kullanım Sayısı",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "Asla",
"provisioningKeysEdit": "Tedarik Anahtarını Düzenle",
"provisioningKeysEditDescription": "Bu anahtar için maksimum toplu iş boyutunu ve son kullanma zamanını güncelleyin.",
"provisioningKeysApproveNewSites": "Yeni siteleri onayla",
"provisioningKeysApproveNewSites": "Yeni Siteleri Onayla",
"provisioningKeysApproveNewSitesDescription": "Bu anahtar ile kayıt olan siteleri otomatik olarak onayla.",
"provisioningKeysUpdateError": "Tedarik anahtarı güncellenirken hata oluştu",
"provisioningKeysUpdated": "Tedarik anahtarı güncellendi",
@@ -922,7 +934,7 @@
"newtVersion": "Sürüm",
"architecture": "Mimari",
"sites": "Siteler",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgAnyClients": "Bağlanmak için herhangi bir WireGuard istemcisi kullanın. Özel kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgCompatibleAllClients": "Tüm WireGuard istemcileriyle uyumlu",
"siteWgManualConfigurationRequired": "Manuel yapılandırma gerekli",
"userErrorNotAdminOrOwner": "Kullanıcı yönetici veya sahibi değil",
@@ -1397,6 +1409,7 @@
"createOrgUser": "Organizasyon Kullanıcısı Oluştur",
"actionUpdateOrg": "Kuruluşu Güncelle",
"actionRemoveInvitation": "Daveti Kaldır",
"actionRemoveUserRole": "Kullanıcı Rolünü Kaldır",
"actionUpdateUser": "Kullanıcıyı Güncelle",
"actionGetUser": "Kullanıcıyı Getir",
"actionGetOrgUser": "Kuruluş Kullanıcısını Al",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
"actionUpdateSite": "Siteyi Güncelle",
"actionApproveSite": "Siteyi Onayla",
"actionRejectSite": "Siteyi Reddet",
"actionResetSiteBandwidth": "Organizasyon Bant Genişliğini Sıfırla",
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
"actionCreateResource": "Kaynak Oluştur",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "Davetiyeleri Listele",
"actionExportLogs": "Kayıtları Dışa Aktar",
"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",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"search": "Ara…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "Authenticator uygulamanızdan veya tek kullanımlık yedek kodlarınızdan birini girin.",
"otpAuthSubmit": "Kodu Gönder",
"idpContinue": "Veya devam et:",
"idpLastUsed": "Son Kullanılan",
"otpAuthBack": "Şifreye Geri Dön",
"navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "Alt Alan: {subdomain}",
"domainPickerNamespace": "Ad Alanı: {namespace}",
"domainPickerShowMore": "Daha Fazla Göster",
"domainPickerNoDomainsAvailableTitle": "Kullanılacak alan adı yok",
"domainPickerNoDomainsAvailableDescription": "Henüz ayarlanmış bir alan adınız yok. Devam etmek için bir alan adı oluşturun.",
"domainPickerNoDomainsAvailableAction": "Alan Adlarına Git",
"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.",
"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.",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "Herkese Açık",
"resourcesTableClientResources": "Özel",
"resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.",
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
"resourcesTableNoInternalResourcesFound": "Özel kaynak bulunamadı.",
"resourcesTableDestination": "Hedef",
"resourcesTableAlias": "Takma Ad",
"resourcesTableAliasAddress": "Alias Adresi",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "İptal",
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
"editInternalResourceDialogSuccess": "Başarı",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Dahili kaynak başarıyla güncellendi",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Özel kaynak başarıyla güncellendi",
"editInternalResourceDialogError": "Hata",
"editInternalResourceDialogFailedToUpdateInternalResource": "Dahili kaynak güncellenemedi",
"editInternalResourceDialogFailedToUpdateInternalResource": "Özel kaynak güncellenemedi",
"editInternalResourceDialogNameRequired": "Ad gerekli",
"editInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
"editInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Takma Ad",
"editInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.",
"createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı",
"createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.",
"createInternalResourceDialogNoSitesAvailableDescription": "Özel kaynaklar oluşturmak için alt ağı yapılandırılmış en az bir Newt sitesine sahip olmalısınız.",
"createInternalResourceDialogClose": "Kapat",
"createInternalResourceDialogCreateClientResource": "Özel Kaynak Oluştur",
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemcilere erişilebilir olacak yeni bir kaynak oluşturun",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "İptal",
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
"createInternalResourceDialogSuccess": "Başarı",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Dahili kaynak başarıyla oluşturuldu",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Özel kaynak başarıyla oluşturuldu",
"createInternalResourceDialogError": "Hata",
"createInternalResourceDialogFailedToCreateInternalResource": "Dahili kaynak oluşturulamadı",
"createInternalResourceDialogFailedToCreateInternalResource": "Özel kaynak oluşturulamadı",
"createInternalResourceDialogNameRequired": "Ad gerekli",
"createInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
"createInternalResourceDialogPleaseSelectSite": "Lütfen bir site seçin",
@@ -3075,8 +3098,8 @@
"sourceAddress": "Kaynak Adresi",
"destinationAddress": "Hedef Adresi",
"duration": "Süre",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı veya <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> gereklidir. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.",
"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>.",
"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": "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ü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "Yükleme başarısız",
"rdpUnicodeKeyboardMode": "Unicode klavye modu",
"sessionToolbarShow": "Araç çubuğunu göster",
"sessionToolbarHide": "Araç çubuğunu gizle"
"sessionToolbarHide": "Araç çubuğunu gizle",
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle"
}
+39 -15
View File
@@ -178,7 +178,7 @@
"shareDeleteConfirm": "确认删除共享链接",
"shareQuestionRemove": "您确定要删除这个共享链接吗?",
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
"shareTokenDescription": "访问令牌可以作为查询参数或请求头传递。默认情况下,它必须在每次请求时发送。如果启用了会话持久性,则第一个请求会将其交换为会话cookie。",
"accessToken": "访问令牌",
"usageExamples": "用法示例",
"tokenId": "令牌 ID",
@@ -196,8 +196,14 @@
"shareTitleOptional": "标题 (可选)",
"sharePathOptional": "路径(可选)",
"sharePathDescription": "认证后,链接将把用户重定向到此路径。",
"shareAssociateUserOptional": "关联用户(可选)",
"shareAssociateUserDescription": "设置后,使用此链接的请求将在访问日志和身份头中归属于用户。如果用户离开组织,则该链接将被删除。",
"userSelect": "选择用户",
"usersNotFound": "未找到用户",
"expireIn": "过期时间",
"neverExpire": "永不过期",
"sharePersistSession": "首次使用后会话持久化",
"sharePersistSessionDescription": "启用后,通过查询参数或头部携带此令牌的首次请求会设置会话cookie,因此后续请求不需要令牌。对于应在每次请求中发送令牌的API客户端,请保持关闭。",
"shareExpireDescription": "过期时间是链接可以使用并提供对资源的访问时间。 此时间后,链接将不再工作,使用此链接的用户将失去对资源的访问。",
"shareSeeOnce": "您只能看到一次此链接。请确保复制它。",
"shareAccessHint": "任何具有此链接的人都可以访问该资源。小心地分享它。",
@@ -449,8 +455,14 @@
"provisioningManage": "预配",
"provisioningDescription": "管理预配密钥,并审核待批准的站点。",
"pendingSites": "待审批站点",
"siteApproveSuccess": "站点批准成功",
"siteApproveSuccess": "站点及相关资源已成功审批",
"siteApproveError": "批准站点出错",
"siteReject": "拒绝站点",
"siteQuestionReject": "您确定要拒绝此站点吗?",
"siteMessageReject": "这将永久删除站点及所有尚未处理的相关资源。",
"siteConfirmReject": "确认拒绝站点",
"siteRejectSuccess": "站点已被成功拒绝",
"siteRejectError": "拒绝站点时出错",
"provisioningKeys": "置备键",
"searchProvisioningKeys": "搜索配备密钥...",
"provisioningKeysAdd": "生成预配密钥",
@@ -466,8 +478,8 @@
"provisioningKeysSave": "保存预配键",
"provisioningKeysSaveDescription": "您只能看到一次。复制它到一个安全的地方。",
"provisioningKeysErrorCreate": "创建预配键时出错",
"provisioningKeysList": "新预配",
"provisioningKeysMaxBatchSize": "最大批大小",
"provisioningKeysList": "新预配密钥",
"provisioningKeysMaxBatchSize": "最大批大小",
"provisioningKeysUnlimitedBatchSize": "无限批量大小(无限制)",
"provisioningKeysMaxBatchUnlimited": "无限制",
"provisioningKeysMaxBatchSizeInvalid": "输入一个有效的最大批处理大小(1-1,000,000)。",
@@ -480,7 +492,7 @@
"provisioningKeysNeverUsed": "永不过期",
"provisioningKeysEdit": "编辑置备键",
"provisioningKeysEditDescription": "更新此密钥的最大批量大小和过期时间。",
"provisioningKeysApproveNewSites": "批准新点",
"provisioningKeysApproveNewSites": "批准新点",
"provisioningKeysApproveNewSitesDescription": "自动批准使用此密钥注册的节点。",
"provisioningKeysUpdateError": "更新预配键时出错",
"provisioningKeysUpdated": "置备密钥已更新",
@@ -922,7 +934,7 @@
"newtVersion": "版本",
"architecture": "架构",
"sites": "站点",
"siteWgAnyClients": "使用任何 WireGuard 客户端连接。您必须使用对等IP解决内部资源问题。",
"siteWgAnyClients": "使用任何WireGuard客户端进行连接。您必须使用对等IP来处理私有资源。",
"siteWgCompatibleAllClients": "与所有WireGuard客户端兼容",
"siteWgManualConfigurationRequired": "需要手动配置",
"userErrorNotAdminOrOwner": "用户不是管理员或所有者",
@@ -1397,6 +1409,7 @@
"createOrgUser": "创建组织用户",
"actionUpdateOrg": "更新组织",
"actionRemoveInvitation": "移除邀请",
"actionRemoveUserRole": "移除用户角色",
"actionUpdateUser": "更新用户",
"actionGetUser": "获取用户",
"actionGetOrgUser": "获取组织用户",
@@ -1419,6 +1432,8 @@
"setupTokenDescription": "从服务器控制台输入设置令牌。",
"setupTokenRequired": "需要设置令牌",
"actionUpdateSite": "更新站点",
"actionApproveSite": "批准站点",
"actionRejectSite": "拒绝站点",
"actionResetSiteBandwidth": "重置组织带宽",
"actionListSiteRoles": "允许站点角色列表",
"actionCreateResource": "创建资源",
@@ -1502,6 +1517,10 @@
"actionListInvitations": "邀请列表",
"actionExportLogs": "导出日志",
"actionViewLogs": "查看日志",
"actionCreateSiteProvisioningKey": "创建站点预配密钥",
"actionListSiteProvisioningKeys": "列出站点预配密钥",
"actionUpdateSiteProvisioningKey": "更新站点预配密钥",
"actionDeleteSiteProvisioningKey": "删除站点预配密钥",
"noneSelected": "未选择",
"orgNotFound2": "未找到组织。",
"search": "搜索…",
@@ -1516,6 +1535,7 @@
"otpAuthDescription": "从您的身份验证程序中输入代码或您的单次备份代码。",
"otpAuthSubmit": "提交代码",
"idpContinue": "或者继续",
"idpLastUsed": "上次使用",
"otpAuthBack": "回到密码",
"navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单",
@@ -1975,6 +1995,9 @@
"domainPickerSubdomain": "子域:{subdomain}",
"domainPickerNamespace": "命名空间:{namespace}",
"domainPickerShowMore": "显示更多",
"domainPickerNoDomainsAvailableTitle": "无可用域",
"domainPickerNoDomainsAvailableDescription": "您还没有设置任何域。创建一个域以继续。",
"domainPickerNoDomainsAvailableAction": "转到域",
"regionSelectorTitle": "选择区域",
"domainPickerRemoteExitNodeWarning": "当站点连接到远程退出节点时不支持所提供的域。为了资源可在远程节点上使用,请使用自定义域名。",
"regionSelectorInfo": "选择区域以帮助提升您所在地的性能。您不必与服务器在相同的区域。",
@@ -2338,7 +2361,7 @@
"resourcesTableProxyResources": "",
"resourcesTableClientResources": "私有资源",
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
"resourcesTableNoInternalResourcesFound": "未找到私有资源。",
"resourcesTableDestination": "目标",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "别名地址",
@@ -2361,9 +2384,9 @@
"editInternalResourceDialogCancel": "取消",
"editInternalResourceDialogSaveResource": "保存资源",
"editInternalResourceDialogSuccess": "成功",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "内部资源更新成功",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "私有资源更新成功",
"editInternalResourceDialogError": "错误",
"editInternalResourceDialogFailedToUpdateInternalResource": "更新内部资源失败",
"editInternalResourceDialogFailedToUpdateInternalResource": "更新私有资源失败",
"editInternalResourceDialogNameRequired": "名称为必填项",
"editInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符",
"editInternalResourceDialogProxyPortMin": "代理端口必须至少为1",
@@ -2389,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
"createInternalResourceDialogNoSitesAvailable": "暂无可用站点",
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建内部资源。",
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建私有资源。",
"createInternalResourceDialogClose": "关闭",
"createInternalResourceDialogCreateClientResource": "创建私有资源",
"createInternalResourceDialogCreateClientResourceDescription": "创建一个新资源只能为连接到组织的客户端访问",
@@ -2418,9 +2441,9 @@
"createInternalResourceDialogCancel": "取消",
"createInternalResourceDialogCreateResource": "创建资源",
"createInternalResourceDialogSuccess": "成功",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "内部资源创建成功",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "私有资源创建成功",
"createInternalResourceDialogError": "错误",
"createInternalResourceDialogFailedToCreateInternalResource": "创建内部资源失败",
"createInternalResourceDialogFailedToCreateInternalResource": "创建私有资源失败",
"createInternalResourceDialogNameRequired": "名称为必填项",
"createInternalResourceDialogNameMaxLength": "名称长度必须小于255个字符",
"createInternalResourceDialogPleaseSelectSite": "请选择一个站点",
@@ -3075,8 +3098,8 @@
"sourceAddress": "源地址",
"destinationAddress": "目的地址",
"duration": "期限",
"licenseRequiredToUse": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>。<bookADemoLink>预约演示或POC试用</bookADemoLink>",
"ossEnterpriseEditionRequired": "需要 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 才能使用此功能。 此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。 <bookADemoLink>预演示或POC 试用</bookADemoLink>",
"licenseRequiredToUse": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>。<bookADemoLink>预约演示或POC试用以了解更多。</bookADemoLink>",
"ossEnterpriseEditionRequired": "需要<enterpriseEditionLink>企业版</enterpriseEditionLink>才能使用此功能。此功能也可在<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。<bookADemoLink>预演示或POC试用以了解更多。</bookADemoLink>",
"certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析",
@@ -3801,5 +3824,6 @@
"rdpUploadFailed": "上传失败",
"rdpUnicodeKeyboardMode": "Unicode 键盘模式",
"sessionToolbarShow": "显示工具栏",
"sessionToolbarHide": "隐藏工具栏"
"sessionToolbarHide": "隐藏工具栏",
"actionUpdateSiteApprovals": "更新站点审批"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

+1
View File
@@ -21,6 +21,7 @@ export enum ActionsEnum {
getSite = "getSite",
listSites = "listSites",
updateSite = "updateSite",
updateSiteApprovals = "updateSiteApprovals",
restartSite = "restartSite",
resetSiteBandwidth = "resetSiteBandwidth",
reGenerateSecret = "reGenerateSecret",
+14 -3
View File
@@ -18,7 +18,7 @@ export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(),
baseDomain: varchar("baseDomain").notNull(),
configManaged: boolean("configManaged").notNull().default(false),
type: varchar("type"), // "ns", "cname", "wildcard"
type: varchar("type").$type<"ns" | "cname" | "wildcard">(),
verified: boolean("verified").notNull().default(false),
failed: boolean("failed").notNull().default(false),
tries: integer("tries").notNull().default(0),
@@ -107,6 +107,7 @@ export const sites = pgTable(
lastPing: integer("lastPing"),
address: varchar("address"),
endpoint: varchar("endpoint"),
localEndpoints: varchar("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"),
@@ -200,7 +201,10 @@ export const resources = pgTable(
authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123)
authDaemonPort: integer("authDaemonPort").default(22123),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
},
(t) => [
index("idx_resources_fulldomain")
@@ -451,7 +455,10 @@ export const siteResources = pgTable(
onDelete: "set null"
}),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain")
fullDomain: varchar("fullDomain"),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
},
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
);
@@ -899,12 +906,16 @@ export const resourceAccessToken = pgTable("resourceAccessToken", {
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
}),
path: varchar("path"),
tokenHash: varchar("tokenHash").notNull(),
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }),
title: varchar("title"),
description: varchar("description"),
persistSession: boolean("persistSession").notNull().default(false),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
});
+12 -3
View File
@@ -15,7 +15,7 @@ export const domains = sqliteTable("domains", {
configManaged: integer("configManaged", { mode: "boolean" })
.notNull()
.default(false),
type: text("type"), // "ns", "cname", "wildcard"
type: text("type").$type<"ns" | "cname" | "wildcard">(),
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
tries: integer("tries").notNull().default(0),
@@ -118,6 +118,7 @@ export const sites = sqliteTable("sites", {
// exit node stuff that is how to connect to the site when it has a wg server
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
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
lastHolePunch: integer("lastHolePunch"),
listenPort: integer("listenPort"),
@@ -209,7 +210,8 @@ export const resources = sqliteTable("resources", {
authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123)
authDaemonPort: integer("authDaemonPort").default(22123),
status: text("status").$type<"pending" | "approved">().default("approved")
});
export const labels = sqliteTable("labels", {
@@ -447,7 +449,8 @@ export const siteResources = sqliteTable("siteResources", {
onDelete: "set null"
}),
subdomain: text("subdomain"),
fullDomain: text("fullDomain")
fullDomain: text("fullDomain"),
status: text("status").$type<"pending" | "approved">().default("approved")
});
export const networks = sqliteTable("networks", {
@@ -1123,12 +1126,18 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", {
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
}),
path: text("path"),
tokenHash: text("tokenHash").notNull(),
sessionLength: integer("sessionLength").notNull(),
expiresAt: integer("expiresAt"),
title: text("title"),
description: text("description"),
persistSession: integer("persistSession", { mode: "boolean" })
.notNull()
.default(false),
createdAt: integer("createdAt").notNull()
});
+13 -11
View File
@@ -30,14 +30,14 @@ export const NotifyTrialExpiring = ({
const isLastDay = daysRemaining === 1;
const previewText = hasEnded
? `Your trial for ${orgName} has ended.`
? `Your cloud trial for ${orgName} has ended.`
: isLastDay
? `Your trial for ${orgName} ends tomorrow.`
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
? `Your cloud trial for ${orgName} ends tomorrow.`
: `Your cloud trial for ${orgName} ends in ${daysRemaining} days.`;
const heading = hasEnded
? "Your Trial Ended"
: "Your Trial is Ending Soon";
? "Your Cloud Trial Ended"
: "Your Cloud Trial is Ending Soon";
return (
<Html>
@@ -55,7 +55,7 @@ export const NotifyTrialExpiring = ({
{hasEnded ? (
<>
<EmailText>
Your free trial for{" "}
Your cloud free trial for{" "}
<strong>{orgName}</strong> ended on{" "}
<strong>{trialEndsAt}</strong>. Your account
has been moved to the free plan, which
@@ -64,10 +64,11 @@ export const NotifyTrialExpiring = ({
<EmailText>
Some features and resources may now be
restricted. To restore full
access and continue using all the features
you had during your trial, please upgrade to
a paid plan.
restricted. To restore full access and
continue using all the features you had
during your trial, please upgrade to a paid
plan. This does not effect any self hosted
licenses.
</EmailText>
<EmailText>
@@ -93,7 +94,8 @@ export const NotifyTrialExpiring = ({
<EmailText>
After your trial ends, your account will be
moved to the free plan and some
functionality may be restricted.
functionality may be restricted. This does
not effect any self hosted licenses.
</EmailText>
<EmailText>
+3 -2
View File
@@ -12,7 +12,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet";
import swaggerUi from "swagger-ui-express";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import { registry } from "./openApi";
import { registry, openApiTags } from "./openApi";
import fs from "fs";
import path from "path";
import { APP_PATH } from "./lib/consts";
@@ -181,7 +181,8 @@ function getOpenApiDocumentation() {
version: "v1",
title: "Pangolin Integration API"
},
servers: [{ url: "/v1" }]
servers: [{ url: "/v1" }],
tags: openApiTags
});
if (!process.env.DISABLE_GEN_OPENAPI) {
-6
View File
@@ -34,12 +34,6 @@ import {
rebuildClientAssociationsFromSiteResource,
waitForSiteResourceRebuildIdle
} from "../rebuildClientAssociations";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
type ApplyBlueprintArgs = {
orgId: string;
+62 -10
View File
@@ -26,9 +26,6 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
@@ -201,17 +198,19 @@ export async function updatePrivateResources(
}
}
let resourceStatusFromSite: "approved" | "pending" = "approved";
if (siteId && allSites.length === 0) {
// only add if there are not provided sites
// Use the provided siteId directly, but verify it belongs to the org
const [siteSingle] = await trx
.select({ siteId: sites.siteId })
.select({ siteId: sites.siteId, status: sites.status })
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
if (siteSingle) {
allSites.push(siteSingle);
}
resourceStatusFromSite = siteSingle.status ?? "approved";
}
if (allSites.length === 0) {
@@ -220,6 +219,13 @@ export async function updatePrivateResources(
);
}
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
if (existingResource) {
let domainInfo:
| { subdomain: string | null; domainId: string }
@@ -233,6 +239,31 @@ export async function updatePrivateResources(
);
}
if (resourceData.alias) {
const [aliasConflict] = await trx
.select({
siteResourceId: siteResources.siteResourceId
})
.from(siteResources)
.where(
and(
eq(siteResources.orgId, orgId),
eq(siteResources.alias, resourceData.alias),
ne(
siteResources.siteResourceId,
existingResource.siteResourceId
)
)
)
.limit(1);
if (aliasConflict) {
throw new Error(
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
);
}
}
// Update existing resource
const [updatedResource] = await trx
.update(siteResources)
@@ -243,8 +274,7 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
enabled: resourceEnabled,
alias: resourceData.alias || null,
disableIcmp:
resourceData["disable-icmp"] ||
@@ -263,7 +293,8 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
})
.where(
eq(
@@ -474,6 +505,27 @@ export async function updatePrivateResources(
);
}
if (resourceData.alias) {
const [aliasConflict] = await trx
.select({
siteResourceId: siteResources.siteResourceId
})
.from(siteResources)
.where(
and(
eq(siteResources.orgId, orgId),
eq(siteResources.alias, resourceData.alias)
)
)
.limit(1);
if (aliasConflict) {
throw new Error(
`Alias ${resourceData.alias} already in use by another site resource in org ${orgId}`
);
}
}
const [network] = await trx
.insert(networks)
.values({
@@ -496,8 +548,7 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
enabled: resourceEnabled,
alias: resourceData.alias || null,
aliasAddress: aliasAddress,
disableIcmp:
@@ -517,7 +568,8 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
})
.returning();
+37 -15
View File
@@ -25,6 +25,7 @@ import {
rolePolicies,
roleResources,
roles,
Site,
sites,
Target,
TargetHealthCheck,
@@ -74,19 +75,40 @@ export async function updatePublicResources(
)) {
const targetsToUpdate: Target[] = [];
const healthchecksToUpdate: TargetHealthCheck[] = [];
let resource: Resource;
let resourceStatusFromSite: "approved" | "pending" = "approved";
let providedSite: Partial<Site> | undefined;
if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[providedSite] = await trx
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
resourceStatusFromSite = providedSite?.status ?? "approved";
}
async function createTarget( // reusable function to create a target
resourceId: number,
targetData: TargetData
) {
const targetSiteId = targetData.site;
let site;
let site: Partial<Site> | undefined;
if (targetSiteId) {
// Look up site by niceId
[site] = await trx
.select({ siteId: sites.siteId, type: sites.type })
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.from(sites)
.where(
and(
@@ -95,15 +117,9 @@ export async function updatePublicResources(
)
)
.limit(1);
} else if (siteId) {
} else if (siteId && providedSite) {
// Use the provided siteId directly, but verify it belongs to the org
[site] = await trx
.select({ siteId: sites.siteId, type: sites.type })
.from(sites)
.where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
)
.limit(1);
site = providedSite;
} else {
throw new Error(`Target site is required`);
}
@@ -139,7 +155,7 @@ export async function updatePublicResources(
.insert(targets)
.values({
resourceId: resourceId,
siteId: site.siteId,
siteId: site.siteId!,
ip: targetData.hostname,
mode: resourceData.mode as Target["mode"],
method: targetData.method,
@@ -172,7 +188,7 @@ export async function updatePublicResources(
.insert(targetHealthCheck)
.values({
name: `${targetData.hostname}:${targetData.port}`,
siteId: site.siteId,
siteId: site.siteId!,
targetId: newTarget.targetId,
orgId: orgId,
hcEnabled: healthcheckData?.enabled || false,
@@ -230,7 +246,10 @@ export async function updatePublicResources(
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceData.enabled;
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
const resourceSsl =
resourceData.ssl == undefined || resourceData.ssl == null
? true
@@ -406,7 +425,8 @@ export async function updatePublicResources(
? (resourceData["proxy-protocol-version"] ??
1)
: 1,
resourcePolicyId: sharedPolicy.resourcePolicyId
resourcePolicyId: sharedPolicy.resourcePolicyId,
status: resourceStatusFromSite
})
.where(
eq(
@@ -590,7 +610,8 @@ export async function updatePublicResources(
authDaemonPort:
resourceData["auth-daemon"]?.port || 22123,
resourcePolicyId: null,
defaultResourcePolicyId: inlinePolicyId
defaultResourcePolicyId: inlinePolicyId,
status: resourceStatusFromSite
})
.where(
eq(
@@ -1131,6 +1152,7 @@ export async function updatePublicResources(
.values({
orgId,
niceId: resourceNiceId,
status: resourceStatusFromSite,
name: resourceData.name || "Unnamed Resource",
mode: resourceData.mode,
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(
+7 -14
View File
@@ -1,7 +1,5 @@
import {
db,
idp,
idpOrg,
resourcePolicies,
resourcePolicyHeaderAuth,
resourcePolicyPassword,
@@ -20,6 +18,7 @@ import { Config, ResourcePolicyData } from "./types";
import logger from "@server/logger";
import { getUniqueResourcePolicyName } from "@server/db/names";
import { hashPassword } from "@server/auth/password";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
@@ -71,19 +70,13 @@ export async function updateResourcePolicies(
// Validate auto-login-idp if provided
if (policyData["auto-login-idp"]) {
const [provider] = await trx
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(
and(
eq(idp.idpId, policyData["auto-login-idp"]),
eq(idpOrg.orgId, orgId)
)
)
.limit(1);
const providerExists = await idpExistsForOrg(
policyData["auto-login-idp"],
orgId,
trx
);
if (!provider) {
if (!providerExists) {
throw new Error(
`Identity provider not found for policy '${policyNiceId}' in this organization`
);
+2 -2
View File
@@ -28,7 +28,7 @@ export const TargetHealthCheckSchema = z.object({
hostname: z.string(),
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
path: z.string().optional(),
path: z.string().optional().default("/"),
scheme: z.string().optional(),
mode: z.string().default("http"),
interval: z.int().default(30),
@@ -470,7 +470,7 @@ export const PrivateResourceSchema = z
// proxyPort: z.int().positive().optional(),
"destination-port": z.int().positive().optional(),
destination: z.string().min(1).optional(),
// enabled: z.boolean().default(true),
enabled: z.boolean().default(true),
"tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"),
"disable-icmp": z.boolean().optional().default(false),
+1
View File
@@ -5,6 +5,7 @@ export async function getValidCertificatesForDomains(
Array<{
id: number;
domain: string;
queriedDomain: string;
wildcard: boolean | null;
certFile: string | null;
keyFile: string | null;
+16
View File
@@ -18,3 +18,19 @@ export function canCompress(
return false;
}
}
// Whether this newt client understands `tlsCertId` references into the
// sync message's `certs` array, instead of requiring each target to carry
// its own inline `tlsCert`/`tlsKey` PEM data. Bump the version floor here to
// match whatever release first ships the newt-side support.
export function supportsCertReferences(
clientVersion: string | null | undefined
): boolean {
try {
if (!clientVersion) return false;
if (!semver.valid(clientVersion)) return false;
return semver.gte(clientVersion, "1.16.0");
} catch {
return false;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.20.0";
export const APP_VERSION = "1.21.1";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
+3 -5
View File
@@ -14,8 +14,6 @@ import {
} from "@server/db";
import logger from "@server/logger";
import { removeTargets } from "@server/routers/newt/targets";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export type DeleteResourceResult = {
deletedResource: Resource;
@@ -117,10 +115,10 @@ export async function runResourceDeleteSideEffects(
.limit(1);
if (!site) {
throw createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${target.siteId} not found`
logger.debug(
`Site with ID ${target.siteId} not found during resource delete side effects; skipping target removal`
);
continue;
}
if (site.pubKey && site.type === "newt") {
+82 -3
View File
@@ -1,6 +1,7 @@
import { and, eq, sql } from "drizzle-orm";
import { and, eq, inArray, sql } from "drizzle-orm";
import {
db,
resources,
siteNetworks,
siteResources,
targets,
@@ -97,6 +98,64 @@ export function exceedsSiteAssociatedResourceDeleteLimit(
return resourceCount > MAX_SITE_ASSOCIATED_RESOURCES_FOR_BULK_DELETE;
}
export async function getPendingResourceIdsForSite(
siteId: number,
trx: Transaction | typeof db = db
): Promise<number[]> {
const resourceIds = await getResourceIdsForSite(siteId, trx);
if (resourceIds.length === 0) {
return [];
}
const rows = await trx
.select({ resourceId: resources.resourceId })
.from(resources)
.where(
and(
inArray(resources.resourceId, resourceIds),
eq(resources.status, "pending")
)
);
return rows.map((row) => row.resourceId);
}
export async function getPendingSiteResourceIdsForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<number[]> {
const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
if (siteResourceIds.length === 0) {
return [];
}
const rows = await trx
.select({ siteResourceId: siteResources.siteResourceId })
.from(siteResources)
.where(
and(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.status, "pending")
)
);
return rows.map((row) => row.siteResourceId);
}
export async function getPendingAssociatedResourceCountForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<number> {
const [resourceIds, siteResourceIds] = await Promise.all([
getPendingResourceIdsForSite(siteId, trx),
getPendingSiteResourceIdsForSite(siteId, orgId, trx)
]);
return resourceIds.length + siteResourceIds.length;
}
export async function deleteAssociatedResourcesForSite(
siteId: number,
orgId: string,
@@ -105,12 +164,32 @@ export async function deleteAssociatedResourcesForSite(
const resourceIds = await getResourceIdsForSite(siteId, trx);
const siteResourceIds = await getSiteResourceIdsForSite(siteId, orgId, trx);
const [resources, siteResourcesDeleted] = await Promise.all([
const [deletedResources, siteResourcesDeleted] = await Promise.all([
performDeleteResources(resourceIds, trx),
performDeleteSiteResources(siteResourceIds, trx)
]);
return { resources, siteResources: siteResourcesDeleted };
return { resources: deletedResources, siteResources: siteResourcesDeleted };
}
export async function deletePendingAssociatedResourcesForSite(
siteId: number,
orgId: string,
trx: Transaction | typeof db = db
): Promise<DeleteSiteAssociatedResourcesSideEffects> {
const resourceIds = await getPendingResourceIdsForSite(siteId, trx);
const siteResourceIds = await getPendingSiteResourceIdsForSite(
siteId,
orgId,
trx
);
const [deletedResources, siteResourcesDeleted] = await Promise.all([
performDeleteResources(resourceIds, trx),
performDeleteSiteResources(siteResourceIds, trx)
]);
return { resources: deletedResources, siteResources: siteResourcesDeleted };
}
export async function runDeleteSiteAssociatedResourcesSideEffects(
+36
View File
@@ -0,0 +1,36 @@
import { db, idp, idpOrg, Transaction } from "@server/db";
import { and, eq } from "drizzle-orm";
export function isOrgIdentityProviderMode(): boolean {
return process.env.IDENTITY_PROVIDER_MODE === "org";
}
/**
* Checks whether an identity provider can be used for the given org.
* In org IdP mode, the provider must be linked via idpOrg.
* In global IdP mode, the provider only needs to exist.
*/
export async function idpExistsForOrg(
idpId: number,
orgId: string,
dbOrTrx: typeof db | Transaction = db
): Promise<boolean> {
if (isOrgIdentityProviderMode()) {
const [provider] = await dbOrTrx
.select({ idpId: idp.idpId })
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)))
.limit(1);
return !!provider;
}
const [provider] = await dbOrTrx
.select({ idpId: idp.idpId })
.from(idp)
.where(eq(idp.idpId, idpId))
.limit(1);
return !!provider;
}
+120 -13
View File
@@ -5,6 +5,7 @@ import config from "@server/lib/config";
import z from "zod";
import logger from "@server/logger";
import semver from "semver";
import { createHash } from "crypto";
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
import { lockManager } from "#dynamic/lib/lock";
@@ -496,6 +497,7 @@ export function generateRemoteSubnets(
): string[] {
const remoteSubnets = allSiteResources
.filter((sr) => {
if (!sr.enabled) return false;
if (!sr.destination) return false;
if (sr.mode === "cidr") {
@@ -530,6 +532,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
return allSiteResources
.filter(
(sr) =>
sr.enabled &&
sr.aliasAddress &&
((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) ||
(sr.fullDomain && sr.mode == "http"))
@@ -646,22 +649,109 @@ export type SubnetProxyTargetV2 = {
httpTargets?: HTTPTarget[];
tlsCert?: string;
tlsKey?: string;
tlsCertId?: string; // references an entry in the sync message's top-level `certs` array instead of inlining tlsCert/tlsKey
};
export type CertRef = { id: string; cert: string; key: string };
/**
* Replaces each target's inline tlsCert/tlsKey with a tlsCertId reference
* into a deduplicated certs array, so that many targets sharing the same
* certificate (e.g. a wildcard cert used by thousands of site resources)
* only need that certificate sent once per sync message.
*/
export function dedupeCertsForTargets(
targetsV2: SubnetProxyTargetV2[]
): { targets: SubnetProxyTargetV2[]; certs: CertRef[] } {
const idByContent = new Map<string, string>();
const certs: CertRef[] = [];
const targets = targetsV2.map((target) => {
if (!target.tlsCert || !target.tlsKey) {
return target;
}
const contentKey = `${target.tlsCert}|${target.tlsKey}`;
let id = idByContent.get(contentKey);
if (!id) {
id = createHash("sha1").update(contentKey).digest("hex").slice(0, 16);
idByContent.set(contentKey, id);
certs.push({ id, cert: target.tlsCert, key: target.tlsKey });
}
const { tlsCert, tlsKey, ...rest } = target;
return { ...rest, tlsCertId: id };
});
return { targets, certs };
}
export type HTTPTarget = {
destAddr: string; // must be an IP or hostname
destPort: number;
scheme: "http" | "https";
};
export type CertByDomain = Map<string, { certFile: string; keyFile: string }>;
/**
* Fetches the TLS certificates for every enabled, SSL-enabled HTTP site
* resource's fullDomain in a single batched call, instead of one call per
* resource. Many resources commonly resolve to the very same certificate
* (e.g. a wildcard covering the org's domain), so batching turns what would
* be N concurrent DB/cache round-trips into one, and a lookup failure fails
* loudly for the whole batch rather than silently dropping the cert on a
* random subset of otherwise-identical resources under load.
*/
export async function batchFetchCertsForSiteResources(
allSiteResources: SiteResource[]
): Promise<CertByDomain> {
const domains = new Set(
allSiteResources
.filter((r) => r.enabled && r.mode === "http" && r.ssl && r.fullDomain)
.map((r) => r.fullDomain as string)
);
const certByDomain: CertByDomain = new Map();
if (domains.size === 0) {
return certByDomain;
}
try {
const certResults = await getValidCertificatesForDomains(domains, true);
for (const cert of certResults) {
if (cert.certFile && cert.keyFile) {
certByDomain.set(cert.queriedDomain, {
certFile: cert.certFile,
keyFile: cert.keyFile
});
}
}
} catch (err) {
logger.error(
`Failed to batch-retrieve certificates for ${domains.size} domain(s): ${err}`
);
}
return certByDomain;
}
export async function generateSubnetProxyTargetV2(
siteResource: SiteResource,
clients: {
clientId: number;
pubKey: string | null;
subnet: string | null;
}[]
}[],
certByDomain?: CertByDomain
): Promise<SubnetProxyTargetV2[] | undefined> {
if (!siteResource.enabled) {
logger.debug(
`Site resource ${siteResource.siteResourceId} is disabled, skipping target generation.`
);
return;
}
if (clients.length === 0) {
logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
@@ -741,23 +831,40 @@ export async function generateSubnetProxyTargetV2(
let tlsKey: string | undefined;
if (siteResource.ssl && siteResource.fullDomain) {
try {
const certs = await getValidCertificatesForDomains(
new Set([siteResource.fullDomain]),
true
);
if (certs.length > 0 && certs[0].certFile && certs[0].keyFile) {
tlsCert = certs[0].certFile;
tlsKey = certs[0].keyFile;
if (certByDomain) {
// Caller batch-fetched certs for all resources up front (the
// common, high-scale path) — just look up this resource's
// domain rather than issuing its own DB/cache round-trip.
const cert = certByDomain.get(siteResource.fullDomain);
if (cert) {
tlsCert = cert.certFile;
tlsKey = cert.keyFile;
} else {
logger.warn(
`No valid certificate found for SSL site resource ${siteResource.siteResourceId} with domain ${siteResource.fullDomain}`
);
}
} catch (err) {
logger.error(
`Failed to retrieve certificate for site resource ${siteResource.siteResourceId} domain ${siteResource.fullDomain}: ${err}`
);
} else {
// No batched map supplied by the caller — fall back to a
// single-domain lookup for this resource alone.
try {
const certs = await getValidCertificatesForDomains(
new Set([siteResource.fullDomain]),
true
);
if (certs.length > 0 && certs[0].certFile && certs[0].keyFile) {
tlsCert = certs[0].certFile;
tlsKey = certs[0].keyFile;
} else {
logger.warn(
`No valid certificate found for SSL site resource ${siteResource.siteResourceId} with domain ${siteResource.fullDomain}`
);
}
} catch (err) {
logger.error(
`Failed to retrieve certificate for site resource ${siteResource.siteResourceId} domain ${siteResource.fullDomain}: ${err}`
);
}
}
}
+34 -3
View File
@@ -15,10 +15,41 @@ function getSegmentRegex(patternPart: string): RegExp {
return regex;
}
// Decodes percent-encoding (so an encoded slash like `%2F` is treated as a
// real path separator, matching what most backends will do) and then
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment
// or a wildcard-swallowed sequence under `/public/*`.
function decodeAndResolvePath(p: string): string[] {
const rawParts = p.split("/").filter(Boolean);
const resolved: string[] = [];
for (const rawPart of rawParts) {
let part: string;
try {
part = decodeURIComponent(rawPart);
} catch {
part = rawPart;
}
// an encoded slash can turn one raw segment into several real ones
for (const segment of part.split("/").filter(Boolean)) {
if (segment === ".") {
continue;
} else if (segment === "..") {
resolved.pop();
} else {
resolved.push(segment);
}
}
}
return resolved;
}
export function isPathAllowed(pattern: string, path: string): boolean {
const normalize = (p: string) => p.split("/").filter(Boolean);
const patternParts = normalize(pattern);
const pathParts = normalize(path);
const patternParts = pattern.split("/").filter(Boolean);
const pathParts = decodeAndResolvePath(path);
function matchSegments(
patternIndex: number,
+30 -16
View File
@@ -1561,9 +1561,19 @@ export async function handleMessagingForUpdatedSiteResource(
updatedSiteResource.udpPortRangeString ||
existingSiteResource.disableIcmp !==
updatedSiteResource.disableIcmp);
// Toggling enabled on/off doesn't change any of the fields above, but it
// does change whether targets/peer data should exist at all, so it needs
// to drive the same old->new diff machinery: going enabled->disabled
// diffs "real data" against "nothing" (a remove), and disabled->enabled
// diffs "nothing" against "real data" (an add). generateSubnetProxyTargetV2/
// generateRemoteSubnets/generateAliasConfig already return nothing for a
// disabled resource, so no other changes are needed here.
const enabledChanged =
existingSiteResource &&
existingSiteResource.enabled !== updatedSiteResource.enabled;
logger.debug(
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)}`
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)} enabledChanged=${Boolean(enabledChanged)}`
);
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
@@ -1574,14 +1584,16 @@ export async function handleMessagingForUpdatedSiteResource(
fullDomainChanged ||
sslChanged ||
portRangesChanged ||
destinationPortChanged
destinationPortChanged ||
enabledChanged
) {
const shouldUpdateTargets =
destinationChanged ||
sslChanged ||
portRangesChanged ||
fullDomainChanged ||
destinationPortChanged;
destinationPortChanged ||
enabledChanged;
logger.debug(
`handleMessagingForUpdatedSiteResource: entering unchanged-site update path shouldUpdateTargets=${shouldUpdateTargets}`
@@ -1657,20 +1669,22 @@ export async function handleMessagingForUpdatedSiteResource(
peerDataUpdateBatch.push({
clientId: client.clientId,
siteId,
remoteSubnets: destinationChanged
? {
oldRemoteSubnets: !oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
remoteSubnets:
destinationChanged || enabledChanged
? {
oldRemoteSubnets:
!oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
aliases:
aliasChanged || fullDomainChanged // the full domain is sent down as an alias
aliasChanged || fullDomainChanged || enabledChanged // the full domain is sent down as an alias
? {
oldAliases: generateAliasConfig([
existingSiteResource
+166 -20
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { db, logsDb, statusHistory } from "@server/db";
import { and, eq, gte, lt, asc, desc } from "drizzle-orm";
import { and, eq, gte, lt, asc, desc, inArray, max, sql } from "drizzle-orm";
import { regionalCache as cache } from "#dynamic/lib/cache";
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
@@ -8,26 +8,42 @@ const STATUS_HISTORY_CACHE_TTL = 60; // seconds
function statusHistoryCacheKey(
entityType: string,
entityId: number,
days: number
days: number,
tzOffsetMinutes: number
): string {
return `statusHistory:${entityType}:${entityId}:${days}`;
return `statusHistory:${entityType}:${entityId}:${days}:${tzOffsetMinutes}`;
}
// Returns the epoch seconds of the most recent local-calendar-day midnight,
// where "local" is defined by tzOffsetMinutes (minutes to ADD to UTC to get
// local time, e.g. Australia/Sydney standard time is 600). Defaults to 0
// (UTC) so callers that don't pass a timezone keep the original behavior.
function localMidnightSec(tzOffsetMinutes: number): number {
const localNow = new Date(Date.now() + tzOffsetMinutes * 60_000);
localNow.setUTCHours(0, 0, 0, 0);
return Math.floor(localNow.getTime() / 1000) - tzOffsetMinutes * 60;
}
export async function getCachedStatusHistory(
entityType: string,
entityId: number,
days: number
days: number,
tzOffsetMinutes: number = 0
): Promise<StatusHistoryResponse> {
const cacheKey = statusHistoryCacheKey(entityType, entityId, days);
const cacheKey = statusHistoryCacheKey(
entityType,
entityId,
days,
tzOffsetMinutes
);
const cached = await cache.get<StatusHistoryResponse>(cacheKey);
if (cached !== undefined) {
return cached;
}
// Anchor to UTC midnight so the query window aligns with stable calendar days
const utcToday = new Date();
utcToday.setUTCHours(0, 0, 0, 0);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const startSec = todayMidnightSec - days * 86400;
const events = await logsDb
@@ -63,7 +79,8 @@ export async function getCachedStatusHistory(
const { buckets, totalDowntime } = computeBuckets(
events,
days,
priorStatus
priorStatus,
tzOffsetMinutes
);
const totalWindow = days * 86400;
const overallUptime =
@@ -99,11 +116,19 @@ export const statusHistoryQuerySchema = z
days: z
.string()
.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(
z.object({
days: z.number().int().min(1).max(365)
days: z.number().int().min(1).max(365),
tzOffsetMinutes: z.number().int().min(-720).max(840)
})
);
@@ -133,15 +158,15 @@ export function computeBuckets(
id: number;
}[],
days: number,
priorStatus: string | null = null
priorStatus: string | null = null,
tzOffsetMinutes: number = 0
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
const nowSec = Math.floor(Date.now() / 1000);
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
// and don't drift as the cache expires and is recomputed
const utcToday = new Date();
utcToday.setUTCHours(0, 0, 0, 0);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
// Anchor bucket boundaries to local midnight (UTC when tzOffsetMinutes is
// 0) so dates are stable calendar days for the requesting client and
// don't drift as the cache expires and is recomputed
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const buckets: StatusHistoryDayBucket[] = [];
let totalDowntime = 0;
@@ -237,7 +262,11 @@ export function computeBuckets(
)
: 100;
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
// Shift by the client's offset before formatting so the label reflects
// their local calendar date rather than the UTC date of dayStartSec
const dateStr = new Date((dayStartSec + tzOffsetMinutes * 60) * 1000)
.toISOString()
.slice(0, 10);
const hasAnyData = currentStatus !== null || dayEvents.length > 0;
@@ -270,6 +299,123 @@ export function computeBuckets(
status
});
}
return { buckets, totalDowntime };
}
export type BatchedStatusHistoryResponse = Record<
string,
StatusHistoryResponse
>;
export async function getBatchedStatusHistory(
entityType: string,
entityIds: number[],
days: number,
tzOffsetMinutes: number = 0
): Promise<BatchedStatusHistoryResponse> {
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const startSec = todayMidnightSec - days * 86400;
const events = await logsDb
.select()
.from(statusHistory)
.where(
and(
eq(statusHistory.entityType, entityType),
inArray(statusHistory.entityId, entityIds),
gte(statusHistory.timestamp, startSec)
)
)
.orderBy(asc(statusHistory.timestamp));
// Fetch the last known state before the window so that entities that
// haven't changed status recently still show the correct status rather
// than appearing as "no_data".
/**
* If we used only postgres, we would have used `SELECT DISTINCT ON` to get the
* latest event for each `entityId`,
* but it doesn't work on SQLite, so instead we use a subquery,
* the `ROW_NUMBER() OVER PARTITION` allows to assign a number
* to each row ordered by the timestamp, the number 1 is the first one appearing in
* the specified order, then the next and more, we only want the highest timestamp,
* so we get for `row_number=1`
*/
const lastKnowEventsSub = logsDb
.select({
entityId: statusHistory.entityId,
status: statusHistory.status,
timestamp: statusHistory.timestamp,
row_number:
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${statusHistory.entityId} ORDER BY ${statusHistory.timestamp} DESC)`.as(
"row_number"
)
})
.from(statusHistory)
.where(
and(
eq(statusHistory.entityType, entityType),
inArray(statusHistory.entityId, entityIds),
lt(statusHistory.timestamp, startSec)
)
)
.as("sub");
const lastKnownEvents = await logsDb
.select({
entityId: lastKnowEventsSub.entityId,
status: lastKnowEventsSub.status,
timestamp: lastKnowEventsSub.timestamp
})
.from(lastKnowEventsSub)
.where(eq(lastKnowEventsSub.row_number, 1));
const eventStatusMap: Record<
number,
{
events: typeof events;
lastKnownEvent: (typeof lastKnownEvents)[number] | null;
}
> = {};
for (const entityId of entityIds) {
eventStatusMap[entityId] = {
events: events.filter((ev) => ev.entityId === entityId),
lastKnownEvent:
lastKnownEvents.find((ev) => ev.entityId === entityId) ?? null
};
}
const result: BatchedStatusHistoryResponse = {};
for (const entityId in eventStatusMap) {
const event = eventStatusMap[Number(entityId)];
const priorStatus = event.lastKnownEvent?.status ?? null;
const { buckets, totalDowntime } = computeBuckets(
event.events,
days,
priorStatus,
tzOffsetMinutes
);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
? Math.max(
0,
((totalWindow - totalDowntime) / totalWindow) * 100
)
: 100;
result[entityId] = {
entityType,
entityId: Number(entityId),
days: buckets,
overallUptimePercent: Math.round(overallUptime * 100) / 100,
totalDowntimeSeconds: totalDowntime
};
}
return result;
}
+10
View File
@@ -2,6 +2,7 @@ import {
db,
Org,
orgs,
resourceAccessToken,
resources,
siteResources,
sites,
@@ -83,6 +84,15 @@ export async function removeUserFromOrg(
.delete(userOrgs)
.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(
and(
eq(userResources.userId, userId),
+19
View File
@@ -1,10 +1,29 @@
import {
getResourceRuleValueValidationError,
isValidDomain,
isValidUrlGlobPattern
} from "./validators";
import { assertEquals } from "@test/assert";
function runTests() {
console.log("Running domain validation tests...");
assertEquals(
isValidDomain("example.com"),
true,
"Standard ASCII domain should be valid"
);
assertEquals(
isValidDomain("xn--e1afmkfd.xn--p1ai"),
true,
"Punycode IDN domain should be valid"
);
assertEquals(
isValidDomain("example.invalid-tld"),
false,
"Domain with unknown TLD should be invalid"
);
console.log("Running URL pattern validation tests...");
// Test valid patterns
+3 -2
View File
@@ -171,9 +171,10 @@ export function isValidDomain(domain: string): boolean {
if (!/^[a-zA-Z0-9-]+$/.test(label)) return false;
}
// TLD should be at least 2 characters and contain only letters
// TLD should be at least 2 characters. Punycode TLDs can contain digits
// and hyphens, so validity is ultimately enforced by the TLD allowlist.
const tld = labels[labels.length - 1];
if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) return false;
if (tld.length < 2) return false;
// Check if TLD is in the list of valid TLDs
if (!validTlds.includes(tld.toUpperCase())) return false;
+1
View File
@@ -17,3 +17,4 @@ export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess";
export * from "./verifyApiKeyDomainAccess";
export * from "./verifyApiKeyResourcePolicyAccess";
export * from "./verifyApiKeySiteProvisioningKeyAccess";
@@ -0,0 +1,111 @@
import { Request, Response, NextFunction } from "express";
import {
db,
siteProvisioningKeys,
siteProvisioningKeyOrg,
apiKeyOrg
} from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getFirstString } from "@server/lib/requestParams";
export async function verifyApiKeySiteProvisioningKeyAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const siteProvisioningKeyId =
getFirstString(req.params.siteProvisioningKeyId) ||
getFirstString(req.body.siteProvisioningKeyId) ||
getFirstString(req.query.siteProvisioningKeyId);
const orgId = getFirstString(req.params.orgId);
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!siteProvisioningKeyId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
if (apiKey.isRoot) {
// Root keys can access any site provisioning key in any org
return next();
}
const [row] = await db
.select()
.from(siteProvisioningKeys)
.innerJoin(
siteProvisioningKeyOrg,
and(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.limit(1);
if (!row?.siteProvisioningKeys || !row.siteProvisioningKeyOrg) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site provisioning key with ID ${siteProvisioningKeyId} not found for organization ${orgId}`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, row.siteProvisioningKeyOrg.orgId)
)
)
.limit(1);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site provisioning key access"
)
);
}
}
+19 -7
View File
@@ -4,22 +4,34 @@ export const registry = new OpenAPIRegistry();
export enum OpenAPITags {
Site = "Site",
Org = "Organization",
PublicResource = "Public Resource",
Target = "Resource Target",
PrivateResource = "Private Resource",
Policy = "Policy",
Client = "Client",
Org = "Organization",
Domain = "Domain",
PublicResourcePolicy = "Public Resource Policy",
Role = "Role",
User = "User",
Invitation = "User Invitation",
Target = "Resource Target",
Rule = "Rule",
Invitation = "User Invitation",
AccessToken = "Access Token",
GlobalIdp = "Identity Provider (Global)",
OrgIdp = "Identity Provider (Organization Only)",
Client = "Client",
ApiKey = "API Key",
Domain = "Domain",
SiteProvisioningKey = "Site Provisioning Key",
Blueprint = "Blueprint",
Ssh = "SSH",
Logs = "Logs"
Logs = "Logs",
EventStreamingDestination = "Event Streaming Destination",
AlertRule = "Alert Rule",
HealthCheck = "Health Check",
PublicResourcePolicyLegacy = "Public Resource Policy (Legacy)",
PublicResourceLegacy = "Public Resource (Legacy)",
PrivateResourceLegacy = "Private Resource (Legacy)"
}
// Order here controls the order tags are displayed in Swagger UI
export const openApiTags = Object.values(OpenAPITags).map((name) => ({
name
}));
@@ -191,7 +191,7 @@ registry.registerPath({
method: "put",
path: "/org/{orgId}/alert-rule",
description: "Create an alert rule for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.AlertRule],
request: {
params: paramsSchema,
body: {
@@ -34,7 +34,7 @@ registry.registerPath({
method: "delete",
path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Delete an alert rule for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.AlertRule],
request: {
params: paramsSchema
},
@@ -48,7 +48,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Get a specific alert rule for an organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.AlertRule],
request: {
params: paramsSchema
},
@@ -90,7 +90,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/alert-rules",
description: "List all alert rules for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.AlertRule],
request: {
query: querySchema,
params: paramsSchema
@@ -158,7 +158,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Update an alert rule for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.AlertRule],
request: {
params: paramsSchema,
body: {
@@ -0,0 +1,190 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { certificates, db, domains, orgDomains } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { type GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import HttpCode from "@server/types/HttpCode";
import { and, eq, inArray, or } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const getCertificateParamSchema = z.strictObject({
orgId: z.string()
});
const getCertificateQuerySchema = z.object({
domains: z.preprocess(
(val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
// the array is returned as this
if (typeof val === "string") {
return val.split(",");
}
return undefined;
},
z.array(z.string().min(1).max(255))
)
});
async function query(orgId: string, domainList: string[]) {
// Try to get CNAME certificates first
let existingCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.innerJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.where(and(inArray(certificates.domain, domainList)));
// All non resolved domain certificates might be `ns` or `wildcard`,
// which means exact domain certificates do not
const nonAvailableCertificates = existingCertificates
.filter((cert) => !domainList.includes(cert.domain))
.map((cert) => cert.domain);
if (nonAvailableCertificates.length > 0) {
const domainLevelDownSet = new Set<string>();
const wildcardDomainSet = new Set<string>();
for (const domain of nonAvailableCertificates) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
domainLevelDownSet.add(domainLevelDown);
wildcardDomainSet.add(wildcardPrefixed);
}
// Need to map the certificates to each domain
const wildcardCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.innerJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.where(
and(
eq(certificates.wildcard, true),
or(
inArray(certificates.domain, [...domainLevelDownSet]),
inArray(certificates.domain, [...wildcardDomainSet])
)
)
);
existingCertificates.push(...wildcardCertificates);
}
const certificateMap: Record<string, any> = {};
for (const domain of domainList) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
certificateMap[domain] =
existingCertificates.find(
(cert) =>
cert.domain === domain ||
cert.domain === domainLevelDown ||
cert.domain === wildcardPrefixed
) ?? null;
}
return certificateMap;
}
export async function getBatchedCertificates(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getCertificateParamSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedQuery = getCertificateQuerySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { domains } = parsedQuery.data;
const cert = await query(orgId, domains);
return response<GetBatchedCertificateResponse>(res, {
data: cert,
success: true,
error: false,
message: "Certificates retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -14,3 +14,4 @@
export * from "./getCertificate";
export * from "./restartCertificate";
export * from "./syncCertToNewts";
export * from "./getBatchedCertificates";
@@ -11,18 +11,16 @@
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { certificates, db } from "@server/db";
import { sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import stoi from "@server/lib/stoi";
import { registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const restartCertificateParamsSchema = z.strictObject({
certId: z.coerce.number().int().positive(),
@@ -52,7 +52,7 @@ registry.registerPath({
method: "put",
path: "/org/{orgId}/event-streaming-destination",
description: "Create an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.EventStreamingDestination],
request: {
params: paramsSchema,
body: {
@@ -35,7 +35,7 @@ registry.registerPath({
path: "/org/{orgId}/event-streaming-destination/{destinationId}",
description:
"Delete an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.EventStreamingDestination],
request: {
params: paramsSchema
},
@@ -109,7 +109,7 @@ registry.registerPath({
path: "/org/{orgId}/event-streaming-destination",
description:
"List all event streaming destinations for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.EventStreamingDestination],
request: {
query: querySchema,
params: paramsSchema
@@ -55,7 +55,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/event-streaming-destination/{destinationId}",
description: "Update an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.EventStreamingDestination],
request: {
params: paramsSchema,
body: {
+15
View File
@@ -175,6 +175,13 @@ authenticated.get(
certificates.getCertificate
);
authenticated.get(
"/org/:orgId/batched-certificates",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getCertificate),
certificates.getBatchedCertificates
);
authenticated.post(
"/org/:orgId/certificate/:certId/restart",
verifyValidLicense,
@@ -853,6 +860,14 @@ authenticated.get(
healthChecks.getHealthCheckStatusHistory
);
authenticated.get(
"/org/:orgId/health-check-status-histories",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getTarget),
healthChecks.getBatchedHealthCheckStatusHistory
);
authenticated.get(
"/client/:clientId/verify-associations-cache",
verifyClientAccess,
@@ -75,7 +75,7 @@ registry.registerPath({
method: "put",
path: "/org/{orgId}/health-check",
description: "Create a health check for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.HealthCheck],
request: {
params: paramsSchema,
body: {
@@ -37,7 +37,7 @@ registry.registerPath({
method: "delete",
path: "/org/{orgId}/health-check/{healthCheckId}",
description: "Delete a health check for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.HealthCheck],
request: {
params: paramsSchema
},
@@ -0,0 +1,96 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import response from "@server/lib/response";
import {
getBatchedStatusHistory,
type BatchedStatusHistoryResponse
} from "@server/lib/statusHistory";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const healthCheckIdParamsSchema = z.object({
days: z
.string()
.optional()
.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)),
healthCheckIds: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
const raw = Array.isArray(val) ? val : [val];
const nums = raw
.map((v) =>
typeof v === "string" ? parseInt(v, 10) : Number(v)
)
.filter((n) => Number.isInteger(n) && n > 0);
const unique = [...new Set(nums)];
return unique.length ? unique : undefined;
}, z.array(z.number().int().positive()))
.openapi({
description: "Filter by healthCheckIds (repeat query param)"
})
});
export async function getBatchedHealthCheckStatusHistory(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = healthCheckIdParamsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const entityType = "health_check";
const { days, healthCheckIds, tzOffsetMinutes } = parsedQuery.data;
const data = await getBatchedStatusHistory(
entityType,
healthCheckIds,
days,
tzOffsetMinutes
);
return response<BatchedStatusHistoryResponse>(res, {
data,
success: true,
error: false,
message: "Status history retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -55,9 +55,14 @@ export async function getHealthCheckStatusHistory(
const entityType = "health_check";
const entityId = parsedParams.data.healthCheckId;
const { days } = parsedQuery.data;
const { days, tzOffsetMinutes } = parsedQuery.data;
const data = await getCachedStatusHistory(entityType, entityId, days);
const data = await getCachedStatusHistory(
entityType,
entityId,
days,
tzOffsetMinutes
);
return response<StatusHistoryResponse>(res, {
data,
@@ -16,3 +16,4 @@ export * from "./createHealthCheck";
export * from "./updateHealthCheck";
export * from "./deleteHealthCheck";
export * from "./getStatusHistory";
export * from "./getBatchedStatusHistory";
@@ -63,7 +63,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/health-checks",
description: "List health checks for an organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.HealthCheck],
request: {
params: paramsSchema,
query: querySchema
@@ -109,7 +109,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/health-check/{healthCheckId}",
description: "Update a health check for a specific organization.",
tags: [OpenAPITags.Org],
tags: [OpenAPITags.HealthCheck],
request: {
params: paramsSchema,
body: {
+317 -2
View File
@@ -58,7 +58,8 @@ import {
resourceRules,
resourcePolicyRules,
userOrgRoles,
roles
roles,
resourceAccessToken
} from "@server/db";
import { eq, and, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
import { alias } from "@server/db";
@@ -81,11 +82,16 @@ import config from "@server/lib/config";
import { exchangeSession } from "@server/routers/badger";
import {
ResourceSessionValidationResult,
createResourceSession,
serializeResourceSessionCookie,
validateResourceSessionToken
} from "@server/auth/sessions/resource";
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
import { maxmindLookup } from "@server/db/maxmind";
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 { maxmindAsnLookup } from "@server/db/maxmindAsn";
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
@@ -178,6 +184,73 @@ const validateResourceAccessTokenBodySchema = z.strictObject({
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
const getCertificatesByDomainsQuerySchema = z.strictObject({
// Accept domains as string or array (domains or domains[])
@@ -1829,8 +1902,23 @@ hybridRouter.post(
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, {
data: result,
data: {
...result,
userData
},
success: true,
error: false,
message: result.valid
@@ -1850,6 +1938,233 @@ hybridRouter.post(
}
);
// Create a resource session from a valid access token (for remote nodes)
hybridRouter.post(
"/resource/:resourceId/session/create-access-token",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = createAccessTokenSessionParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = createAccessTokenSessionBodySchema.safeParse(
req.body
);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const { accessTokenId } = parsedBody.data;
const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
.limit(1);
if (!tokenItem || tokenItem.resourceId !== resourceId) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Access token not found"
)
);
}
if (!tokenItem.persistSession) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Access token does not allow session persistence"
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!resource || !resource.fullDomain) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
const token = generateSessionToken();
const sess = await createResourceSession({
resourceId: resource.resourceId,
token,
accessTokenId: tokenItem.accessTokenId,
sessionLength: tokenItem.sessionLength,
expiresAt: tokenItem.expiresAt,
doNotExtend: tokenItem.expiresAt ? true : false
});
const cookieName = config.getRawConfig().server.session_cookie_name;
const cookie = serializeResourceSessionCookie(
cookieName,
resource.fullDomain,
token,
!resource.ssl,
new Date(sess.expiresAt)
);
return response(res, {
data: { cookie },
success: true,
error: false,
message: "Access token session created successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create access token session"
)
);
}
}
);
// Resolve access token metadata for remote nodes (cookie session path)
hybridRouter.get(
"/resource/access-token/:accessTokenId",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getAccessTokenParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { accessTokenId } = parsedParams.data;
const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
.limit(1);
if (!tokenItem) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Access token not found"
)
);
}
let userData: AccessTokenUserData | undefined;
if (tokenItem.userId) {
userData = await resolveAccessTokenUserData(
tokenItem.userId,
tokenItem.orgId
);
}
return response(res, {
data: { tokenItem, userData },
success: true,
error: false,
message: "Access token retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get access token"
)
);
}
}
);
// Access audit log from remote nodes
hybridRouter.post(
"/logs/access",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedBody = logAccessAuditBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
if (
await checkExitNodeOrg(
remoteExitNode.exitNodeId,
parsedBody.data.orgId
)
) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Exit node not allowed for this organization"
)
);
}
await logAccessAudit(parsedBody.data);
return response(res, {
data: null,
success: true,
error: false,
message: "Access audit log saved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to save access audit log"
)
);
}
}
);
const geoIpLookupParamsSchema = z.object({
ip: z.union([z.ipv4(), z.ipv6()])
});
+196
View File
@@ -16,6 +16,11 @@ import * as org from "#private/routers/org";
import * as logs from "#private/routers/auditLogs";
import * as alertEvents from "#private/routers/alertEvents";
import * as certificates from "#private/routers/certificates";
import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as policy from "#private/routers/policy";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
import {
verifyApiKeyHasAction,
@@ -24,6 +29,8 @@ import {
verifyApiKeyIdpAccess,
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyApiKeySiteProvisioningKeyAccess,
verifyApiKeyResourcePolicyAccess,
verifyLimits
} from "@server/middlewares";
import * as user from "#private/routers/user";
@@ -215,3 +222,192 @@ authenticated.delete(
logActionAudit(ActionsEnum.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
);
authenticated.get(
["/org/:orgId/resource-policies", "/org/:orgId/public-resource-policies"],
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.listResourcePolicies),
logActionAudit(ActionsEnum.listResourcePolicies),
policy.listResourcePolicies
);
authenticated.post(
["/org/:orgId/resource-policy", "/org/:orgId/public-resource-policy"],
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourcePolicy),
logActionAudit(ActionsEnum.createResourcePolicy),
policy.createResourcePolicy
);
authenticated.delete(
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.deleteResourcePolicy),
logActionAudit(ActionsEnum.deleteResourcePolicy),
policy.deleteResourcePolicy
);
authenticated.put(
"/org/:orgId/event-streaming-destination",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createEventStreamingDestination),
logActionAudit(ActionsEnum.createEventStreamingDestination),
eventStreamingDestination.createEventStreamingDestination
);
authenticated.post(
"/org/:orgId/event-streaming-destination/:destinationId",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateEventStreamingDestination),
logActionAudit(ActionsEnum.updateEventStreamingDestination),
eventStreamingDestination.updateEventStreamingDestination
);
authenticated.delete(
"/org/:orgId/event-streaming-destination/:destinationId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteEventStreamingDestination),
logActionAudit(ActionsEnum.deleteEventStreamingDestination),
eventStreamingDestination.deleteEventStreamingDestination
);
authenticated.get(
"/org/:orgId/event-streaming-destinations",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listEventStreamingDestinations),
eventStreamingDestination.listEventStreamingDestinations
);
authenticated.put(
"/org/:orgId/alert-rule",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createAlertRule),
logActionAudit(ActionsEnum.createAlertRule),
alertRule.createAlertRule
);
authenticated.post(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateAlertRule),
logActionAudit(ActionsEnum.updateAlertRule),
alertRule.updateAlertRule
);
authenticated.delete(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteAlertRule),
logActionAudit(ActionsEnum.deleteAlertRule),
alertRule.deleteAlertRule
);
authenticated.get(
"/org/:orgId/alert-rules",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listAlertRules),
alertRule.listAlertRules
);
authenticated.get(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.getAlertRule),
alertRule.getAlertRule
);
authenticated.get(
"/org/:orgId/health-checks",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listHealthChecks),
healthChecks.listHealthChecks
);
authenticated.put(
"/org/:orgId/health-check",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createHealthCheck),
logActionAudit(ActionsEnum.createHealthCheck),
healthChecks.createHealthCheck
);
authenticated.post(
"/org/:orgId/health-check/:healthCheckId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateHealthCheck),
logActionAudit(ActionsEnum.updateHealthCheck),
healthChecks.updateHealthCheck
);
authenticated.delete(
"/org/:orgId/health-check/:healthCheckId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteHealthCheck),
logActionAudit(ActionsEnum.deleteHealthCheck),
healthChecks.deleteHealthCheck
);
authenticated.get(
"/org/:orgId/health-check/:healthCheckId/status-history",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.getTarget),
healthChecks.getHealthCheckStatusHistory
);
@@ -14,8 +14,6 @@
import { hashPassword } from "@server/auth/password";
import {
db,
idp,
idpOrg,
orgs,
resourcePolicies,
resourcePolicyHeaderAuth,
@@ -31,6 +29,7 @@ import {
type ResourcePolicy
} from "@server/db";
import { getUniqueResourcePolicyName } from "@server/db/names";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
import response from "@server/lib/response";
import {
getResourceRuleValueValidationError,
@@ -121,7 +120,7 @@ registry.registerPath({
method: "post",
path: "/org/{orgId}/resource-policy",
description: "Create a resource policy.",
tags: [OpenAPITags.Org, OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: createResourcePolicyParamsSchema,
body: {
@@ -204,14 +203,9 @@ export async function createResourcePolicy(
// Check if Identity provider in `skipToIdpId` exists
if (skipToIdpId) {
const [provider] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId)))
.limit(1);
const providerExists = await idpExistsForOrg(skipToIdpId, orgId);
if (!provider) {
if (!providerExists) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
@@ -31,7 +31,7 @@ registry.registerPath({
method: "delete",
path: "/resource-policy/{resourcePolicyId}",
description: "Delete a resource policy.",
tags: [OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: deleteResourcePolicySchema
},
@@ -79,7 +79,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/resource-policies",
description: "List resource policies for an organization.",
tags: [OpenAPITags.Org, OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: z.object({
orgId: z.string()
@@ -26,6 +26,8 @@ import {
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
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({
orgId: z.string().nonempty()
@@ -34,10 +36,17 @@ const paramsSchema = z.object({
const bodySchema = z
.strictObject({
name: z.string().min(1).max(255),
maxBatchSize: z.union([
z.null(),
z.coerce.number().int().positive().max(1_000_000)
]),
maxBatchSize: z
.union([
z.null(),
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(),
approveNewSites: z.boolean().optional().default(true)
})
@@ -57,6 +66,49 @@ const bodySchema = z
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(
req: Request,
res: Response,
@@ -13,23 +13,46 @@
import { Request, Response, NextFunction } from "express";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.object({
siteProvisioningKeyId: 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(
req: Request,
res: Response,
@@ -11,11 +11,7 @@
* 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 HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
@@ -25,6 +21,8 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
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({
orgId: z.string().nonempty()
@@ -45,11 +43,55 @@ const querySchema = z.object({
.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) {
return db
.select({
siteProvisioningKeyId:
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId: siteProvisioningKeys.siteProvisioningKeyId,
orgId: siteProvisioningKeyOrg.orgId,
lastChars: siteProvisioningKeys.lastChars,
createdAt: siteProvisioningKeys.createdAt,
@@ -13,11 +13,7 @@
import { Request, Response, NextFunction } from "express";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -25,6 +21,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
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({
siteProvisioningKeyId: z.string().nonempty(),
@@ -38,7 +36,13 @@ const bodySchema = z
z.null(),
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(),
approveNewSites: z.boolean().optional()
})
@@ -50,7 +54,8 @@ const bodySchema = z
) {
ctx.addIssue({
code: "custom",
message: "Provide maxBatchSize and/or validUntil and/or approveNewSites",
message:
"Provide maxBatchSize and/or validUntil and/or approveNewSites",
path: ["maxBatchSize"]
});
}
@@ -69,6 +74,48 @@ const bodySchema = z
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(
req: Request,
res: Response,
@@ -1,4 +1,3 @@
import { hash } from "@node-rs/argon2";
import {
generateId,
generateIdFromEntropySize,
@@ -8,18 +7,18 @@ import { db } from "@server/db";
import {
ResourceAccessToken,
resourceAccessToken,
resources
resources,
userOrgs
} from "@server/db";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { createDate, TimeSpan } from "oslo";
import { hashPassword } from "@server/auth/password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { OpenAPITags, registry } from "@server/openApi";
@@ -28,7 +27,9 @@ export const generateAccessTokenBodySchema = z.strictObject({
validForSeconds: z.int().positive().optional(), // seconds
title: 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({
@@ -44,6 +45,39 @@ registry.registerPath({
method: "post",
path: "/resource/{resourceId}/access-token",
description: "Generate a new access token for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: generateAccssTokenParamsSchema,
body: {
content: {
"application/json": {
schema: generateAccessTokenBodySchema
}
}
}
},
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()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/access-token",
description: "Generate a new access token for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
request: {
params: generateAccssTokenParamsSchema,
@@ -101,7 +135,14 @@ export async function generateAccessToken(
}
const { resourceId } = parsedParams.data;
const { validForSeconds, title, path, description } = parsedBody.data;
const {
validForSeconds,
title,
path,
description,
persistSession,
userId
} = parsedBody.data;
const [resource] = await db
.select()
@@ -112,6 +153,28 @@ export async function generateAccessToken(
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 {
const sessionLength = validForSeconds
? validForSeconds * 1000
@@ -133,23 +196,27 @@ export async function generateAccessToken(
accessTokenId: id,
orgId: resource.orgId,
resourceId,
userId: userId || null,
tokenHash,
expiresAt: expiresAt || null,
sessionLength: sessionLength,
title: title || null,
path: path || null,
description: description || null,
persistSession,
createdAt: new Date().getTime()
})
.returning({
accessTokenId: resourceAccessToken.accessTokenId,
orgId: resourceAccessToken.orgId,
resourceId: resourceAccessToken.resourceId,
userId: resourceAccessToken.userId,
expiresAt: resourceAccessToken.expiresAt,
sessionLength: resourceAccessToken.sessionLength,
title: resourceAccessToken.title,
path: resourceAccessToken.path,
description: resourceAccessToken.description,
persistSession: resourceAccessToken.persistSession,
createdAt: resourceAccessToken.createdAt
})
.execute();
+39 -2
View File
@@ -6,12 +6,13 @@ import {
userResources,
roleResources,
resourceAccessToken,
sites
sites,
users
} from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
import { sql, eq, or, inArray, and, count, isNull, gt } from "drizzle-orm";
import logger from "@server/logger";
import stoi from "@server/lib/stoi";
import { fromZodError } from "zod-validation-error";
@@ -55,11 +56,16 @@ function queryAccessTokens(
accessTokenId: resourceAccessToken.accessTokenId,
orgId: resourceAccessToken.orgId,
resourceId: resourceAccessToken.resourceId,
userId: resourceAccessToken.userId,
userName: users.name,
username: users.username,
userEmail: users.email,
sessionLength: resourceAccessToken.sessionLength,
expiresAt: resourceAccessToken.expiresAt,
tokenHash: resourceAccessToken.tokenHash,
title: resourceAccessToken.title,
description: resourceAccessToken.description,
persistSession: resourceAccessToken.persistSession,
createdAt: resourceAccessToken.createdAt,
resourceName: resources.name,
resourceNiceId: resources.niceId,
@@ -75,6 +81,7 @@ function queryAccessTokens(
eq(resourceAccessToken.resourceId, resources.resourceId)
)
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
.where(
and(
inArray(
@@ -97,6 +104,7 @@ function queryAccessTokens(
eq(resourceAccessToken.resourceId, resources.resourceId)
)
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
.where(
and(
inArray(
@@ -151,6 +159,35 @@ registry.registerPath({
method: "get",
path: "/resource/{resourceId}/access-tokens",
description: "List all access tokens for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: z.object({
resourceId: z.number()
}),
query: listAccessTokensSchema
},
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()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/access-tokens",
description: "List all access tokens for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
request: {
params: z.object({
+15 -10
View File
@@ -16,18 +16,26 @@ export async function logout(
next: NextFunction
): Promise<any> {
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 (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Log out failed because missing or invalid session. IP: ${req.ip}.`
`Log out with missing or invalid session. IP: ${req.ip}.`
);
}
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"You must be logged in to sign out"
)
);
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Logged out successfully",
status: HttpCode.OK
});
}
try {
@@ -37,9 +45,6 @@ export async function logout(
logger.error("Failed to invalidate session", error);
}
const isSecure = req.protocol === "https";
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
return response<null>(res, {
data: null,
success: true,
+1 -1
View File
@@ -99,7 +99,7 @@ export async function requestPasswordReset(
});
});
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`;
const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(email)}&token=${token}`;
if (!config.getRawConfig().email) {
logger.info(
@@ -236,6 +236,38 @@ function runTests() {
"Root path should not match non-root path"
);
// Path traversal / encoded-slash bypass regression tests
assertEquals(
isPathAllowed("public/*", "public/../admin"),
false,
"Literal .. traversal out of an allowed prefix must not match"
);
assertEquals(
isPathAllowed("public/*", "public%2F..%2Fadmin"),
false,
"Encoded-slash traversal out of an allowed prefix must not match"
);
assertEquals(
isPathAllowed("public/*", "public%2f..%2fadmin%2ffile"),
false,
"Encoded-slash traversal is case-insensitively decoded before matching"
);
assertEquals(
isPathAllowed("admin/*", "public/../admin/secret"),
true,
".. traversal INTO a restricted path should still be caught by its own rule"
);
assertEquals(
isPathAllowed("public/*", "public%2Ffoo"),
true,
"Encoded slash without traversal should still resolve and match normally"
);
assertEquals(
isPathAllowed("public/*", "public/./foo"),
true,
"Single-dot segments are a no-op and should not affect matching"
);
console.log("All path matching tests passed!");
}
+274 -41
View File
@@ -1,4 +1,9 @@
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
import {
createResourceSession,
serializeResourceSessionCookie,
validateResourceSessionToken
} from "@server/auth/sessions/resource";
import { generateSessionToken } from "@server/auth/sessions/app";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import {
getResourceByDomain,
@@ -13,6 +18,7 @@ import {
LoginPage,
Org,
Resource,
ResourceAccessToken,
ResourceHeaderAuth,
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword,
@@ -21,7 +27,10 @@ import {
ResourcePolicyPassword,
ResourcePolicyHeaderAuth,
ResourceRule,
ResourceSession
ResourceSession,
db,
resourceAccessToken,
users
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
@@ -41,11 +50,13 @@ import {
enforceResourceSessionLength
} from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
import { REGIONS } from "@server/db/regions";
import { localCache } from "#dynamic/lib/cache";
import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { eq } from "drizzle-orm";
const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(),
@@ -350,22 +361,15 @@ export async function verifyResourceSession(
}
if (valid && tokenItem) {
logRequestAudit(
{
action: true,
reason: 102, // valid access token
resourceId: resource.resourceId,
orgId: resource.orgId,
location: ipCC,
apiKey: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
return await allowAccessToken(
res,
resource,
tokenItem,
sessions,
dontStripSession,
parsedBody.data,
ipCC
);
return allowed(res, undefined, dontStripSession);
}
}
@@ -401,27 +405,20 @@ export async function verifyResourceSession(
}
if (valid && tokenItem) {
logRequestAudit(
{
action: true,
reason: 102, // valid access token
resourceId: resource.resourceId,
orgId: resource.orgId,
location: ipCC,
apiKey: {
name: tokenItem.title,
apiKeyId: tokenItem.accessTokenId
}
},
parsedBody.data
return await allowAccessToken(
res,
resource,
tokenItem,
sessions,
dontStripSession,
parsedBody.data,
ipCC
);
return allowed(res, undefined, dontStripSession);
}
}
// check for HTTP Basic Auth header
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
const clientHeaderAuthKey = `headerAuth:${resource.resourceId}:${clientHeaderAuth}`;
if (headerAuth && clientHeaderAuth) {
if (localCache.get(clientHeaderAuthKey)) {
logger.debug(
@@ -666,22 +663,37 @@ export async function verifyResourceSession(
"Resource allowed because access token session is valid"
);
logRequestAudit(
const [tokenItem] = await db
.select()
.from(resourceAccessToken)
.where(
eq(
resourceAccessToken.accessTokenId,
resourceSession.accessTokenId
)
)
.limit(1);
const userData = tokenItem
? await getAccessTokenUserData(
tokenItem,
resource.orgId
)
: undefined;
logAccessTokenRequestAudit(
{
action: true,
reason: 102, // valid access token
resourceId: resource.resourceId,
orgId: resource.orgId,
location: ipCC,
apiKey: {
name: null,
apiKeyId: resourceSession.accessTokenId
}
accessTokenId: resourceSession.accessTokenId,
tokenTitle: tokenItem?.title ?? null,
userData
},
parsedBody.data
);
return allowed(res, undefined, dontStripSession);
return allowed(res, userData, dontStripSession);
}
if (resourceSession.userSessionId && sso) {
@@ -892,6 +904,227 @@ function allowed(
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(
res: Response,
redirectPath?: string,
+9 -2
View File
@@ -1,9 +1,11 @@
import type { Domain } from "@server/db";
export type GetCertificateResponse = {
certId: number;
domain: string;
domainId: string;
wildcard: boolean;
domainType: string;
domainType: Domain["type"];
status: string; // pending, requested, valid, expired, failed
expiresAt: string | null;
lastRenewalAttempt: Date | null;
@@ -11,4 +13,9 @@ export type GetCertificateResponse = {
updatedAt: number;
errorMessage?: string | null;
renewalCount: number;
};
};
export type GetBatchedCertificateResponse = Record<
string,
GetCertificateResponse | null
>;
+26 -1
View File
@@ -33,7 +33,7 @@ const UpdateDomainResponseDataSchema = z.object({
registry.registerPath({
method: "patch",
method: "post",
path: "/org/{orgId}/domain/{domainId}",
description: "Update a domain by domainId.",
tags: [OpenAPITags.Domain],
@@ -55,6 +55,31 @@ registry.registerPath({
}
});
registry.registerPath({
method: "patch",
path: "/org/{orgId}/domain/{domainId}",
description:
"Update a domain by domainId. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.Domain],
request: {
params: z.object({
domainId: z.string(),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateDomainResponseDataSchema)
}
}
}
}
});
export async function updateOrgDomain(
req: Request,
res: Response,
+36 -1
View File
@@ -248,6 +248,22 @@ authenticated.post(
site.updateSite
);
authenticated.post(
"/site/:siteId/approve",
verifySiteAccess,
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
logActionAudit(ActionsEnum.updateSiteApprovals),
site.approveSite
);
authenticated.post(
"/site/:siteId/reject",
verifySiteAccess,
verifyUserHasAction(ActionsEnum.updateSiteApprovals),
logActionAudit(ActionsEnum.updateSiteApprovals),
site.rejectSite
);
authenticated.delete(
"/site/:siteId",
verifySiteAccess,
@@ -303,6 +319,13 @@ authenticated.get(
site.getSiteStatusHistory
);
authenticated.get(
"/org/:orgId/site-status-histories",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listSites),
site.getBatchedSiteStatusHistory
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site-resource",
@@ -453,6 +476,13 @@ authenticated.get(
resource.getResourceStatusHistory
);
authenticated.get(
"/org/:orgId/resource-status-histories",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listResources),
resource.getBatchedResourceStatusHistory
);
authenticated.get(
"/org/:orgId/resources",
verifyOrgAccess,
@@ -1381,6 +1411,12 @@ authenticated.put(
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
// Register setup-check BEFORE the global auth rate limiter.
// This endpoint is called on every dashboard root page load (pure boolean
// read, no secrets) and must not consume the auth rate-limit budget.
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
authRouter.use(
rateLimit({
windowMs:
@@ -1685,7 +1721,6 @@ authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl);
authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
authRouter.put("/set-server-admin", auth.setServerAdmin);
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
authRouter.post("/validate-setup-token", auth.validateSetupToken);
// Security Key routes
+267 -70
View File
@@ -138,6 +138,7 @@ authenticated.post(
logActionAudit(ActionsEnum.updateSite),
site.updateSite
);
authenticated.post(
"/org/:orgId/reset-bandwidth",
verifyApiKeyOrgAccess,
@@ -162,7 +163,7 @@ authenticated.get(
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site-resource",
["/org/:orgId/site-resource", "/org/:orgId/private-resource"],
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
@@ -171,7 +172,10 @@ authenticated.put(
);
authenticated.get(
"/org/:orgId/site/:siteId/resources",
[
"/org/:orgId/site/:siteId/resources",
"/org/:orgId/site/:siteId/private-resources"
],
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
@@ -179,21 +183,21 @@ authenticated.get(
);
authenticated.get(
"/org/:orgId/site-resources",
["/org/:orgId/site-resources", "/org/:orgId/private-resources"],
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
siteResource.listAllSiteResourcesByOrg
);
authenticated.get(
"/site-resource/:siteResourceId",
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
siteResource.getSiteResource
);
authenticated.post(
"/site-resource/:siteResourceId",
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
@@ -202,7 +206,7 @@ authenticated.post(
);
authenticated.delete(
"/site-resource/:siteResourceId",
["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
logActionAudit(ActionsEnum.deleteSiteResource),
@@ -210,28 +214,40 @@ authenticated.delete(
);
authenticated.get(
"/site-resource/:siteResourceId/roles",
[
"/site-resource/:siteResourceId/roles",
"/private-resource/:siteResourceId/roles"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
siteResource.listSiteResourceRoles
);
authenticated.get(
"/site-resource/:siteResourceId/users",
[
"/site-resource/:siteResourceId/users",
"/private-resource/:siteResourceId/users"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
siteResource.listSiteResourceUsers
);
authenticated.get(
"/site-resource/:siteResourceId/clients",
[
"/site-resource/:siteResourceId/clients",
"/private-resource/:siteResourceId/clients"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
siteResource.listSiteResourceClients
);
authenticated.post(
"/site-resource/:siteResourceId/roles",
[
"/site-resource/:siteResourceId/roles",
"/private-resource/:siteResourceId/roles"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -241,7 +257,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/users",
[
"/site-resource/:siteResourceId/users",
"/private-resource/:siteResourceId/users"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -251,7 +270,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/roles/add",
[
"/site-resource/:siteResourceId/roles/add",
"/private-resource/:siteResourceId/roles/add"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -261,7 +283,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/roles/remove",
[
"/site-resource/:siteResourceId/roles/remove",
"/private-resource/:siteResourceId/roles/remove"
],
verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -271,7 +296,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/users/add",
[
"/site-resource/:siteResourceId/users/add",
"/private-resource/:siteResourceId/users/add"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -281,7 +309,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/users/remove",
[
"/site-resource/:siteResourceId/users/remove",
"/private-resource/:siteResourceId/users/remove"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -291,7 +322,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/clients",
[
"/site-resource/:siteResourceId/clients",
"/private-resource/:siteResourceId/clients"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
@@ -301,7 +335,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/clients/add",
[
"/site-resource/:siteResourceId/clients/add",
"/private-resource/:siteResourceId/clients/add"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
@@ -311,7 +348,10 @@ authenticated.post(
);
authenticated.post(
"/site-resource/:siteResourceId/clients/remove",
[
"/site-resource/:siteResourceId/clients/remove",
"/private-resource/:siteResourceId/clients/remove"
],
verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients,
verifyLimits,
@@ -321,7 +361,7 @@ authenticated.post(
);
authenticated.post(
"/client/:clientId/site-resources",
["/client/:clientId/site-resources", "/client/:clientId/private-resources"],
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers),
@@ -329,7 +369,7 @@ authenticated.post(
);
authenticated.put(
"/org/:orgId/resource",
["/org/:orgId/resource", "/org/:orgId/public-resource"],
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
@@ -338,7 +378,10 @@ authenticated.put(
);
authenticated.put(
"/org/:orgId/site/:siteId/resource",
[
"/org/:orgId/site/:siteId/resource",
"/org/:orgId/site/:siteId/public-resource"
],
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource),
@@ -347,14 +390,14 @@ authenticated.put(
);
authenticated.get(
"/site/:siteId/resources",
["/site/:siteId/resources", "/site/:siteId/public-resources"],
verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listResources
);
authenticated.get(
"/org/:orgId/resources",
["/org/:orgId/resources", "/org/:orgId/public-resources"],
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listResources
@@ -383,6 +426,15 @@ authenticated.put(
domain.createOrgDomain
);
authenticated.post(
"/org/:orgId/domain/:domainId",
verifyApiKeyOrgAccess,
verifyApiKeyDomainAccess,
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
domain.updateOrgDomain
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.patch(
"/org/:orgId/domain/:domainId",
verifyApiKeyOrgAccess,
@@ -442,42 +494,45 @@ authenticated.delete(
);
authenticated.get(
"/resource/:resourceId/roles",
["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
resource.listResourceRoles
);
authenticated.get(
"/resource/:resourceId/users",
["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
resource.listResourceUsers
);
authenticated.get(
"/resource/:resourceId",
["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResource),
resource.getResource
);
authenticated.get(
"/resource-policy/:resourcePolicyId",
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
policy.getResourcePolicy
);
authenticated.get(
"/resource/:resourceId/policies",
["/resource/:resourceId/policies", "/public-resource/:resourceId/policies"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
resource.getResourcePolicies
);
authenticated.post(
"/resource/:resourceId",
["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResource),
@@ -485,15 +540,29 @@ authenticated.post(
resource.updateResource
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
policy.updateResourcePolicy
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId",
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
policy.updateResourcePolicy
);
authenticated.delete(
"/resource/:resourceId",
["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteResource),
logActionAudit(ActionsEnum.deleteResource),
@@ -501,7 +570,7 @@ authenticated.delete(
);
authenticated.put(
"/resource/:resourceId/target",
["/resource/:resourceId/target", "/public-resource/:resourceId/target"],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createTarget),
@@ -510,14 +579,14 @@ authenticated.put(
);
authenticated.get(
"/resource/:resourceId/targets",
["/resource/:resourceId/targets", "/public-resource/:resourceId/targets"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listTargets),
target.listTargets
);
authenticated.put(
"/resource/:resourceId/rule",
["/resource/:resourceId/rule", "/public-resource/:resourceId/rule"],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
@@ -526,14 +595,17 @@ authenticated.put(
);
authenticated.get(
"/resource/:resourceId/rules",
["/resource/:resourceId/rules", "/public-resource/:resourceId/rules"],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRules),
resource.listResourceRules
);
authenticated.post(
"/resource/:resourceId/rule/:ruleId",
[
"/resource/:resourceId/rule/:ruleId",
"/public-resource/:resourceId/rule/:ruleId"
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
@@ -542,7 +614,10 @@ authenticated.post(
);
authenticated.delete(
"/resource/:resourceId/rule/:ruleId",
[
"/resource/:resourceId/rule/:ruleId",
"/public-resource/:resourceId/rule/:ruleId"
],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
logActionAudit(ActionsEnum.deleteResourceRule),
@@ -624,7 +699,7 @@ authenticated.post(
);
authenticated.post(
"/resource/:resourceId/roles",
["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -634,7 +709,7 @@ authenticated.post(
);
authenticated.post(
"/resource/:resourceId/users",
["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -643,8 +718,11 @@ authenticated.post(
resource.setResourceUsers
);
authenticated.put(
"/resource-policy/:resourcePolicyId/access-control",
authenticated.post(
[
"/resource-policy/:resourcePolicyId/access-control",
"/public-resource-policy/:resourcePolicyId/access-control"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -655,8 +733,27 @@ authenticated.put(
policy.setResourcePolicyAccessControl
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId/password",
[
"/resource-policy/:resourcePolicyId/access-control",
"/public-resource-policy/:resourcePolicyId/access-control"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
verifyUserHasAction(ActionsEnum.setResourcePolicyRoles),
logActionAudit(ActionsEnum.setResourcePolicyUsers),
logActionAudit(ActionsEnum.setResourcePolicyRoles),
policy.setResourcePolicyAccessControl
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/password",
"/public-resource-policy/:resourcePolicyId/password"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
@@ -664,8 +761,24 @@ authenticated.put(
policy.setResourcePolicyPassword
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId/pincode",
[
"/resource-policy/:resourcePolicyId/password",
"/public-resource-policy/:resourcePolicyId/password"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
logActionAudit(ActionsEnum.setResourcePolicyPassword),
policy.setResourcePolicyPassword
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/pincode",
"/public-resource-policy/:resourcePolicyId/pincode"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
@@ -673,8 +786,24 @@ authenticated.put(
policy.setResourcePolicyPincode
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId/header-auth",
[
"/resource-policy/:resourcePolicyId/pincode",
"/public-resource-policy/:resourcePolicyId/pincode"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
logActionAudit(ActionsEnum.setResourcePolicyPincode),
policy.setResourcePolicyPincode
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/header-auth",
"/public-resource-policy/:resourcePolicyId/header-auth"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
@@ -682,8 +811,24 @@ authenticated.put(
policy.setResourcePolicyHeaderAuth
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId/whitelist",
[
"/resource-policy/:resourcePolicyId/header-auth",
"/public-resource-policy/:resourcePolicyId/header-auth"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
policy.setResourcePolicyHeaderAuth
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/whitelist",
"/public-resource-policy/:resourcePolicyId/whitelist"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
@@ -691,8 +836,37 @@ authenticated.put(
policy.setResourcePolicyWhitelist
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
"/resource-policy/:resourcePolicyId/rules",
[
"/resource-policy/:resourcePolicyId/whitelist",
"/public-resource-policy/:resourcePolicyId/whitelist"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
policy.setResourcePolicyWhitelist
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/rules",
"/public-resource-policy/:resourcePolicyId/rules"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
logActionAudit(ActionsEnum.setResourcePolicyRules),
policy.setResourcePolicyRules
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
[
"/resource-policy/:resourcePolicyId/rules",
"/public-resource-policy/:resourcePolicyId/rules"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
@@ -701,7 +875,10 @@ authenticated.put(
);
authenticated.post(
"/resource/:resourceId/roles/add",
[
"/resource/:resourceId/roles/add",
"/public-resource/:resourceId/roles/add"
],
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -711,7 +888,10 @@ authenticated.post(
);
authenticated.post(
"/resource/:resourceId/roles/remove",
[
"/resource/:resourceId/roles/remove",
"/public-resource/:resourceId/roles/remove"
],
verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess,
verifyLimits,
@@ -721,7 +901,10 @@ authenticated.post(
);
authenticated.post(
"/resource/:resourceId/users/add",
[
"/resource/:resourceId/users/add",
"/public-resource/:resourceId/users/add"
],
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -731,7 +914,10 @@ authenticated.post(
);
authenticated.post(
"/resource/:resourceId/users/remove",
[
"/resource/:resourceId/users/remove",
"/public-resource/:resourceId/users/remove"
],
verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers,
verifyLimits,
@@ -741,7 +927,7 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/password`,
[`/resource/:resourceId/password`, `/public-resource/:resourceId/password`],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
@@ -750,7 +936,7 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/pincode`,
[`/resource/:resourceId/pincode`, `/public-resource/:resourceId/pincode`],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
@@ -759,7 +945,10 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/header-auth`,
[
`/resource/:resourceId/header-auth`,
`/public-resource/:resourceId/header-auth`
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
@@ -768,7 +957,10 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/whitelist`,
[
`/resource/:resourceId/whitelist`,
`/public-resource/:resourceId/whitelist`
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -777,7 +969,10 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/whitelist/add`,
[
`/resource/:resourceId/whitelist/add`,
`/public-resource/:resourceId/whitelist/add`
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -785,7 +980,10 @@ authenticated.post(
);
authenticated.post(
`/resource/:resourceId/whitelist/remove`,
[
`/resource/:resourceId/whitelist/remove`,
`/public-resource/:resourceId/whitelist/remove`
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -793,14 +991,20 @@ authenticated.post(
);
authenticated.get(
`/resource/:resourceId/whitelist`,
[
`/resource/:resourceId/whitelist`,
`/public-resource/:resourceId/whitelist`
],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist),
resource.getResourceWhitelist
);
authenticated.post(
`/resource/:resourceId/access-token`,
[
`/resource/:resourceId/access-token`,
`/public-resource/:resourceId/access-token`
],
verifyApiKeyResourceAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
@@ -808,16 +1012,6 @@ authenticated.post(
accessToken.generateAccessToken
);
authenticated.post(
`/resource/:resourceId/session-token`,
verifyApiKeyResourceAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
logActionAudit(ActionsEnum.createResourceSessionToken),
resource.createResourceSessionToken
);
authenticated.delete(
`/access-token/:accessTokenId`,
verifyApiKeyAccessTokenAccess,
@@ -834,7 +1028,10 @@ authenticated.get(
);
authenticated.get(
`/resource/:resourceId/access-tokens`,
[
`/resource/:resourceId/access-tokens`,
`/public-resource/:resourceId/access-tokens`
],
verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
accessToken.listAccessTokens
@@ -1164,7 +1361,7 @@ authenticated.get(
);
authenticated.get(
"/org/:orgId/resource-names",
["/org/:orgId/resource-names", "/org/:orgId/public-resource-names"],
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listAllResourceNames
@@ -157,7 +157,8 @@ async function resolveAccessibleIdsUncached(
.where(
and(
eq(userResources.userId, userId),
eq(resources.orgId, orgId)
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
),
userRoleIds.length > 0
@@ -171,7 +172,8 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(roleResources.roleId, userRoleIds),
eq(resources.orgId, orgId)
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
)
: Promise.resolve([]),
@@ -183,7 +185,11 @@ async function resolveAccessibleIdsUncached(
eq(effectiveResourcePolicyId, userPolicies.resourcePolicyId)
)
.where(
and(eq(userPolicies.userId, userId), eq(resources.orgId, orgId))
and(
eq(userPolicies.userId, userId),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
),
userRoleIds.length > 0
? db
@@ -199,21 +205,48 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(rolePolicies.roleId, userRoleIds),
eq(resources.orgId, orgId)
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
)
: Promise.resolve([]),
db
.select({ siteResourceId: userSiteResources.siteResourceId })
.from(userSiteResources)
.where(eq(userSiteResources.userId, userId)),
.innerJoin(
siteResources,
eq(
userSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
eq(userSiteResources.userId, userId),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
),
userRoleIds.length > 0
? db
.select({
siteResourceId: roleSiteResources.siteResourceId
})
.from(roleSiteResources)
.where(inArray(roleSiteResources.roleId, userRoleIds))
.innerJoin(
siteResources,
eq(
roleSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
inArray(roleSiteResources.roleId, userRoleIds),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
)
: Promise.resolve([])
]);
@@ -365,6 +398,7 @@ async function filterPublicResourceIdsByTextSearch(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
textMatch
)
);
@@ -402,6 +436,7 @@ async function filterSiteResourceIdsByTextSearch(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
textMatch
)
);
@@ -503,7 +538,8 @@ async function listSiteGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true)
eq(resources.enabled, true),
eq(resources.status, "approved")
];
if (searchPublic) {
publicConditions.push(searchPublic);
@@ -558,7 +594,8 @@ async function listSiteGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true)
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
];
if (searchSite) {
siteConditions.push(searchSite);
@@ -621,7 +658,8 @@ async function listSiteGroups(
const noSitePublicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true)
eq(resources.enabled, true),
eq(resources.status, "approved")
];
if (searchPublic) {
noSitePublicConditions.push(searchPublic);
@@ -655,7 +693,8 @@ async function listSiteGroups(
const noSiteSiteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true)
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
];
if (searchSite) {
noSiteSiteConditions.push(searchSite);
@@ -746,7 +785,8 @@ async function listLabelGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true)
eq(resources.enabled, true),
eq(resources.status, "approved")
];
const searchPublic = buildSearchConditionForPublic(query.query);
if (searchPublic) {
@@ -810,7 +850,8 @@ async function listLabelGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true)
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
];
const searchSite = buildSearchConditionForSiteResource(query.query);
if (searchSite) {
@@ -997,6 +1038,7 @@ async function mapPublicResources(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@@ -1088,6 +1130,7 @@ async function mapSiteResources(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@@ -1382,7 +1425,8 @@ async function collectAccessibleSites(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true)
eq(resources.enabled, true),
eq(resources.status, "approved")
];
if (siteNameSearch) {
publicConditions.push(siteNameSearch);
@@ -1422,7 +1466,8 @@ async function collectAccessibleSites(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true)
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
];
if (siteNameSearch) {
siteConditions.push(siteNameSearch);
@@ -1476,6 +1521,7 @@ async function collectAccessibleLabels(
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {
@@ -1511,6 +1557,7 @@ async function collectAccessibleLabels(
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {
+15 -2
View File
@@ -19,6 +19,7 @@ import { eq, and, inArray } from "drizzle-orm";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import {
batchFetchCertsForSiteResources,
formatEndpoint,
generateSubnetProxyTargetV2,
SubnetProxyTargetV2
@@ -148,7 +149,12 @@ export async function buildClientConfigurationForNewtClient(
.from(siteResources)
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(eq(siteNetworks.siteId, siteId))
.where(
and(
eq(siteNetworks.siteId, siteId),
eq(siteResources.enabled, true)
)
)
.then((rows) => rows.map((r) => r.siteResources));
const targetsToSend: SubnetProxyTargetV2[] = [];
@@ -201,11 +207,18 @@ export async function buildClientConfigurationForNewtClient(
});
}
// Batch-fetch certs for every SSL-enabled HTTP resource's domain in one
// call rather than letting each resource fetch its own — with thousands
// of resources this avoids a concurrent DB/cache stampede for what is
// often the very same (e.g. wildcard) certificate.
const certByDomain = await batchFetchCertsForSiteResources(allSiteResources);
const resourceTargetsArr = await Promise.all(
allSiteResources.map((resource) =>
generateSubnetProxyTargetV2(
resource,
clientsByResourceId.get(resource.siteResourceId) ?? []
clientsByResourceId.get(resource.siteResourceId) ?? [],
certByDomain
)
)
);
+75
View File
@@ -0,0 +1,75 @@
import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { CertRef } from "@server/lib/ip";
/**
* Pushes an incremental set of certs to a newt client outside of a full
* newt/sync or newt/wg/receive-config, e.g. after a certificate renewal so
* that every target referencing it (by tlsCertId) picks up the new material
* without waiting for the next full resync.
*/
export async function sendCertsAdd(
newtId: string,
certs: CertRef[],
version?: string | null
) {
if (certs.length === 0) {
return;
}
if (!supportsCertReferences(version)) {
logger.debug(
`Newt ${newtId} (version ${version}) does not support cert references, skipping certs/add`
);
return;
}
await sendToClient(
newtId,
{
type: "newt/certs/add",
data: certs
},
{
incrementConfigVersion: true,
compress: canCompress(version, "newt")
}
);
}
/**
* Tells a newt client to drop the given cert IDs, e.g. once the server knows
* no target references them anymore.
*/
export async function sendCertsRemove(
newtId: string,
certIds: string[],
version?: string | null
) {
if (certIds.length === 0) {
return;
}
if (!supportsCertReferences(version)) {
logger.debug(
`Newt ${newtId} (version ${version}) does not support cert references, skipping certs/remove`
);
return;
}
await sendToClient(
newtId,
{
type: "newt/certs/remove",
data: { ids: certIds }
},
{
incrementConfigVersion: true,
compress: canCompress(version, "newt")
}
);
}
@@ -7,7 +7,11 @@ import { eq } from "drizzle-orm";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
import { convertTargetsIfNecessary } from "../client/targets";
import { canCompress } from "@server/lib/clientVersionChecks";
import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { dedupeCertsForTargets } from "@server/lib/ip";
import config from "@server/lib/config";
import { waitForSiteRebuildIdle } from "@server/lib/rebuildClientAssociations";
@@ -29,7 +33,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
return;
}
const { publicKey, port, chainId } = message.data;
const { publicKey, port, localEndpoints, chainId } = message.data;
const siteId = newt.siteId;
// Get the current site data
@@ -69,7 +73,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
.update(sites)
.set({
publicKey,
listenPort: port
listenPort: port,
localEndpoints: localEndpoints
? JSON.stringify(localEndpoints)
: null
})
.where(eq(sites.siteId, siteId))
.returning();
@@ -116,7 +123,16 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
exitNode
);
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format
// Older newt clients only understand inline tlsCert/tlsKey on each
// target, so only switch to certId references once we know the client
// can resolve them.
let dedupedTargets = targets;
let certs: { id: string; cert: string; key: string }[] = [];
if (supportsCertReferences(newt.version)) {
({ targets: dedupedTargets, certs } = dedupeCertsForTargets(targets));
}
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, dedupedTargets); // for backward compatibility with old newt versions that don't support the new target format
return {
message: {
@@ -125,6 +141,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
ipAddress: site.address,
peers,
targets: targetsToSend,
certs,
chainId: chainId
}
},
+17 -2
View File
@@ -6,7 +6,11 @@ import {
buildClientConfigurationForNewtClient,
buildTargetConfigurationForNewtClient
} from "./buildConfiguration";
import { canCompress } from "@server/lib/clientVersionChecks";
import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { dedupeCertsForTargets } from "@server/lib/ip";
export async function sendNewtSyncMessage(newt: Newt, site: Site) {
const {
@@ -28,6 +32,16 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
site,
exitNode
);
// Older newt clients only understand inline tlsCert/tlsKey on each
// target, so only switch to certId references once we know the client
// can resolve them.
let clientTargets = targets;
let certs: { id: string; cert: string; key: string }[] = [];
if (supportsCertReferences(newt.version)) {
({ targets: clientTargets, certs } = dedupeCertsForTargets(targets));
}
await sendToClient(
newt.newtId,
{
@@ -39,7 +53,8 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
},
healthCheckTargets: validHealthCheckTargets,
peers: peers,
clientTargets: targets,
clientTargets: clientTargets,
certs: certs,
browserGatewayTargets: browserGatewayTargets,
remoteExitNodeSubnets: remoteExitNodeSubnets
}
+12 -2
View File
@@ -16,7 +16,7 @@ import {
generateRemoteSubnets
} from "@server/lib/ip";
import logger from "@server/logger";
import { eq, inArray } from "drizzle-orm";
import { and, eq, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import config from "@server/lib/config";
@@ -30,6 +30,7 @@ export async function buildSiteConfigurationForOlmClient(
siteId: number;
name?: string;
endpoint?: string;
localEndpoints?: string[];
publicKey?: string;
serverIP?: string | null;
serverPort?: number | null;
@@ -70,7 +71,13 @@ export async function buildSiteConfigurationForOlmClient(
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
and(
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
),
eq(siteResources.enabled, true)
)
);
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
@@ -200,6 +207,9 @@ export async function buildSiteConfigurationForOlmClient(
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
endpoint: site.endpoint,
localEndpoints: site.localEndpoints
? JSON.parse(site.localEndpoints)
: undefined,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort,
@@ -0,0 +1,74 @@
import { db, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, Olm } from "@server/db";
import { and, eq } from "drizzle-orm";
import { updatePeer as newtUpdatePeer } from "../newt/peers";
import logger from "@server/logger";
export const handleOlmLocalMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
logger.info("Handling local olm message!");
if (!olm) {
logger.warn("Olm not found");
return;
}
if (!olm.clientId) {
logger.warn("Olm has no client!");
return;
}
const clientId = olm.clientId;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
logger.warn("Client not found");
return;
}
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
if (!client.pubKey) {
logger.warn("Client has no endpoint or listen port");
return;
}
const { siteId, chainId } = message.data;
// Get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site || !site.exitNodeId) {
logger.warn("Site not found or has no exit node");
return;
}
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: "" // this removes the endpoint so the newt knows to accept local
});
// Just ack the message, we don't keep sending it
return {
message: {
type: "olm/wg/peer/local",
data: {
siteId: siteId,
chainId
}
},
broadcast: false,
excludeSender: false
};
};
+2 -2
View File
@@ -79,9 +79,9 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
)
);
// update the peer on the exit node
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: "" // this removes the endpoint so the exit node knows to relay
endpoint: "" // this removes the endpoint so the newt knows to relay
});
return {
@@ -3,24 +3,15 @@ import {
db,
networks,
siteNetworks,
siteResources,
siteResources
} from "@server/db";
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 logger from "@server/logger";
import {
generateAliasConfig,
} from "@server/lib/ip";
import { generateAliasConfig } 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 (
context
@@ -135,10 +126,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
clientSiteResourcesAssociationsCache.siteResourceId
)
)
.innerJoin(
networks,
eq(siteResources.networkId, networks.networkId)
)
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(
siteNetworks,
and(
@@ -147,10 +135,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
)
)
.where(
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
)
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
);
// Return connect message with all site configurations
@@ -161,6 +146,9 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
siteId: site.siteId,
name: site.name,
endpoint: site.endpoint,
localEndpoints: site.localEndpoints
? JSON.parse(site.localEndpoints)
: undefined,
publicKey: site.publicKey,
serverIP: site.address,
serverPort: site.listenPort,
@@ -170,7 +158,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async (
aliases: generateAliasConfig(
allSiteResources.map(({ siteResources }) => siteResources)
),
chainId: chainId,
chainId: chainId
}
},
broadcast: false,
@@ -0,0 +1,95 @@
import { db, exitNodes, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, clientSitesAssociationsCache, Olm } from "@server/db";
import { and, eq } from "drizzle-orm";
import { updatePeer as newtUpdatePeer } from "../newt/peers";
import logger from "@server/logger";
export const handleOlmUnLocalMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
logger.info("Handling unlocal olm message!");
if (!olm) {
logger.warn("Olm not found");
return;
}
if (!olm.clientId) {
logger.warn("Olm has no client!");
return;
}
const clientId = olm.clientId;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
logger.warn("Client not found");
return;
}
// make sure we hand endpoints for both the site and the client and the lastHolePunch is not too old
if (!client.pubKey) {
logger.warn("Client has no endpoint or listen port");
return;
}
const { siteId, chainId } = message.data;
// Get the site
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site) {
logger.warn("Site not found or has no exit node");
return;
}
const [clientSiteAssociation] = await db
.select()
.from(clientSitesAssociationsCache)
.where(
and(
eq(clientSitesAssociationsCache.clientId, olm.clientId),
eq(clientSitesAssociationsCache.siteId, siteId)
)
);
if (!clientSiteAssociation) {
logger.warn("Client-Site association not found");
return;
}
if (!clientSiteAssociation.endpoint) {
logger.warn("Client-Site association has no endpoint, cannot unrelay");
return;
}
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: clientSiteAssociation.isRelayed
? ""
: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
});
return {
message: {
type: "olm/wg/peer/unlocal",
data: {
siteId: siteId,
chainId
}
},
broadcast: false,
excludeSender: false
};
};
@@ -77,9 +77,9 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => {
return;
}
// update the peer on the exit node
// update the peer on the newt
await newtUpdatePeer(siteId, client.pubKey, {
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the exit node
endpoint: clientSiteAssociation.endpoint // this is the endpoint of the client to connect directly to the newt
});
return {
+2
View File
@@ -13,3 +13,5 @@ export * from "./recoverOlmWithFingerprint";
export * from "./handleOlmDisconnectingMessage";
export * from "./handleOlmServerInitAddPeerHandshake";
export * from "./offlineChecker";
export * from "./handleOlmUnLocalMessage";
export * from "./handleOlmLocalMessage";
+2
View File
@@ -18,6 +18,7 @@ export async function addPeer(
serverPort: number | null;
remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access
aliases: Alias[];
localEndpoints?: string[]; // optional, list of local endpoints for the peer
},
olmId?: string,
version?: string | null
@@ -44,6 +45,7 @@ export async function addPeer(
name: peer.name,
publicKey: peer.publicKey,
endpoint: peer.endpoint,
localEndpoints: peer.localEndpoints,
relayEndpoint: peer.relayEndpoint,
serverIP: peer.serverIP,
serverPort: peer.serverPort,
+15 -2
View File
@@ -168,7 +168,7 @@ registry.registerPath({
path: "/org/{orgId}/resource-policy/{niceId}",
description:
"Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
tags: [OpenAPITags.Org, OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: z.object({
orgId: z.string(),
@@ -182,7 +182,20 @@ registry.registerPath({
method: "get",
path: "/resource-policy/{resourcePolicyId}",
description: "Get a resource policy by its resourcePolicyId.",
tags: [OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: z.object({
resourcePolicyId: z.number()
})
},
responses: {}
});
registry.registerPath({
method: "get",
path: "/public-resource-policy/{resourcePolicyId}",
description: "Get a resource policy by its resourcePolicyId.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: z.object({
resourcePolicyId: z.number()
@@ -2,8 +2,6 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
idp,
idpOrg,
resourcePolicies,
rolePolicies,
roles,
@@ -18,6 +16,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq, inArray, ne } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
const setResourcePolicyAcccessControlBodySchema = z.strictObject({
sso: z.boolean(),
@@ -27,7 +26,7 @@ const setResourcePolicyAcccessControlBodySchema = z.strictObject({
}),
skipToIdpId: z.int().positive().optional().nullable().openapi({
type: "integer",
description: "Page number to retrieve"
description: "Default identity provider ID to skip to on login"
})
});
@@ -36,11 +35,70 @@ const setResourcePolicyAccessControlParamsSchema = z.strictObject({
});
registry.registerPath({
method: "post",
path: "/resource-policy/{resourceId}/access-control",
method: "put",
path: "/resource-policy/{resourcePolicyId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
tags: [OpenAPITags.Policy, OpenAPITags.User],
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
@@ -104,16 +162,9 @@ export async function setResourcePolicyAccessControl(
// Check if Identity provider in `skipToIdpId` exists
if (idpId) {
const [provider] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(
and(eq(idp.idpId, idpId), eq(idpOrg.orgId, policy.orgId))
)
.limit(1);
const providerExists = await idpExistsForOrg(idpId, policy.orgId);
if (!provider) {
if (!providerExists) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
@@ -25,11 +25,70 @@ const setResourcePolicyHeaderAuthBodySchema = z.strictObject({
});
registry.registerPath({
method: "put",
method: "post",
path: "/resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.Policy],
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {

Some files were not shown because too many files have changed in this diff Show More