mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-15 15:19:51 +02:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b982639dc4 | |||
| 56e758a656 | |||
| a84db679b2 | |||
| fb726cc6b2 | |||
| 5d0ceebe0d | |||
| c964821195 | |||
| 5c44c6da73 | |||
| 6e52758d6e | |||
| 0af194550f | |||
| 0d12402662 | |||
| 3fb2e34256 | |||
| ec2b1fbd37 | |||
| 806ae93a12 | |||
| 5d99e0a2f6 | |||
| a75f335791 | |||
| 153d29c47c | |||
| ee8749c292 | |||
| 4f905ddae5 | |||
| 74e686ff3f | |||
| a4ade43380 | |||
| 68821514a0 | |||
| a2652cf692 | |||
| 957144ac3a | |||
| 20c23cdfc5 | |||
| c03e1c5781 | |||
| e69d3eded5 | |||
| 34084cc3a6 | |||
| 91776ad2e4 | |||
| 77eabf41e7 | |||
| d68895e64a | |||
| 1c2d81d77c | |||
| 998778eb72 | |||
| 7be206947a | |||
| 125d33c071 | |||
| 410d26f2df | |||
| 6aa1dafa10 | |||
| 8f1344a665 | |||
| d23028d38e | |||
| 7506a2614b | |||
| b3e5de4a4d | |||
| f9f38fb3f0 | |||
| 888ccce397 | |||
| 49e0a8bd47 | |||
| f59aed8482 | |||
| b6c048c805 | |||
| 018ee17304 | |||
| 5cad796023 | |||
| 5a445817e0 | |||
| fd2c397bfc | |||
| 6c794e0b0c | |||
| 7acb98e245 | |||
| d59ab30c22 | |||
| e4a8e9ac8c | |||
| a874a0b745 | |||
| 9ba2cfdef2 | |||
| bba8d4e7a2 | |||
| 350f8c012a | |||
| 8a97ed5200 | |||
| e66f7fe71b | |||
| a37a59f39a | |||
| d0bf553f6f | |||
| 87e005d32f | |||
| ad343a7453 | |||
| 7e4d38548f | |||
| 3421441635 | |||
| 073bfb32e9 | |||
| 2c5b1e5f0f | |||
| 294830db08 | |||
| fb8d531435 | |||
| 11d947f4ef | |||
| aad18e6501 | |||
| dac8b5a132 | |||
| 162dade852 | |||
| dfe7f60244 | |||
| 07bea71cb9 | |||
| 557c398d0b | |||
| 0a385d1e44 | |||
| 521f78c2f3 | |||
| 0ea4a5fb3d | |||
| e2609f1a0d | |||
| 8b20a88838 | |||
| 7aae51ca9d | |||
| 1d6d885f30 | |||
| fef1476f67 | |||
| e371f26d73 | |||
| 9c8b93d6cc | |||
| aed325f273 | |||
| c7645e5c5b | |||
| c0eed078c8 | |||
| 0f69012c4d | |||
| ba2eb87f20 | |||
| c0ea32863c | |||
| 4d71fcbac8 | |||
| 1d3dcec4c8 | |||
| 17375348b0 | |||
| e7fdbf9e85 | |||
| cddb5ecc3d |
@@ -46,7 +46,6 @@ public/branding
|
||||
server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
|
||||
@@ -23,7 +23,7 @@ export const clearExitNodes: CommandModule<
|
||||
// Delete all exit nodes
|
||||
const deletedCount = await db
|
||||
.delete(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)).returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## Example Docker Reference HA Deployment
|
||||
|
||||
This directory contains basic config for a highly available deployment of Pangolin with two nodes. For more information [refer to the docs](/self-host/clustering/understanding-clustering).
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_DB: postgres # Default database name
|
||||
POSTGRES_USER: postgres # Default user
|
||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,38 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "<THIS_NODE_EXTERNAL_IP>"
|
||||
exit_node_name: "node1"
|
||||
|
||||
app:
|
||||
dashboard_url: "https://pangolin.example.com"
|
||||
log_level: "info"
|
||||
|
||||
postgres:
|
||||
connection_string: postgresql://<POSTGRES_USERNAME>:<POSTGRES_PASSWORD>@<POSTGRES_INTERNAL_HOST>:5432/postgres
|
||||
|
||||
traefik:
|
||||
site_types: ["newt"] # Wireguard and local sites are not support in clustering
|
||||
file_mode: true # Pangolin will generate and save yaml files in a shared volume
|
||||
|
||||
server:
|
||||
secret: "<SECRET>"
|
||||
cors:
|
||||
origins: ["https://pangolin.example.com"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
maxmind_db_path: "./config/GeoLite2-Country.mmdb" # Make sure to download and place into the config dir
|
||||
maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: false
|
||||
enable_acme_cert_sync: false
|
||||
disable_local_sites: true
|
||||
disable_basic_wireguard_sites: true
|
||||
disable_config_managed_domains: true
|
||||
@@ -0,0 +1,67 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
|
||||
routers:
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "!PathPrefix(`/api/v1`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
# API router (handles /api/v1 paths)
|
||||
api-router:
|
||||
rule: "PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
# WebSocket router
|
||||
ws-router:
|
||||
rule: "PathPrefix(`/`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
services:
|
||||
next-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3002" # Next.js server
|
||||
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
udp:
|
||||
routers:
|
||||
dns-router:
|
||||
entryPoints:
|
||||
- dns
|
||||
service: dns-service
|
||||
|
||||
services:
|
||||
dns-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- address: "pangolin:53"
|
||||
@@ -0,0 +1,18 @@
|
||||
app:
|
||||
region: "region1"
|
||||
identity_provider_mode: "org"
|
||||
redis:
|
||||
host: "<REDIS_INTERNAL_HOST>"
|
||||
port: 6379
|
||||
flags:
|
||||
enable_redis: true
|
||||
use_pangolin_dns: true
|
||||
acme:
|
||||
cert_mode: "pangolin"
|
||||
contact_email: "<CONTACT_EMAIL>"
|
||||
enable_acme_client: true
|
||||
dns:
|
||||
enabled: true
|
||||
nameserver_name: "ns.example.com"
|
||||
cname_extension: "cname.example.com"
|
||||
site_extension: "site.example.com" # Optional
|
||||
@@ -0,0 +1,45 @@
|
||||
providers:
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "v1.7.0"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
proxyProtocol: # We trust gerbil upstream
|
||||
trustedIPs:
|
||||
- 0.0.0.0/0
|
||||
- ::1/128
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
dashboard:
|
||||
address: ":3000"
|
||||
dns:
|
||||
address: ":53/udp"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
@@ -0,0 +1,62 @@
|
||||
name: pangolin
|
||||
services:
|
||||
pangolin:
|
||||
image: docker.io/fosrl/pangolin:ee-latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./config/certificates:/var/certificates
|
||||
- ./config/dynamic:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
timeout: "10s"
|
||||
retries: 15
|
||||
|
||||
gerbil:
|
||||
image: docker.io/fosrl/gerbil:latest
|
||||
container_name: gerbil
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://<NODE1_INTERNAL_IP>:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
- --trusted-upstreams=<NODE1_EXTERNAL_IP>,<NODE2_EXTERNAL_IP>
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
ports:
|
||||
- 51820:51820/udp # wireguard
|
||||
- 21820:21820/udp # relay
|
||||
- 53:53/udp # DNS
|
||||
- 443:8443 # resources
|
||||
- 80:80 # web
|
||||
- 3004:3004 # gerbil api
|
||||
- 3000:3000 # Pangolin UI
|
||||
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.7.11
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --configFile=/etc/traefik/traefik_config.yml
|
||||
volumes:
|
||||
- ./config/traefik:/etc/traefik:ro
|
||||
- ./config/traefik/logs:/var/log/traefik
|
||||
- ./config/certificates:/var/certificates:ro
|
||||
- ./config/dynamic:/var/dynamic:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
@@ -0,0 +1,38 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
base_endpoint: "<THIS_NODE_EXTERNAL_IP>"
|
||||
exit_node_name: "node2"
|
||||
|
||||
app:
|
||||
dashboard_url: "https://pangolin.example.com"
|
||||
log_level: "info"
|
||||
|
||||
postgres:
|
||||
connection_string: postgresql://<POSTGRES_USERNAME>:<POSTGRES_PASSWORD>@<POSTGRES_INTERNAL_HOST>:5432/postgres
|
||||
|
||||
traefik:
|
||||
site_types: ["newt"] # Wireguard and local sites are not support in clustering
|
||||
file_mode: true # Pangolin will generate and save yaml files in a shared volume
|
||||
|
||||
server:
|
||||
secret: "<SECRET>"
|
||||
cors:
|
||||
origins: ["https://pangolin.example.com"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
maxmind_db_path: "./config/GeoLite2-Country.mmdb" # Make sure to download and place into the config dir
|
||||
maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: false
|
||||
enable_acme_cert_sync: false
|
||||
disable_local_sites: true
|
||||
disable_basic_wireguard_sites: true
|
||||
disable_config_managed_domains: true
|
||||
@@ -0,0 +1,67 @@
|
||||
http:
|
||||
middlewares:
|
||||
badger:
|
||||
plugin:
|
||||
badger:
|
||||
disableForwardAuth: true
|
||||
|
||||
routers:
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "!PathPrefix(`/api/v1`)"
|
||||
service: next-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
# API router (handles /api/v1 paths)
|
||||
api-router:
|
||||
rule: "PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
# WebSocket router
|
||||
ws-router:
|
||||
rule: "PathPrefix(`/`)"
|
||||
service: api-service
|
||||
entryPoints:
|
||||
- dashboard
|
||||
middlewares:
|
||||
- badger
|
||||
|
||||
services:
|
||||
next-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3002" # Next.js server
|
||||
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
udp:
|
||||
routers:
|
||||
dns-router:
|
||||
entryPoints:
|
||||
- dns
|
||||
service: dns-service
|
||||
|
||||
services:
|
||||
dns-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- address: "pangolin:53"
|
||||
@@ -0,0 +1,16 @@
|
||||
app:
|
||||
region: "region1"
|
||||
identity_provider_mode: "org"
|
||||
redis:
|
||||
host: "<REDIS_INTERNAL_HOST>"
|
||||
port: 6379
|
||||
flags:
|
||||
enable_redis: true
|
||||
use_pangolin_dns: true
|
||||
acme:
|
||||
cert_mode: "pangolin"
|
||||
dns:
|
||||
enabled: true
|
||||
nameserver_name: "ns.example.com"
|
||||
cname_extension: "cname.example.com"
|
||||
site_extension: "site.example.com" # Optional
|
||||
@@ -0,0 +1,45 @@
|
||||
providers:
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "v1.7.0"
|
||||
|
||||
log:
|
||||
level: "INFO"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
proxyProtocol: # We trust gerbil upstream
|
||||
trustedIPs:
|
||||
- 0.0.0.0/0
|
||||
- ::1/128
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
dashboard:
|
||||
address: ":3000"
|
||||
dns:
|
||||
address: ":53/udp"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
@@ -0,0 +1,62 @@
|
||||
name: pangolin
|
||||
services:
|
||||
pangolin:
|
||||
image: docker.io/fosrl/pangolin:ee-latest
|
||||
container_name: pangolin
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./config/certificates:/var/certificates
|
||||
- ./config/dynamic:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
timeout: "10s"
|
||||
retries: 15
|
||||
|
||||
gerbil:
|
||||
image: docker.io/fosrl/gerbil:latest
|
||||
container_name: gerbil
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://<NODE1_INTERNAL_IP>:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
- --trusted-upstreams=<NODE1_EXTERNAL_IP>,<NODE2_EXTERNAL_IP>
|
||||
volumes:
|
||||
- ./config/:/var/config
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
ports:
|
||||
- 51820:51820/udp # wireguard
|
||||
- 21820:21820/udp # relay
|
||||
- 53:53/udp # DNS
|
||||
- 443:8443 # resources
|
||||
- 80:80 # web
|
||||
- 3004:3004 # gerbil api
|
||||
- 3000:3000 # Pangolin UI
|
||||
|
||||
traefik:
|
||||
image: docker.io/traefik:v3.7.11
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||
depends_on:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --configFile=/etc/traefik/traefik_config.yml
|
||||
volumes:
|
||||
- ./config/traefik:/etc/traefik:ro
|
||||
- ./config/traefik/logs:/var/log/traefik
|
||||
- ./config/certificates:/var/certificates:ro
|
||||
- ./config/dynamic:/var/dynamic:ro
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
files:
|
||||
- source: /messages/en-US.json
|
||||
translation: /messages/%locale%.json
|
||||
translation: /messages/%locale%.json
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
tls:
|
||||
certificates: []
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+35
-3
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Цели на този сайт",
|
||||
"siteSetting": "Настройки на {siteName}",
|
||||
"siteNewtTunnel": "Нов Сайт (Препоръчително)",
|
||||
"pangolinSite": "Сайт Панголиин",
|
||||
"siteNewtTunnelDescription": "Най-лесният начин да създадете точка за достъп до всяка мрежа. Няма нужда от допълнителни настройки.",
|
||||
"siteWg": "Основен WireGuard",
|
||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Изтрийте API ключа",
|
||||
"apiKeysManage": "Управление на API ключове",
|
||||
"apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API",
|
||||
"orgsManage": "Управление на организации",
|
||||
"orgsDescription": "Преглед и управление на всички организации в тази инстанция",
|
||||
"provisioningKeysTitle": "Ключ за осигуряване",
|
||||
"provisioningKeysManage": "Управление на ключове за осигуряване",
|
||||
"provisioningKeysDescription": "Ключовете за осигуряване се използват за удостоверяване на автоматичното осигуряване на сайта за вашата организация.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Име (по избор)",
|
||||
"accessControls": "Контрол на достъпа",
|
||||
"userDescription2": "Управление на настройките на този потребител",
|
||||
"userGeneralSettingsDescription": "Управление на ролите и настройките на този потребител в организацията",
|
||||
"accessRoleErrorAdd": "Неуспешно добавяне на потребител към роля",
|
||||
"accessRoleErrorAddDescription": "Възникна грешка при добавяне на потребителя към ролята.",
|
||||
"userSaved": "Потребителят е запазен",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Когато правилата са изключени, целият трафик преминава към удостоверяване.",
|
||||
"policyAccessRulesFallthroughOn": "Когато няма съвпадение, трафикът преминава към удостоверяване.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/администратор/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Запазете правилата",
|
||||
"resourceErrorCreate": "Грешка при създаване на ресурс",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Грешка при излизане",
|
||||
"signingAs": "Влезли сте като",
|
||||
"serverAdmin": "Администратор на сървъра",
|
||||
"promoteServerAdmin": "Повишаване до администратор на сървъра",
|
||||
"promoteServerAdminTitle": "Повишаване до Admin на сървъра",
|
||||
"promoteServerAdminQuestion": "Сигурни ли сте, че искате да повишите {selectedUser} до администратор на сървъра?",
|
||||
"promoteServerAdminMessage": "Администраторите на сървъра имат най-високите привилегии и могат да управляват сървъра.",
|
||||
"promoteServerAdminWarning": "Това може да бъде отменено по всяко време посредством понижаване на потребителя.",
|
||||
"promoteServerAdminConfirm": "Повишаване до Admin на сървъра",
|
||||
"promoteServerAdminSuccess": "Потребителят е повишен",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} вече е администратор на сървъра.",
|
||||
"promoteServerAdminError": "Неуспешно повишаване на потребител",
|
||||
"demoteServerAdmin": "Понижаване от администратор на сървъра",
|
||||
"demoteServerAdminTitle": "Понижаване от Admin на сървъра",
|
||||
"demoteServerAdminQuestion": "Сигурни ли сте, че искате да понижите {selectedUser} от администратор на сървъра?",
|
||||
"demoteServerAdminMessage": "{selectedUser} ще загуби всички привилегии на администратор на сървъра.",
|
||||
"demoteServerAdminWarning": "Това може да бъде отменено по всяко време посредством повишаване на потребителя.",
|
||||
"demoteServerAdminConfirm": "Понижаване от администратор на сървъра",
|
||||
"demoteServerAdminSuccess": "Потребителят е понижен",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} вече не е администратор на сървъра.",
|
||||
"demoteServerAdminError": "Неуспешно понижаване на потребител",
|
||||
"managedSelfhosted": "Управлявано Самостоятелно-хоствано",
|
||||
"otpEnable": "Включване на двуфакторен",
|
||||
"otpDisable": "Изключване на двуфакторен",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Бюджет",
|
||||
"resourceBudgetSettingsDescription": "Конфигурирайте как AI порталът ограничава употребата, основавайки се на разходи или лимити на токени",
|
||||
"sidebarApiKeys": "API ключове",
|
||||
"sidebarOrgs": "Организации",
|
||||
"sidebarProvisioning": "Осигуряване",
|
||||
"sidebarSettings": "Настройки",
|
||||
"sidebarAllUsers": "Всички потребители",
|
||||
@@ -3168,7 +3191,7 @@
|
||||
"idpAzureConfiguration": "Конфигурация на Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Конфигурирайте OAuth2 идентификационни данни на Azure Entra ID",
|
||||
"idpTenantId": "Идентификационен номер на наемателя",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "идентификационен номер на наемателя",
|
||||
"idpAzureTenantIdDescription": "Идентификационен номер на наемателя на Azure (намира се в прегледа на Azure Active Directory)",
|
||||
"idpAzureClientIdDescription": "Идентификационен код на клиента за регистриране на приложение в Azure",
|
||||
"idpAzureClientSecretDescription": "Секретен код на клиента за регистриране на приложение в Azure",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Приоритет",
|
||||
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
|
||||
"instanceName": "Име на инстанция",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Конфигурация на съвпадение по пътека",
|
||||
"pathMatchModalDescription": "Настройте как трябва да бъдат съвпадани входящите заявки въз основа на техния път.",
|
||||
"pathMatchType": "Вид на съвпадението",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Няма Данни",
|
||||
"machineClients": "Машинни клиенти",
|
||||
"install": "Инсталирай",
|
||||
"downloadInstaller": "Изтегляне на инсталатора",
|
||||
"run": "Изпълни",
|
||||
"envFile": "Файл за среда",
|
||||
"serviceFile": "Файл за услуга",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Прекъснат",
|
||||
"approvalsEmptyStateTitle": "Одобрения на устройство не са активирани",
|
||||
"approvalsEmptyStateDescription": "Активирайте одобрения на устройства за роли, така че да изискват администраторско одобрение, преди потребителите да могат да свързват нови устройства.",
|
||||
"approvalsEmptyStateHowToTitle": "Как да го активирате",
|
||||
"approvalsEmptyStateStep1Title": "Отидете на роли",
|
||||
"approvalsEmptyStateStep1Description": "Навигирайте до настройките на ролите на вашата организация, за да конфигурирате одобренията на устройства.",
|
||||
"approvalsEmptyStateStep2Title": "Активирайте одобрения на устройства",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Вижте като Админ",
|
||||
"resourceLauncherResourceDetailsDescription": "Информация и статус на връзката за този ресурс.",
|
||||
"resourceLauncherResourceDetails": "Детайли за ресурса",
|
||||
"resourceLauncherSitesDescription": "Ресурсът е достъпен чрез следните сайтове.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Вижте сайта като Админ",
|
||||
"resourceLauncherFilterBySite": "Филтриране по сайт",
|
||||
"resourceLauncherSshCommand": "SSH Команда",
|
||||
"resourceLauncherSshCommandDescription": "Използвайте Pangolin CLI, за да отворите SSH сесия до този ресурс.",
|
||||
"resourceLauncherAuthMethodsDescription": "Методи на автентикация, активирани за този ресурс.",
|
||||
"resourceLauncherPrivateClientRequired": "Свържете се с клиент на вашето устройство, за да получите частен достъп до този ресурс.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Изисква се връзка с клиента",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
|
||||
"sessionToolbarShow": "Показване на лентата с инструменти",
|
||||
"sessionToolbarHide": "Скриване на лентата с инструменти",
|
||||
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
|
||||
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта",
|
||||
"check": "Проверка"
|
||||
}
|
||||
|
||||
+81
-49
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Cíle na tomto webu",
|
||||
"siteSetting": "Nastavení {siteName}",
|
||||
"siteNewtTunnel": "Novinka (doporučeno)",
|
||||
"pangolinSite": "Stránka Pangolin",
|
||||
"siteNewtTunnelDescription": "Nejjednodušší způsob, jak vytvořit vstupní bod do jakékoli sítě. Žádné další nastavení.",
|
||||
"siteWg": "Základní WireGuard",
|
||||
"siteWgDescription": "Použijte jakéhokoli klienta WireGuard abyste sestavili tunel. Vyžaduje se ruční nastavení NAT.",
|
||||
@@ -323,7 +324,7 @@
|
||||
"resourceConfig": "Konfigurační snippety",
|
||||
"resourceConfigDescription": "Zkopírujte a vložte tyto konfigurační úryvky pro nastavení TCP/UDP zdroje.",
|
||||
"resourceAddEntrypoints": "Traefik: Přidat vstupní body",
|
||||
"resourceExposePorts": "Gerbil: Expose Ports in Docker Compose",
|
||||
"resourceExposePorts": "Gerbil: Popis port v Docker Compose",
|
||||
"resourceLearnRaw": "Naučte se konfigurovat zdroje TCP/UDP",
|
||||
"resourceBack": "Zpět na zdroje",
|
||||
"resourceGoTo": "Přejít na dokument",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Odstranit klíč API",
|
||||
"apiKeysManage": "Správa API klíčů",
|
||||
"apiKeysDescription": "API klíče se používají k ověření s integračním API",
|
||||
"orgsManage": "Spravovat organizace",
|
||||
"orgsDescription": "Zobrazit a spravovat všechny organizace v této instanci",
|
||||
"provisioningKeysTitle": "Zajišťovací klíč",
|
||||
"provisioningKeysManage": "Spravovat zajišťovací klíče",
|
||||
"provisioningKeysDescription": "Zajišťovací klíče slouží k ověření automatického poskytování služeb vaší organizaci.",
|
||||
@@ -532,7 +535,7 @@
|
||||
"userMessageRemove": "Uživatel bude odstraněn ze všech organizací a bude zcela odstraněn ze serveru.",
|
||||
"userQuestionRemove": "Jste si jisti, že chcete trvale odstranit uživatele ze serveru?",
|
||||
"licenseKey": "Licenční klíč",
|
||||
"valid": "Valid",
|
||||
"valid": "Platný",
|
||||
"numberOfSites": "Počet lokalit",
|
||||
"licenseKeySearch": "Hledat licenční klíče...",
|
||||
"licenseKeyAdd": "Přidat licenční klíč",
|
||||
@@ -609,7 +612,7 @@
|
||||
"inviteSent": "Nová pozvánka byla odeslána na {email}.",
|
||||
"inviteSentEmail": "Poslat uživateli oznámení e-mailem",
|
||||
"inviteGenerate": "Nová pozvánka byla vygenerována pro {email}.",
|
||||
"inviteDuplicateError": "Duplicate Invite",
|
||||
"inviteDuplicateError": "Duplicitní pozvání",
|
||||
"inviteDuplicateErrorDescription": "Pozvánka pro tohoto uživatele již existuje.",
|
||||
"inviteRateLimitError": "Limit sazby překročen",
|
||||
"inviteRateLimitErrorDescription": "Překročil jsi limit 3 regenerací za hodinu. Opakujte akci později.",
|
||||
@@ -652,7 +655,7 @@
|
||||
"ownerMustRetainAdminRole": "Vlastník organizace musí zachovat alespoň jednu roli správce.",
|
||||
"usernameRequired": "Uživatelské jméno je povinné",
|
||||
"idpSelectPlease": "Vyberte poskytovatele identity",
|
||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||
"idpGenericOidc": "Obecný poskytovatel OAuth2/OIDC.",
|
||||
"accessRoleErrorFetch": "Nepodařilo se načíst role",
|
||||
"accessRoleErrorFetchDescription": "Při načítání rolí došlo k chybě",
|
||||
"idpErrorFetch": "Nepodařilo se načíst poskytovatele identity",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Jméno (nepovinné)",
|
||||
"accessControls": "Kontrola přístupu",
|
||||
"userDescription2": "Spravovat nastavení tohoto uživatele",
|
||||
"userGeneralSettingsDescription": "Spravujte role a nastavení tohoto uživatele v organizaci",
|
||||
"accessRoleErrorAdd": "Přidání uživatele do role se nezdařilo",
|
||||
"accessRoleErrorAddDescription": "Došlo k chybě při přidávání uživatele do role.",
|
||||
"userSaved": "Uživatel uložen",
|
||||
@@ -736,15 +740,15 @@
|
||||
"proxyErrorTls": "Neplatné jméno TLS serveru. Použijte formát doménového jména nebo uložte prázdné pro odstranění názvu TLS serveru.",
|
||||
"proxyEnableSSL": "Povolit TLS",
|
||||
"proxyEnableSSLDescription": "Povolit šifrování SSL/TLS pro zabezpečená připojení HTTPS k cílům.",
|
||||
"target": "Target",
|
||||
"target": "Cíl",
|
||||
"configureTarget": "Konfigurace cílů",
|
||||
"targetErrorFetch": "Nepodařilo se načíst cíle",
|
||||
"targetErrorFetchDescription": "Při načítání cílů došlo k chybě",
|
||||
"siteErrorFetch": "Nepodařilo se načíst zdroj",
|
||||
"siteErrorFetchDescription": "Při načítání zdroje došlo k chybě",
|
||||
"targetErrorDuplicate": "Duplicate target",
|
||||
"targetErrorDuplicate": "Duplicitní cíl",
|
||||
"targetErrorDuplicateDescription": "Cíl s těmito nastaveními již existuje",
|
||||
"targetWireGuardErrorInvalidIp": "Invalid target IP",
|
||||
"targetWireGuardErrorInvalidIp": "Neplatná IP adresa cíle",
|
||||
"targetWireGuardErrorInvalidIpDescription": "Cílová IP adresa musí být v podsíti webu",
|
||||
"targetsUpdated": "Cíle byly aktualizovány",
|
||||
"targetsUpdatedDescription": "Cíle a nastavení byly úspěšně aktualizovány",
|
||||
@@ -772,11 +776,11 @@
|
||||
"targetStickySessions": "Povolit Rychlé relace",
|
||||
"targetStickySessionsDescription": "Zachovat spojení na stejném cíli pro celou relaci.",
|
||||
"methodSelect": "Vyberte metodu",
|
||||
"targetSubmit": "Add Target",
|
||||
"targetSubmit": "Přidat cíl",
|
||||
"targetNoOne": "Tento zdroj nemá žádné cíle. Přidejte cíl pro konfiguraci kam poslat žádosti na backend.",
|
||||
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
||||
"targetsSubmit": "Uložit Nastavení",
|
||||
"addTarget": "Add Target",
|
||||
"addTarget": "Přidat cíl",
|
||||
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing nebude fungovat mezi lokalitami, které nejsou připojeny ke stejnému uzlu, ale failover bude fungovat.",
|
||||
"targetErrorInvalidIp": "Neplatná IP adresa",
|
||||
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
||||
@@ -892,7 +896,7 @@
|
||||
"policyAuthPincodeTitle": "PIN Kód",
|
||||
"policyAuthPincodeDescription": "Krátký číselný kód vyžadován pro přístup ke zdroji",
|
||||
"policyAuthPincodeSummary": "Nastaven 6místný PIN",
|
||||
"policyAuthEmailTitle": "Email Whitelist",
|
||||
"policyAuthEmailTitle": "Seznam povolených emailů",
|
||||
"policyAuthEmailDescription": "Povolit vybraným emailovým adresám s jednorázovými hesly",
|
||||
"policyAuthEmailSummary": "Povoleno {count} adres(y)",
|
||||
"policyAuthEmailOtpCallout": "Povolení seznamu povolených e-mailů odešle jednorázové heslo na e-mail návštěvníka při přihlášení.",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Neznámý příkaz",
|
||||
"newtErrorFetchReleases": "Nepodařilo se načíst informace o vydání: {err}",
|
||||
"newtErrorFetchLatest": "Chyba při načítání nejnovější verze: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Koncový bod",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Tajný klíč",
|
||||
"newtVersion": "Verze",
|
||||
@@ -1226,7 +1230,7 @@
|
||||
"orgIdpRedirectUrls": "Přesměrovat URL",
|
||||
"redirectUrlAbout": "O přesměrování URL",
|
||||
"redirectUrlAboutDescription": "Toto je URL, na kterou budou uživatelé po ověření přesměrováni. Tuto URL je třeba nastavit v nastavení poskytovatele identity.",
|
||||
"pangolinAuth": "Auth - Pangolin",
|
||||
"pangolinAuth": "Autentizace - Pangolin",
|
||||
"verificationCodeLengthRequirements": "Váš ověřovací kód musí mít 8 znaků.",
|
||||
"errorOccurred": "Došlo k chybě",
|
||||
"emailErrorVerify": "Nepodařilo se ověřit e-mail:",
|
||||
@@ -1269,7 +1273,7 @@
|
||||
"passwordRequirementUppercaseText": "Velké písmeno (A-Z)",
|
||||
"passwordRequirementLowercaseText": "Malé písmeno (a-z)",
|
||||
"passwordRequirementNumberText": "Číslo (0-9)",
|
||||
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||
"passwordRequirementSpecialText": "Speciální znak (!@#$%...)",
|
||||
"passwordsDoNotMatch": "Hesla se neshodují",
|
||||
"otpEmailRequirementsLength": "OTP musí mít alespoň 1 znak",
|
||||
"otpEmailSent": "OTP odesláno",
|
||||
@@ -1306,12 +1310,12 @@
|
||||
"passwordReset": "Obnovit heslo",
|
||||
"passwordResetDescription": "Postupujte podle kroků pro obnovení hesla",
|
||||
"passwordResetSent": "Na tuto e-mailovou adresu zašleme kód pro obnovení hesla.",
|
||||
"passwordResetCode": "Reset Code",
|
||||
"passwordResetCode": "Kód obnovení",
|
||||
"passwordResetCodeDescription": "Zkontrolujte svůj e-mail pro kód pro obnovení.",
|
||||
"generatePasswordResetCode": "Vygenerovat kód pro obnovení hesla",
|
||||
"passwordResetCodeGenerated": "Kód pro obnovení hesla byl vytvořen",
|
||||
"passwordResetCodeGeneratedDescription": "Sdílejte tento kód s uživatelem. Mohou jej použít k obnovení hesla.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "URL obnovení",
|
||||
"passwordNew": "Nové heslo",
|
||||
"passwordNewConfirm": "Potvrdit nové heslo",
|
||||
"changePassword": "Změnit heslo",
|
||||
@@ -1368,7 +1372,7 @@
|
||||
"inviteErrorExpired": "Pozvánka možná vypršela",
|
||||
"inviteErrorRevoked": "Pozvánka mohla být zrušena",
|
||||
"inviteErrorTypo": "Na pozvánce může být typol",
|
||||
"pangolinSetup": "Setup - Pangolin",
|
||||
"pangolinSetup": "Nastavení - Pangolin",
|
||||
"orgNameRequired": "Je vyžadován název organizace",
|
||||
"orgIdRequired": "Je vyžadováno ID organizace",
|
||||
"orgIdMaxLength": "ID organizace musí mít nejvýše 32 znaků",
|
||||
@@ -1396,7 +1400,7 @@
|
||||
"tagWarnDuplicate": "Duplicitní značka {tagText} nebyla přidána",
|
||||
"supportKeyInvalid": "Neplatný klíč",
|
||||
"supportKeyInvalidDescription": "Váš supporter klíč je neplatný.",
|
||||
"supportKeyValid": "Valid Key",
|
||||
"supportKeyValid": "Platný klíč",
|
||||
"supportKeyValidDescription": "Váš klíč podporovatele byl ověřen. Děkujeme za vaši podporu!",
|
||||
"supportKeyErrorValidationDescription": "Nepodařilo se ověřit klíč podporovatele.",
|
||||
"supportKey": "Podpořte vývoj a přijměte Pangolin!",
|
||||
@@ -1418,12 +1422,30 @@
|
||||
"supportKeyHideSevenDays": "Skrýt na 7 dní",
|
||||
"supportKeyEnter": "Zadejte klíč podpory",
|
||||
"supportKeyEnterDescription": "Seznamte se s vlastním mazlíčkem Pangolin!",
|
||||
"githubUsername": "GitHub Username",
|
||||
"githubUsername": "Uživatelské jméno GitHub",
|
||||
"supportKeyInput": "Klíč pro podporu",
|
||||
"supportKeyBuy": "Koupit klíč pro podporu",
|
||||
"logoutError": "Chyba při odhlášení",
|
||||
"signingAs": "Přihlášen jako",
|
||||
"serverAdmin": "Správce serveru",
|
||||
"promoteServerAdmin": "Podpora na úroveň správce serveru",
|
||||
"promoteServerAdminTitle": "Podpora na úroveň Správce serveru",
|
||||
"promoteServerAdminQuestion": "Jste si jisti, že chcete podpořit {selectedUser} na úroveň správce serveru?",
|
||||
"promoteServerAdminMessage": "Správci serveru mají nejvyšší oprávnění a mohou spravovat server.",
|
||||
"promoteServerAdminWarning": "To lze kdykoli vrátit zpět snížením úrovně uživatele.",
|
||||
"promoteServerAdminConfirm": "Podpora na úroveň Správce serveru",
|
||||
"promoteServerAdminSuccess": "Uživatel podpořen",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} je nyní správce serveru.",
|
||||
"promoteServerAdminError": "Nepodařilo se podpořit uživatele",
|
||||
"demoteServerAdmin": "Snížit úroveň správce serveru",
|
||||
"demoteServerAdminTitle": "Snížit úroveň Správce serveru",
|
||||
"demoteServerAdminQuestion": "Jste si jisti, že chcete snížit {selectedUser} z úrovně správce serveru?",
|
||||
"demoteServerAdminMessage": "{selectedUser} ztratí všechna oprávnění správce serveru.",
|
||||
"demoteServerAdminWarning": "To lze kdykoli vrátit zpět podporou uživatele.",
|
||||
"demoteServerAdminConfirm": "Snížit úroveň ze správce serveru",
|
||||
"demoteServerAdminSuccess": "Uživatel snížen",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} již není správce serveru.",
|
||||
"demoteServerAdminError": "Nepodařilo se snížit úroveň uživatele",
|
||||
"managedSelfhosted": "Spravované vlastní hostování",
|
||||
"otpEnable": "Povolit dvoufaktorové",
|
||||
"otpDisable": "Zakázat dvoufaktorové",
|
||||
@@ -1509,11 +1531,11 @@
|
||||
"actionSetResourcePolicyHeaderAuth": "Nastavit autentizaci hlavičky zásady zdroje",
|
||||
"actionSetResourcePolicyWhitelist": "Nastavit seznam povolených e-mailů v zásadě zdroje",
|
||||
"actionSetResourcePolicyRules": "Nastavit pravidla zásady zdroje",
|
||||
"actionCreateTarget": "Create Target",
|
||||
"actionCreateTarget": "Vytvořit cíl",
|
||||
"actionDeleteTarget": "Odstranit cíl",
|
||||
"actionGetTarget": "Získat cíl",
|
||||
"actionListTargets": "Seznam cílů",
|
||||
"actionUpdateTarget": "Update Target",
|
||||
"actionUpdateTarget": "Aktualizovat cíl",
|
||||
"actionCreateRole": "Vytvořit roli",
|
||||
"actionDeleteRole": "Odstranit roli",
|
||||
"actionGetRole": "Získat roli",
|
||||
@@ -1590,7 +1612,7 @@
|
||||
"idpContinue": "Nebo pokračovat s",
|
||||
"idpLastUsed": "Naposled použito",
|
||||
"otpAuthBack": "Zpět na heslo",
|
||||
"navbar": "Navigation Menu",
|
||||
"navbar": "Navigační menu",
|
||||
"navbarDescription": "Hlavní navigační menu aplikace",
|
||||
"navbarDocsLink": "Dokumentace",
|
||||
"commandPaletteTitle": "Paleta příkazů",
|
||||
@@ -1846,7 +1868,7 @@
|
||||
"aiProviderTypeSearch": "Hledat poskytovatele...",
|
||||
"aiProviderTypeNotFound": "Typ poskytovatele nenalezen",
|
||||
"aiProviderTypeOpenai": "OpenAI",
|
||||
"aiProviderTypeAnthropic": "Anthropic",
|
||||
"aiProviderTypeAnthropic": "Antropický",
|
||||
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||
"aiProviderTypeVertexAi": "Vertex AI",
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
@@ -1874,11 +1896,11 @@
|
||||
"aiProviderAuthType": "Typ ověření",
|
||||
"aiProviderAuthTypeSearch": "Vyhledat typy ověření...",
|
||||
"aiProviderAuthTypeNotFound": "Nebyl nalezen žádný typ ověření",
|
||||
"aiProviderAuthTypeBearer": "Bearer",
|
||||
"aiProviderAuthTypeBearer": "Přepravitel",
|
||||
"aiProviderAuthTypeBearerDescription": "Oprávnění: Bearer klíč. Používá OpenAI a většina poskytovatelů",
|
||||
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||
"aiProviderAuthTypeXApiKey": "x-api-klíč",
|
||||
"aiProviderAuthTypeXApiKeyDescription": "x-api-key záhlaví. Používá Anthropic",
|
||||
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-klíč",
|
||||
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key záhlaví. Používá Google Gemini",
|
||||
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||
"aiProviderAuthTypeHecDescription": "Oprávnění: Splunk klíč. Používá Splunk HTTP Event Collector",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Rozpočet",
|
||||
"resourceBudgetSettingsDescription": "Zjistěte, jak tato AI brána omezuje užívání na základě výdajů nebo limitů tokenů",
|
||||
"sidebarApiKeys": "API klíče",
|
||||
"sidebarOrgs": "Organizace",
|
||||
"sidebarProvisioning": "Zajištění",
|
||||
"sidebarSettings": "Nastavení",
|
||||
"sidebarAllUsers": "Všichni uživatelé",
|
||||
@@ -2105,7 +2128,7 @@
|
||||
"sidebarMachineClients": "Stroje a přístroje",
|
||||
"sidebarDomains": "Domény",
|
||||
"sidebarGeneral": "Spravovat",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarLogAndAnalytics": "Protokoly a analytika",
|
||||
"sidebarBluePrints": "Plány",
|
||||
"sidebarAlerting": "Upozornění",
|
||||
"sidebarHealthChecks": "Kontroly stavu",
|
||||
@@ -2357,7 +2380,7 @@
|
||||
"containerLabels": "Popisky",
|
||||
"containerLabelsCount": "{count, plural, one {# štítek} other {# štítků}}",
|
||||
"containerLabelsTitle": "Popisky kontejneru",
|
||||
"containerLabelEmpty": "<empty>",
|
||||
"containerLabelEmpty": "<prázdné>",
|
||||
"containerPorts": "Přístavy",
|
||||
"containerPortsMore": "+{count} další",
|
||||
"containerActions": "Akce",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Stáhněte si Olm běžící ve vašem systému",
|
||||
"clientOlmCredentials": "Pověření",
|
||||
"clientOlmCredentialsDescription": "Tímto způsobem bude klient autentizovat se serverem",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Koncový bod",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Tajný klíč",
|
||||
"clientCredentialsSave": "Uložit pověření",
|
||||
@@ -2700,7 +2723,7 @@
|
||||
"resourceEnableProxy": "Povolit veřejné proxy",
|
||||
"resourceEnableProxyDescription": "Povolit veřejné proxying pro tento zdroj. To umožňuje přístup ke zdrojům mimo síť prostřednictvím cloudu na otevřeném portu. Vyžaduje nastavení Traefik.",
|
||||
"externalProxyEnabled": "Externí proxy povolen",
|
||||
"addNewTarget": "Add New Target",
|
||||
"addNewTarget": "Přidat nový cíl",
|
||||
"targetsList": "Seznam cílů",
|
||||
"advancedMode": "Pokročilý režim",
|
||||
"advancedSettings": "Pokročilá nastavení",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Nebyly nalezeny žádné zdroje proxy",
|
||||
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné privátní zdroje.",
|
||||
"resourcesTableDestination": "Místo určení",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Přezdívka",
|
||||
"resourcesTableAliasAddress": "Adresa aliasu",
|
||||
"resourcesTableAliasAddressInfo": "Tato adresa je součástí subsítě veřejných služeb organizace. Používá se k řešení záznamů aliasů pomocí interního rozlišení DNS.",
|
||||
"resourcesTableClients": "Klienti",
|
||||
@@ -2865,7 +2888,7 @@
|
||||
"editInternalResourceDialogName": "Jméno",
|
||||
"editInternalResourceDialogProtocol": "Protokol",
|
||||
"editInternalResourceDialogSitePort": "Port webu",
|
||||
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||
"editInternalResourceDialogTargetConfiguration": "Konfigurace cíle",
|
||||
"editInternalResourceDialogCancel": "Zrušit",
|
||||
"editInternalResourceDialogSaveResource": "Uložit dokument",
|
||||
"editInternalResourceDialogSuccess": "Úspěšně",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "IP adresa nebo název hostitele zdroje v síti webu.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "IP nebo název hostitele zdroje v síti webu.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Přezdívka",
|
||||
"editInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Nejsou k dispozici žádné weby",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "K vytvoření privátních zdrojů potřebujete mít alespoň jednu lokalitu Newt s nastavenou podsítí.",
|
||||
@@ -2921,7 +2944,7 @@
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
"createInternalResourceDialogSitePort": "Port webu",
|
||||
"createInternalResourceDialogSitePortDescription": "Použijte tento port pro přístup ke zdroji na webu při připojení s klientem.",
|
||||
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||
"createInternalResourceDialogTargetConfiguration": "Konfigurace cíle",
|
||||
"createInternalResourceDialogDestinationIPDescription": "IP nebo název hostitele zdroje v síti webu.",
|
||||
"createInternalResourceDialogDestinationPortDescription": "Přístav na cílové IP adrese, kde je zdroj dostupný.",
|
||||
"createInternalResourceDialogCancel": "Zrušit",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Místo určení",
|
||||
"createInternalResourceDialogDestinationHostDescription": "IP adresa nebo název hostitele zdroje v síti webu.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Rozsah zdrojů CIDR v síti webu.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Přezdívka",
|
||||
"createInternalResourceDialogAliasDescription": "Volitelný interní DNS alias pro tento dokument.",
|
||||
"internalResourceAliasLocalWarning": "Aliasy končící na .local mohou způsobit problémy s vyřešením díky mDNS v některých sítích.",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP metoda je vyžadována pro HTTP zdroje",
|
||||
@@ -3089,11 +3112,11 @@
|
||||
"regionNorthernEurope": "Severní Evropa",
|
||||
"regionSouthernEurope": "Jižní Evropa",
|
||||
"regionWesternEurope": "Západní Evropa",
|
||||
"regionOceania": "Oceania",
|
||||
"regionOceania": "Oceánie",
|
||||
"regionAustraliaAndNewZealand": "Austrálie a Nový Zéland",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"regionMelanesia": "Melanésie",
|
||||
"regionMicronesia": "Mikronésie",
|
||||
"regionPolynesia": "Polynésie",
|
||||
"managedSelfHosted": {
|
||||
"title": "Spravované vlastní hostování",
|
||||
"description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Odstranit",
|
||||
"idpGoogleConfiguration": "Konfigurace Google",
|
||||
"idpGoogleConfigurationDescription": "Konfigurace přihlašovacích údajů Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Vaše ID klienta Google OAuth2",
|
||||
"idpGoogleClientSecretDescription": "Tajný klíč klienta Google OAuth2",
|
||||
"idpAzureConfiguration": "Nastavení Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Konfigurace Azure Entra ID OAuth2",
|
||||
"idpTenantId": "ID tenanta",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "id tenanta",
|
||||
"idpAzureTenantIdDescription": "ID nájemce Azura (nalezeno v přehledu Azure Active Directory - Azure Active Directory - přehled )",
|
||||
"idpAzureClientIdDescription": "ID klienta pro registraci aplikace Azure",
|
||||
"idpAzureClientSecretDescription": "Tajný klíč registrace aplikace Azure",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "ID klienta pro registraci aplikace Azure",
|
||||
"idpAzureClientSecretDescription2": "Tajný klíč registrace aplikace Azure",
|
||||
"idpGoogleDescription": "Poskytovatel Google OAuth2/OIDC",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Poskytovatel Microsoft Azure OAuth2/OIDC",
|
||||
"subnet": "Podsíť",
|
||||
"utilitySubnet": "Nástrojová podsíť",
|
||||
"subnetDescription": "Podsíť pro konfiguraci sítě této organizace.",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Priorita",
|
||||
"priorityDescription": "Vyšší priorita je vyhodnocena jako první. Priorita = 100 znamená automatické řazení (rozhodnutí systému). Pro vynucení manuální priority použijte jiné číslo.",
|
||||
"instanceName": "Název instance",
|
||||
"clearInstanceName": "Obnovit spojení se serverem",
|
||||
"pathMatchModalTitle": "Nastavit porovnávání cest",
|
||||
"pathMatchModalDescription": "Nastavte jak se bude ověřovat, ze cesty příchozích požadavků se shodují.",
|
||||
"pathMatchType": "Typ shody",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Povoleno pomocí pravidla",
|
||||
"allowedNoAuth": "Povoleno bez ověření",
|
||||
"validAccessToken": "Platný přístupový token",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Platná záhlaví ověření",
|
||||
"validPincode": "Platný pin",
|
||||
"validPassword": "Platné heslo",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Platný email",
|
||||
"validSSO": "Platné SSO",
|
||||
"validVirtualAPIKey": "Platný virtuální API klíč",
|
||||
"view": "Zobrazit",
|
||||
"configManaged": "Správa konfigurace",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Zrušeno pravidlem",
|
||||
"noSessions": "Žádné relace",
|
||||
"temporaryRequestToken": "Dočasný požadavek token",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Žádné platné ověření",
|
||||
"ip": "IP adresa",
|
||||
"reason": "Důvod",
|
||||
"requestLogs": "Záznamy HTTP požadavků",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Obnovení přihlašovacích údajů zneplatní předchozí a způsobí odpojení. Ujistěte se, že aktualizujete všechny konfigurace, které tyto přihlašovací údaje používají.",
|
||||
"confirm": "Potvrdit",
|
||||
"regenerateCredentialsConfirmation": "Jste si jisti, že chcete obnovit přihlašovací údaje?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Koncový bod",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Tajný klíč",
|
||||
"niceId": "Pěkné ID",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Došlo k chybě při aktualizaci identifikátoru Nice.",
|
||||
"niceIdCannotBeEmpty": "Nice ID nemůže být prázdné",
|
||||
"enterIdentifier": "Zadejte identifikátor",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Identifikátor",
|
||||
"deviceLoginUseDifferentAccount": "Nejste vy? Použijte jiný účet.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Zařízení žádá o přístup k tomuto účtu.",
|
||||
"loginSelectAuthenticationMethod": "Chcete-li pokračovat, vyberte metodu ověřování.",
|
||||
"noData": "Žádná data",
|
||||
"machineClients": "Strojoví klienti",
|
||||
"install": "Instalovat",
|
||||
"downloadInstaller": "Stáhnout instalátor",
|
||||
"run": "Spustit",
|
||||
"envFile": "Konfigurační soubor prostředí",
|
||||
"serviceFile": "Služební soubor",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Odpojeno",
|
||||
"approvalsEmptyStateTitle": "Schvalování zařízení není povoleno",
|
||||
"approvalsEmptyStateDescription": "Povolte oprávnění oprávnění pro role správce před připojením nových zařízení.",
|
||||
"approvalsEmptyStateHowToTitle": "Jak povolit",
|
||||
"approvalsEmptyStateStep1Title": "Přejít na role",
|
||||
"approvalsEmptyStateStep1Description": "Přejděte do nastavení rolí vaší organizace pro konfiguraci schválení zařízení.",
|
||||
"approvalsEmptyStateStep2Title": "Povolit schválení zařízení",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Zobrazit jako administrátor",
|
||||
"resourceLauncherResourceDetailsDescription": "Informace o připojení a stavu pro tento zdroj.",
|
||||
"resourceLauncherResourceDetails": "Podrobnosti o zdroji",
|
||||
"resourceLauncherSitesDescription": "Zdroj je přístupný prostřednictvím následujících stránek.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Zobrazit web jako administrátor",
|
||||
"resourceLauncherFilterBySite": "Filtrovat podle stránky",
|
||||
"resourceLauncherSshCommand": "SSH příkaz",
|
||||
"resourceLauncherSshCommandDescription": "Použijte Pangolin CLI pro otevření SSH relace k tomuto zdroji.",
|
||||
"resourceLauncherAuthMethodsDescription": "Povolené metody autentizace pro tento zdroj.",
|
||||
"resourceLauncherPrivateClientRequired": "Připojte se s klientem na vašem zařízení, abyste měli k tomuto zdroji soukromý přístup.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "K připojení klienta vyžadováno",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Režim Unicode klávesnice",
|
||||
"sessionToolbarShow": "Zobrazit panel nástrojů",
|
||||
"sessionToolbarHide": "Skrýt panel nástrojů",
|
||||
"actionUpdateSiteApprovals": "Aktualizovat schválení webu"
|
||||
"actionUpdateSiteApprovals": "Aktualizovat schválení webu",
|
||||
"check": "Zkontrolovat"
|
||||
}
|
||||
|
||||
+52
-20
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Mål på dette site",
|
||||
"siteSetting": "{siteName} Indstillinger",
|
||||
"siteNewtTunnel": "Newt-site (anbefalet)",
|
||||
"pangolinSite": "Pangolin Site",
|
||||
"siteNewtTunnelDescription": "Lekkeste måte at oprette et indgangspunkt til ethvert netværk. Ingen ekstra opsætning på.",
|
||||
"siteWg": "Grunnleggende WireGuard",
|
||||
"siteWgDescription": "Brug en hvilken som helst WireGuard-klient til at etablere en tunnel. Manuel NAT-opsætning er påkrævet.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Slet API-nøgle",
|
||||
"apiKeysManage": "Administrer API-nøgler",
|
||||
"apiKeysDescription": "API-nøgler bruges for at autentificere med integrasjons-API",
|
||||
"orgsManage": "Administrer Organisationer",
|
||||
"orgsDescription": "Vis og administrer alle organisationer i denne instans",
|
||||
"provisioningKeysTitle": "Provisioneringsnøgle",
|
||||
"provisioningKeysManage": "Administrer provisioneringsnøgler",
|
||||
"provisioningKeysDescription": "Provisioneringsnøgler bruges til at godkende automatiseret site-provisionering for din organisation.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Navn (valgfrit)",
|
||||
"accessControls": "Adgangskontroller",
|
||||
"userDescription2": "Administrer indstillingerne for denne bruger",
|
||||
"userGeneralSettingsDescription": "Administrer denne brugers roller og indstillinger i organisationen",
|
||||
"accessRoleErrorAdd": "Kunne ikke tilføje bruger i rolle",
|
||||
"accessRoleErrorAddDescription": "Det opstod en fejl under tildeling af brugeren til rollen.",
|
||||
"userSaved": "Bruger gemt",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Når regler er deaktiveret, går all trafik gennem til autentificering.",
|
||||
"policyAccessRulesFallthroughOn": "Når ingen regler matcher, fortsetter trafikken til autentificering.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/administrator/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Gem Regler",
|
||||
"resourceErrorCreate": "Fejl under oprettelse af ressource",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Ukjent kommando",
|
||||
"newtErrorFetchReleases": "Mislykkedes at hente utgivelsesinfo: {err}",
|
||||
"newtErrorFetchLatest": "Fejl ved hentning af seneste utgivelse: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Slutpunkt",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Sikkerhedsnøgle",
|
||||
"newtVersion": "Version",
|
||||
@@ -1184,7 +1188,7 @@
|
||||
"idpJmespathEmailPathOptionalDescription": "Stien til brugerens e-mailadresse i ID-tokenet",
|
||||
"idpJmespathNamePathOptional": "Navn Sti (Valgfrit)",
|
||||
"idpJmespathNamePathOptionalDescription": "Stien til brugerens navn i ID-tokenet",
|
||||
"idpOidcConfigureScopes": "Scopes",
|
||||
"idpOidcConfigureScopes": "Scope",
|
||||
"idpOidcConfigureScopesDescription": "Mellemrumsepareret liste over OAuth2-scopes at be om",
|
||||
"idpSubmit": "Opret identitetsudbyder",
|
||||
"orgPolicies": "Organisationspolitikker",
|
||||
@@ -1262,7 +1266,7 @@
|
||||
"passwordRequirementsMet": "✓ Adgangskoden opfylder alle krav",
|
||||
"passwordStrength": "Adgangskodestyrke",
|
||||
"passwordStrengthWeak": "Svag",
|
||||
"passwordStrengthMedium": "Medium",
|
||||
"passwordStrengthMedium": "Middel",
|
||||
"passwordStrengthStrong": "Stærk",
|
||||
"passwordRequirements": "Krav:",
|
||||
"passwordRequirementLengthText": "8+ tegn",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Fejl ved logout",
|
||||
"signingAs": "Logget ind som",
|
||||
"serverAdmin": "Serveradministrator",
|
||||
"promoteServerAdmin": "Fremme til serveradministrator",
|
||||
"promoteServerAdminTitle": "Fremme til Serveradministrator",
|
||||
"promoteServerAdminQuestion": "Er du sikker på at du vil forfremme {selectedUser} til serveradministrator?",
|
||||
"promoteServerAdminMessage": "Serveradministratorer har de højeste privilegier og kan administrere serveren.",
|
||||
"promoteServerAdminWarning": "Dette kan fortrydes når som helst ved at degradere brugeren.",
|
||||
"promoteServerAdminConfirm": "Fremme til Serveradministrator",
|
||||
"promoteServerAdminSuccess": "Bruger Forfremmet",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} er nu serveradministrator.",
|
||||
"promoteServerAdminError": "Kunne ikke forfremme bruger",
|
||||
"demoteServerAdmin": "Degradere fra serveradministrator",
|
||||
"demoteServerAdminTitle": "Degradere fra Serveradministrator",
|
||||
"demoteServerAdminQuestion": "Er du sikker på at du vil degradere {selectedUser} fra serveradministrator?",
|
||||
"demoteServerAdminMessage": "{selectedUser} vil miste alle serveradministrator privilegier.",
|
||||
"demoteServerAdminWarning": "Dette kan fortrydes når som helst ved at forfremme brugeren.",
|
||||
"demoteServerAdminConfirm": "Degradere fra serveradministrator",
|
||||
"demoteServerAdminSuccess": "Bruger degraderet",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} er ikke længere serveradministrator.",
|
||||
"demoteServerAdminError": "Kunne ikke degradere bruger",
|
||||
"managedSelfhosted": "Administreret selvhostet",
|
||||
"otpEnable": "Aktivér tofaktor",
|
||||
"otpDisable": "Deaktivér tofaktor",
|
||||
@@ -1977,7 +1999,7 @@
|
||||
"aiProviderModelsCatalogEmpty": "Ingen matchende katalogmodeller.",
|
||||
"aiProviderModelsCatalogHeading": "Kendte Modeller",
|
||||
"aiProviderModelsAllLabel": "Alle modeller",
|
||||
"aiProviderModelsAllPatternHint": "Wildcard: *",
|
||||
"aiProviderModelsAllPatternHint": "Jokertegn: *",
|
||||
"aiProviderModelsAddAllAllow": "Tillad alle modeller",
|
||||
"aiProviderModelsAddAllBlock": "Blokér alle modeller",
|
||||
"aiProviderModelsAddAllDescription": "Bruger * wildcard så hver modelnøgle matcher.",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettingsDescription": "Konfigurer, hvordan denne AI-gateway begrænser brug baseret på udgifts- eller token-grænser",
|
||||
"sidebarApiKeys": "API-nøgler",
|
||||
"sidebarOrgs": "Organisationer",
|
||||
"sidebarProvisioning": "Provisionering",
|
||||
"sidebarSettings": "Indstillinger",
|
||||
"sidebarAllUsers": "Alle brugere",
|
||||
@@ -2149,7 +2172,7 @@
|
||||
"commandBilling": "Fakturering",
|
||||
"commandEnterpriseLicenses": "Licenser",
|
||||
"commandSettings": "Indstillinger",
|
||||
"commandLauncher": "Launcher",
|
||||
"commandLauncher": "Startprogram",
|
||||
"commandResourceLauncher": "Ressource-launcher",
|
||||
"commandSearchResults": "Søgeresultater",
|
||||
"alertingTitle": "Varsling",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Få Olm til at køre på dit system",
|
||||
"clientOlmCredentials": "Legitimationsoplysninger",
|
||||
"clientOlmCredentialsDescription": "Dette er hvordan klienten vil godkende med serveren",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Slutpunkt",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Sikkerhedsnøgle",
|
||||
"clientCredentialsSave": "Gem brugeroplysninger",
|
||||
@@ -2730,7 +2753,7 @@
|
||||
"requireDeviceApproval": "Kræv enhedsgodkendelse",
|
||||
"requireDeviceApprovalDescription": "Brugere med denne rolle skal have nye enheder godkendt af en administrator, før de kan oprette forbindelse og få adgang til ressourcer.",
|
||||
"sshSettings": "SSH",
|
||||
"inferenceSettings": "AI Gateway",
|
||||
"inferenceSettings": "AI-gateway",
|
||||
"sshAccess": "SSH-adgang",
|
||||
"rdpSettings": "RDP",
|
||||
"vncSettings": "VNC",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressourcer fundet.",
|
||||
"resourcesTableNoInternalResourcesFound": "Ingen private ressourcer fundet.",
|
||||
"resourcesTableDestination": "Destination",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Navn",
|
||||
"resourcesTableAliasAddress": "Alias adresse",
|
||||
"resourcesTableAliasAddressInfo": "Denne adressen er en del af organisationens subnet. Den bruges til at løse aliasposter ved hjælp af intern DNS-opløsning.",
|
||||
"resourcesTableClients": "Klienter",
|
||||
@@ -2886,7 +2909,7 @@
|
||||
"editInternalResourceDialogModeCidr": "CIDR",
|
||||
"editInternalResourceDialogModeHttp": "HTTP",
|
||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||
"editInternalResourceDialogModeInference": "AI-gateway",
|
||||
"editInternalResourceDialogModeSsh": "SSH",
|
||||
"editInternalResourceDialogScheme": "Skema",
|
||||
"editInternalResourceDialogEnableSsl": "Aktivér TLS",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "IP-adressen eller værtsnavnet til ressourcen på sitets netværk.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "IP eller hostnavn til ressourcen på sitets netværk.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "CIDR-området til ressourcen på sitets netværk.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Navn",
|
||||
"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 private ressourcer.",
|
||||
@@ -2907,7 +2930,7 @@
|
||||
"privateResourceAllowIcmpPing": "Tillad ICMP-ping",
|
||||
"privateResourceNetworkAccess": "Netværksadgang",
|
||||
"privateResourceNetworkAccessDescription": "Styr TCP/UDP-portadgang og om ICMP-ping er tilladt for denne ressource.",
|
||||
"hostSettings": "Host",
|
||||
"hostSettings": "Vært",
|
||||
"cidrSettings": "CIDR",
|
||||
"createInternalResourceDialogResourceProperties": "Ressourceegenskaber",
|
||||
"createInternalResourceDialogName": "Navn",
|
||||
@@ -2946,7 +2969,7 @@
|
||||
"createInternalResourceDialogModeHttp": "HTTP",
|
||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||
"createInternalResourceDialogModeSsh": "SSH",
|
||||
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||
"createInternalResourceDialogModeInference": "AI-gateway",
|
||||
"scheme": "Skema",
|
||||
"createInternalResourceDialogScheme": "Skema",
|
||||
"createInternalResourceDialogEnableSsl": "Aktivér TLS",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Destination",
|
||||
"createInternalResourceDialogDestinationHostDescription": "IP-adressen eller værtsnavnet til ressourcen på sitets netværk.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR-området til ressourcen på sitets netværk.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Navn",
|
||||
"createInternalResourceDialogAliasDescription": "Et valgfrit internt DNS-alias for denne ressource.",
|
||||
"internalResourceAliasLocalWarning": "Aliasser, der ender på .local, kan forårsage opløsningsproblemer på grund af mDNS på nogle netværk.",
|
||||
"internalResourceDownstreamSchemeRequired": "Skema er påkrævet for HTTP-ressourcer",
|
||||
@@ -3197,7 +3220,7 @@
|
||||
"authPageBrandingRemoveTitle": "Fjern markedsføring for autentiseringsside",
|
||||
"authPageBrandingQuestionRemove": "Er du sikker på at du vil fjerne merkevarebyggingen for autentiseringssider?",
|
||||
"authPageBrandingDeleteConfirm": "Bekræft sletning af merkevarebygging",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingLogoURL": "Logo-URL",
|
||||
"brandingLogoURLOrPath": "Logoen URL eller sti",
|
||||
"brandingLogoPathDescription": "Indtast en URL eller en lokal sti.",
|
||||
"brandingLogoURLDescription": "Indtast en offentligt tilgængelig webadresse til din logobillede.",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Prioritet",
|
||||
"priorityDescription": "Ruter med højere prioritet evalueres først. Prioritet = 100 betyder automatisk rækkefølge (systembeslutninger). Brug et andet tal for at gennemtvinge manuel prioritet.",
|
||||
"instanceName": "Forekomst navn",
|
||||
"clearInstanceName": "Nulstil serverforbindelse",
|
||||
"pathMatchModalTitle": "Konfigurere matching af sti",
|
||||
"pathMatchModalDescription": "Opsæt hvordan indgående forespørgsler skal matches baseret på deres sti.",
|
||||
"pathMatchType": "Matchtype",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Regenerering af legitimationsoplysninger vil ugyldiggøre de forrige og forårsage en frakobling. Sørg for at opdatere alle konfigurationer, der bruger disse legitimationsoplysninger.",
|
||||
"confirm": "Bekræft",
|
||||
"regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimationerne?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Slutpunkt",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Hemmelig nøgle",
|
||||
"niceId": "God ID",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Ingen data",
|
||||
"machineClients": "Maskinklienter",
|
||||
"install": "Installer",
|
||||
"downloadInstaller": "Download Installer",
|
||||
"run": "Kjør",
|
||||
"envFile": "Miljøfil",
|
||||
"serviceFile": "Tjenestefil",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Offline",
|
||||
"approvalsEmptyStateTitle": "Enhedsgodkendelser er ikke aktiveret",
|
||||
"approvalsEmptyStateDescription": "Aktivere godkendelser af enheder for at roller skal godkendes af admin før brugere kan opret forbindelse til nye enheder.",
|
||||
"approvalsEmptyStateHowToTitle": "Sådan aktiveres",
|
||||
"approvalsEmptyStateStep1Title": "Gå til roller",
|
||||
"approvalsEmptyStateStep1Description": "Naviger til organisationens roller indstillinger for at konfigurere enhedsgodkendelser.",
|
||||
"approvalsEmptyStateStep2Title": "Aktivér enhedsgodkendelser",
|
||||
@@ -4214,7 +4240,7 @@
|
||||
"memberPortalResourceDisabled": "Ressource deaktiveret",
|
||||
"memberPortalShowingResources": "Viser {start}-{end} af {total} ressourcer",
|
||||
"resourceLauncherTitle": "Ressource Starter",
|
||||
"resourceSidebarLauncherTitle": "Launcher",
|
||||
"resourceSidebarLauncherTitle": "Startprogram",
|
||||
"resourceLauncherDescription": "Se alle tilgængelige ressourcer og start dem fra ét centralt knudepunkt",
|
||||
"resourceLauncherSearchPlaceholder": "Søg i dine ressourcer...",
|
||||
"resourceLauncherDefaultView": "Standard",
|
||||
@@ -4230,7 +4256,7 @@
|
||||
"resourceLauncherSaveForEveryone": "Gem for Alle",
|
||||
"resourceLauncherSaveForEveryoneDescription": "Del denne visning med alle organisationsmedlemmer. Når den er ikke markeret, er visningen kun synlig for dig.",
|
||||
"resourceLauncherMakePersonal": "Gør Personlig",
|
||||
"resourceLauncherFilter": "Filter",
|
||||
"resourceLauncherFilter": "Filtre",
|
||||
"resourceLauncherFilterWithCount": "Filter, {count} anvendt",
|
||||
"resourceLauncherSort": "Sortér",
|
||||
"resourceLauncherSortAscending": "Sortér stigende",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Vis som Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Forbindelsesoplysninger og status for denne ressource.",
|
||||
"resourceLauncherResourceDetails": "Ressourcedetaljer",
|
||||
"resourceLauncherSitesDescription": "Ressourcen er tilgængelig via følgende sites.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Vis side som Admin",
|
||||
"resourceLauncherFilterBySite": "Filtrer efter site",
|
||||
"resourceLauncherSshCommand": "SSH-kommando",
|
||||
"resourceLauncherSshCommandDescription": "Brug Pangolin CLI til at åbne en SSH-session til denne ressource.",
|
||||
"resourceLauncherAuthMethodsDescription": "Autentificeringsmetoder aktiveret for denne ressource.",
|
||||
"resourceLauncherPrivateClientRequired": "Forbind med en klient på din enhed for at få privat adgang til denne ressource.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Klientforbindelse påkrævet",
|
||||
@@ -4262,7 +4293,7 @@
|
||||
"resourceLauncherTcp": "TCP",
|
||||
"resourceLauncherUdp": "UDP",
|
||||
"resourceLauncherUnlabeled": "Uden Etiket",
|
||||
"resourceLauncherAiGateway": "AI Gateway",
|
||||
"resourceLauncherAiGateway": "AI-gateway",
|
||||
"resourceLauncherNoSite": "Ingen Site",
|
||||
"resourceLauncherAvailableModels": "Tilgængelige Modeller",
|
||||
"resourceLauncherAvailableModelsDescription": "Modeller, du kan bruge med denne AI-gateway.",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode tastaturtilstand",
|
||||
"sessionToolbarShow": "Vis værktøjslinje",
|
||||
"sessionToolbarHide": "Skjul værktøjslinje",
|
||||
"actionUpdateSiteApprovals": "Opdater godkendelser på site"
|
||||
"actionUpdateSiteApprovals": "Opdater godkendelser på site",
|
||||
"check": "Tjek"
|
||||
}
|
||||
|
||||
+44
-12
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Ziele an diesem Standort",
|
||||
"siteSetting": "{siteName} Einstellungen",
|
||||
"siteNewtTunnel": "Newt Standort (empfohlen)",
|
||||
"pangolinSite": "Pangolin Seite",
|
||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
|
||||
"siteWg": "Einfacher WireGuard Tunnel",
|
||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "API-Schlüssel löschen",
|
||||
"apiKeysManage": "API-Schlüssel verwalten",
|
||||
"apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet",
|
||||
"orgsManage": "Organisationen verwalten",
|
||||
"orgsDescription": "Alle Organisationen in dieser Instanz anzeigen und verwalten",
|
||||
"provisioningKeysTitle": "Bereitstellungsschlüssel",
|
||||
"provisioningKeysManage": "Bereitstellungsschlüssel verwalten",
|
||||
"provisioningKeysDescription": "Bereitstellungsschlüssel werden verwendet, um die automatisierte Bereitstellung von Standorten für Ihr Unternehmen zu authentifizieren.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Name (optional)",
|
||||
"accessControls": "Zugriffskontrolle",
|
||||
"userDescription2": "Verwalten Sie die Einstellungen dieses Benutzers",
|
||||
"userGeneralSettingsDescription": "Die Rollen und Einstellungen dieses Benutzers in der Organisation verwalten",
|
||||
"accessRoleErrorAdd": "Fehler beim Hinzufügen des Benutzers zur Rolle",
|
||||
"accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.",
|
||||
"userSaved": "Benutzer gespeichert",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Passwort zurücksetzen Code generieren",
|
||||
"passwordResetCodeGenerated": "Passwort zurücksetzen Code generiert",
|
||||
"passwordResetCodeGeneratedDescription": "Teilen Sie diesen Code mit dem Benutzer. Sie können ihn verwenden, um ihr Passwort zurückzusetzen.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "URL zurücksetzen",
|
||||
"passwordNew": "Neues Passwort",
|
||||
"passwordNewConfirm": "Neues Passwort bestätigen",
|
||||
"changePassword": "Passwort ändern",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Fehler beim Abmelden",
|
||||
"signingAs": "Angemeldet als",
|
||||
"serverAdmin": "Server-Administrator",
|
||||
"promoteServerAdmin": "Zum Server-Admin befördern",
|
||||
"promoteServerAdminTitle": "Zum Server-Admin befördern",
|
||||
"promoteServerAdminQuestion": "Sind Sie sicher, dass Sie {selectedUser} zum Server-Admin befördern möchten?",
|
||||
"promoteServerAdminMessage": "Server-Admins haben die höchsten Privilegien und können den Server verwalten.",
|
||||
"promoteServerAdminWarning": "Das kann jederzeit rückgängig gemacht werden, indem der Benutzer degradiert wird.",
|
||||
"promoteServerAdminConfirm": "Zum Server-Admin befördern",
|
||||
"promoteServerAdminSuccess": "Benutzer befördert",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} ist jetzt ein Server-Admin.",
|
||||
"promoteServerAdminError": "Fehler beim Befördern des Benutzers",
|
||||
"demoteServerAdmin": "Vom Server-Admin degradieren",
|
||||
"demoteServerAdminTitle": "Vom Server-Admin degradieren",
|
||||
"demoteServerAdminQuestion": "Sind Sie sicher, dass Sie {selectedUser} als Server-Admin degradieren möchten?",
|
||||
"demoteServerAdminMessage": "{selectedUser} wird alle Server-Admin-Rechte verlieren.",
|
||||
"demoteServerAdminWarning": "Das kann jederzeit rückgängig gemacht werden, indem der Benutzer befördert wird.",
|
||||
"demoteServerAdminConfirm": "Vom Server-Admin degradieren",
|
||||
"demoteServerAdminSuccess": "Benutzer degradiert",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} ist kein Server-Admin mehr.",
|
||||
"demoteServerAdminError": "Fehler beim Degradieren des Benutzers",
|
||||
"managedSelfhosted": "Verwaltetes Selbsthosted",
|
||||
"otpEnable": "Zwei-Faktor aktivieren",
|
||||
"otpDisable": "Zwei-Faktor deaktivieren",
|
||||
@@ -1682,7 +1704,7 @@
|
||||
"commandAiProviders": "KI-Anbieter",
|
||||
"sidebarVirtualApiKeys": "Virtuelle API-Schlüssel",
|
||||
"sidebarMyApiKeys": "Ihre API-Schlüssel",
|
||||
"sidebarAccount": "Launcher",
|
||||
"sidebarAccount": "Starter",
|
||||
"commandVirtualApiKeys": "Virtuelle API-Schlüssel",
|
||||
"virtualApiKeysTitle": "Virtuelle API-Schlüssel verwalten",
|
||||
"virtualApiKeysDescription": "Erstellen und verwalten Sie manuelle API-Schlüssel für den Zugang zum KI-Gateway des öffentlichen KI-Gateways",
|
||||
@@ -1885,7 +1907,7 @@
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer-Schlüssel. Wird vom Cloudflare AI Gateway verwendet",
|
||||
"aiProviderAuthTypeNone": "Keine Auth",
|
||||
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||
"aiProviderAuthTypePassthrough": "Durchreichen",
|
||||
"aiProviderAuthTypeDescription": "Wie die Upstream-API Anfragen authentifiziert",
|
||||
"aiProviderAuthTypePassthroughDescription": "Leiten Sie die API-Schlüssel-Header des Anrufers an den Upstream weiter",
|
||||
"aiProviderAuthTypeNoneDescription": "Keine Authentifizierungs-Header an den Upstream senden",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettingsDescription": "Konfigurieren Sie, wie dieses KI-Gateway die Nutzung basierend auf Ausgabe- oder Token-Limits einschränkt",
|
||||
"sidebarApiKeys": "API-Schlüssel",
|
||||
"sidebarOrgs": "Organisationen",
|
||||
"sidebarProvisioning": "Bereitstellung",
|
||||
"sidebarSettings": "Einstellungen",
|
||||
"sidebarAllUsers": "Alle Benutzer",
|
||||
@@ -2110,7 +2133,7 @@
|
||||
"sidebarAlerting": "Benachrichtigung",
|
||||
"sidebarHealthChecks": "Gesundheits-Checks",
|
||||
"sidebarOrganization": "Organisation",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarManagement": "Verwaltung",
|
||||
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
|
||||
"sidebarLogsAnalytics": "Analytik",
|
||||
"commandSites": "Seiten",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Entfernen",
|
||||
"idpGoogleConfiguration": "Google-Konfiguration",
|
||||
"idpGoogleConfigurationDescription": "Google OAuth2 Zugangsdaten konfigurieren",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client-ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 Client Geheimnis",
|
||||
"idpAzureConfiguration": "Azure Entra ID Konfiguration",
|
||||
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 Zugangsdaten konfigurieren",
|
||||
"idpTenantId": "Mandanten-ID",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "mandanten-ID",
|
||||
"idpAzureTenantIdDescription": "Azure Tenant ID (gefunden in Azure Active Directory Übersicht)",
|
||||
"idpAzureClientIdDescription": "Azure App Registration Client ID",
|
||||
"idpAzureClientSecretDescription": "Azure App Registration Client Geheimnis",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Priorität",
|
||||
"priorityDescription": "Die Routen mit höherer Priorität werden zuerst ausgewertet. Priorität = 100 bedeutet automatische Bestellung (Systementscheidung). Verwenden Sie eine andere Nummer, um manuelle Priorität zu erzwingen.",
|
||||
"instanceName": "Instanzname",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Pfad anpassen konfigurieren",
|
||||
"pathMatchModalDescription": "Legen Sie fest, wie eingehende Anfragen basierend auf ihrem Pfad übereinstimmen sollen.",
|
||||
"pathMatchType": "Übereinstimmungstyp",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Keine Daten",
|
||||
"machineClients": "Maschinen-Clients",
|
||||
"install": "Installieren",
|
||||
"downloadInstaller": "Installer herunterladen",
|
||||
"run": "Ausführen",
|
||||
"envFile": "Umgebungsdatei",
|
||||
"serviceFile": "Servicedatei",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Verbindung getrennt",
|
||||
"approvalsEmptyStateTitle": "Gerätezulassungen nicht aktiviert",
|
||||
"approvalsEmptyStateDescription": "Aktiviere Gerätegenehmigungen für Rollen, um Administratorgenehmigungen zu benötigen, bevor Benutzer neue Geräte verbinden können.",
|
||||
"approvalsEmptyStateHowToTitle": "Aktivieren",
|
||||
"approvalsEmptyStateStep1Title": "Gehe zu Rollen",
|
||||
"approvalsEmptyStateStep1Description": "Navigieren Sie zu den Rolleneinstellungen Ihrer Organisation, um die Gerätefreigaben zu konfigurieren.",
|
||||
"approvalsEmptyStateStep2Title": "Gerätegenehmigungen aktivieren",
|
||||
@@ -3982,12 +4008,12 @@
|
||||
"approvalsEmptyStateButtonText": "Rollen verwalten",
|
||||
"domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain",
|
||||
"idpAdminAutoProvisionPoliciesTabHint": "Konfigurieren Sie Rollenzuordnungs- und Organisationsrichtlinien auf der Registerkarte <policiesTabLink>Auto-Bereitstellungseinstellungen</policiesTabLink>.",
|
||||
"streamingTitle": "Event Streaming",
|
||||
"streamingTitle": "Ereignis-Streaming",
|
||||
"streamingDescription": "Streamen Sie Events aus Ihrem Unternehmen in Echtzeit zu externen Zielen.",
|
||||
"streamingUnnamedDestination": "Unbenanntes Ziel",
|
||||
"streamingNoUrlConfigured": "Keine URL konfiguriert",
|
||||
"streamingAddDestination": "Ziel hinzufügen",
|
||||
"streamingHttpWebhookTitle": "HTTP Webhook",
|
||||
"streamingHttpWebhookTitle": "HTTP-Webhook",
|
||||
"streamingHttpWebhookDescription": "Sende Ereignisse an jeden HTTP-Endpunkt mit flexibler Authentifizierung und Vorlage.",
|
||||
"streamingS3Title": "Amazon S3",
|
||||
"streamingS3Description": "Streame Ereignisse in eine S3-kompatible Objekt-Speicher-Eimer. Kommt bald.",
|
||||
@@ -4079,7 +4105,7 @@
|
||||
"httpDestBodyTemplateHint": "Verwenden Sie Template-Variablen, um Ereignisfelder in Ihrer Payload zu referenzieren.",
|
||||
"httpDestPayloadFormatTitle": "Payload-Format",
|
||||
"httpDestPayloadFormatDescription": "Wie Ereignisse in jedes Anfragegremium serialisiert werden.",
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"httpDestFormatJsonArrayTitle": "JSON-Array",
|
||||
"httpDestFormatJsonArrayDescription": "Eine Anfrage pro Stapel ist ein JSON-Array. Kompatibel mit den meisten generischen Webhooks und Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Eine Anfrage pro Batch, der Körper ist newline-getrenntes JSON - ein Objekt pro Zeile, kein äußeres Array. Benötigt von Splunk HEC, Elastic / OpenSearch, und Grafana Loki.",
|
||||
@@ -4214,7 +4240,7 @@
|
||||
"memberPortalResourceDisabled": "Ressource deaktiviert",
|
||||
"memberPortalShowingResources": "Zeige {start}-{end} von {total} Ressourcen",
|
||||
"resourceLauncherTitle": "Ressourcenstarter",
|
||||
"resourceSidebarLauncherTitle": "Launcher",
|
||||
"resourceSidebarLauncherTitle": "Starter",
|
||||
"resourceLauncherDescription": "Alle verfügbaren Ressourcen anzeigen und von einem zentralen Hub aus starten",
|
||||
"resourceLauncherSearchPlaceholder": "Suche deine Ressourcen...",
|
||||
"resourceLauncherDefaultView": "Standard",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Ansicht als Administrator anzeigen",
|
||||
"resourceLauncherResourceDetailsDescription": "Verbindungsinformationen und Status für diese Ressource.",
|
||||
"resourceLauncherResourceDetails": "Ressourcendetails",
|
||||
"resourceLauncherSitesDescription": "Die Ressource ist über die folgenden Standorte zugänglich.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Seite als Administrator anzeigen",
|
||||
"resourceLauncherFilterBySite": "Nach Standort filtern",
|
||||
"resourceLauncherSshCommand": "SSH-Befehl",
|
||||
"resourceLauncherSshCommandDescription": "Verwenden Sie die Pangolin-CLI, um eine SSH-Sitzung zu dieser Ressource zu öffnen.",
|
||||
"resourceLauncherAuthMethodsDescription": "Aktivierte Authentifizierungsmethoden für diese Ressource.",
|
||||
"resourceLauncherPrivateClientRequired": "Verbinde dich mit einem Client auf deinem Gerät, um privat auf diese Ressource zuzugreifen.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Client-Verbindung erforderlich",
|
||||
@@ -4354,7 +4385,7 @@
|
||||
"rdpConnectionFailed": "Verbindung fehlgeschlagen",
|
||||
"rdpFit": "Anpassen",
|
||||
"rdpFull": "Vollständig",
|
||||
"rdpReal": "Real",
|
||||
"rdpReal": "Echt",
|
||||
"rdpMeta": "Meta",
|
||||
"rdpUploadFiles": "Dateien hochladen",
|
||||
"rdpFilesReadyToPaste": "Dateien bereit zum Einfügen",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus",
|
||||
"sessionToolbarShow": "Werkzeugleiste zeigen",
|
||||
"sessionToolbarHide": "Werkzeugleiste ausblenden",
|
||||
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren"
|
||||
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren",
|
||||
"check": "Prüfen"
|
||||
}
|
||||
|
||||
+50
-18
@@ -80,7 +80,7 @@
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerButtonText": "Install Site Connector",
|
||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||
@@ -152,7 +152,8 @@
|
||||
"siteResourcesHowToAccess": "How to access",
|
||||
"siteResourcesTargetsOnSite": "Targets on this site",
|
||||
"siteSetting": "{siteName} Settings",
|
||||
"siteNewtTunnel": "Newt Site (Recommended)",
|
||||
"siteNewtTunnel": "Pangolin Site (Recommended)",
|
||||
"pangolinSite": "Pangolin Site",
|
||||
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into any network. No extra setup.",
|
||||
"siteWg": "Basic WireGuard",
|
||||
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
|
||||
@@ -226,9 +227,9 @@
|
||||
"never": "Never",
|
||||
"shareErrorSelectResource": "Please select a resource",
|
||||
"proxyResourceTitle": "Manage Public Resources",
|
||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible via a proxy",
|
||||
"publicResourcesBannerTitle": "Web-based Public Access",
|
||||
"publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet through a web browser and include identity and context-aware access policies. Unlike private resources, they do not require client-side software.",
|
||||
"publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet, like a website or API, and include identity and context-aware access policies. Unlike private resources, they do not require any client-side software to access.",
|
||||
"clientResourceTitle": "Manage Private Resources",
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Delete API Key",
|
||||
"apiKeysManage": "Manage API Keys",
|
||||
"apiKeysDescription": "API keys are used to authenticate with the integration API",
|
||||
"orgsManage": "Manage Organizations",
|
||||
"orgsDescription": "View and manage all organizations on this instance",
|
||||
"provisioningKeysTitle": "Provisioning Key",
|
||||
"provisioningKeysManage": "Manage Provisioning Keys",
|
||||
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
|
||||
@@ -520,12 +523,12 @@
|
||||
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
|
||||
"pendingSitesBannerButtonText": "Learn More",
|
||||
"apiKeysSettings": "{apiKeyName} Settings",
|
||||
"userTitle": "Manage All Users",
|
||||
"userDescription": "View and manage all users in the system",
|
||||
"userTitle": "Manage Users",
|
||||
"userDescription": "View and manage all users in this instance",
|
||||
"userAbount": "About User Management",
|
||||
"userAbountDescription": "This table displays all base user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their base user object. They will remain in the system. To completely remove a user from the system, you must delete their base user object using the delete action in this table.",
|
||||
"userServer": "Server Users",
|
||||
"userSearch": "Search server users...",
|
||||
"userSearch": "Search users...",
|
||||
"userErrorDelete": "Error deleting user",
|
||||
"userDeleteConfirm": "Confirm Delete User",
|
||||
"userDeleteServer": "Delete User from Server",
|
||||
@@ -594,7 +597,7 @@
|
||||
"licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ",
|
||||
"invite": "Invitations",
|
||||
"inviteRegenerate": "Regenerate Invitation",
|
||||
"inviteRegenerateDescription": "Revoke previous invitation and create a new one",
|
||||
"inviteRegenerateDescription": "Create a new invite link for this user. The previous invitation will be revoked.",
|
||||
"inviteRemove": "Remove Invitation",
|
||||
"inviteRemoveError": "Failed to remove invitation",
|
||||
"inviteRemoveErrorDescription": "An error occurred while removing the invitation.",
|
||||
@@ -674,11 +677,11 @@
|
||||
"accessUserCreateDescription": "Follow the steps below to create a new user",
|
||||
"userSeeAll": "See All Users",
|
||||
"userTypeTitle": "User Type",
|
||||
"userTypeDescription": "Determine how you want to create the user",
|
||||
"userTypeDescription": "Select the identity provider to use for this user",
|
||||
"userSettings": "User Information",
|
||||
"userSettingsDescription": "Enter the details for the new user",
|
||||
"userSettingsDescription": "Enter the general details for the new user",
|
||||
"inviteEmailSent": "Send invite email to user",
|
||||
"inviteValid": "Invite Valid For (days)",
|
||||
"inviteValid": "Invite Valid For",
|
||||
"selectDuration": "Select duration",
|
||||
"selectResource": "Select Resource",
|
||||
"filterByResource": "Filter By Resource",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Name (Optional)",
|
||||
"accessControls": "Access Controls",
|
||||
"userDescription2": "Manage the settings on this user",
|
||||
"userGeneralSettingsDescription": "Manage this user's roles and settings in the organization",
|
||||
"accessRoleErrorAdd": "Failed to add user to role",
|
||||
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
||||
"userSaved": "User saved",
|
||||
@@ -1214,7 +1218,7 @@
|
||||
"orgPoliciesEdit": "Edit Organization Policy",
|
||||
"org": "Organization",
|
||||
"orgSelect": "Select organization",
|
||||
"orgSearch": "Search org",
|
||||
"orgSearch": "Search organizations...",
|
||||
"orgNotFound": "No org found.",
|
||||
"roleMappingPathOptional": "Role Mapping Path (Optional)",
|
||||
"orgMappingPathOptional": "Organization Mapping Path (Optional)",
|
||||
@@ -1353,7 +1357,7 @@
|
||||
"siteLabelsDescription": "Manage labels associated with this site.",
|
||||
"labelsNotFound": "No labels found.",
|
||||
"labelsEmptyCreateHint": "Start typing above to create a label.",
|
||||
"labelSearch": "Search labels",
|
||||
"labelSearch": "Search labels...",
|
||||
"labelSearchOrCreate": "Search or create a label",
|
||||
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
|
||||
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Error logging out",
|
||||
"signingAs": "Signed in as",
|
||||
"serverAdmin": "Server Admin",
|
||||
"promoteServerAdmin": "Promote to Server admin",
|
||||
"promoteServerAdminTitle": "Promote to Server Admin",
|
||||
"promoteServerAdminQuestion": "Are you sure you want to promote {selectedUser} to server admin?",
|
||||
"promoteServerAdminMessage": "Server admins have the highest privileges and can manage the server.",
|
||||
"promoteServerAdminWarning": "This can be undone at any time by demoting the user.",
|
||||
"promoteServerAdminConfirm": "Promote to Server Admin",
|
||||
"promoteServerAdminSuccess": "User Promoted",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.",
|
||||
"promoteServerAdminError": "Failed to promote user",
|
||||
"demoteServerAdmin": "Demote from Server admin",
|
||||
"demoteServerAdminTitle": "Demote from Server Admin",
|
||||
"demoteServerAdminQuestion": "Are you sure you want to demote {selectedUser} from server admin?",
|
||||
"demoteServerAdminMessage": "{selectedUser} will lose all server admin privileges.",
|
||||
"demoteServerAdminWarning": "This can be undone at any time by promoting the user.",
|
||||
"demoteServerAdminConfirm": "Demote from server admin",
|
||||
"demoteServerAdminSuccess": "User demoted",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} is no longer a server admin.",
|
||||
"demoteServerAdminError": "Failed to demote user",
|
||||
"managedSelfhosted": "Managed Self-Hosted",
|
||||
"otpEnable": "Enable Two-factor",
|
||||
"otpDisable": "Disable Two-factor",
|
||||
@@ -2095,9 +2117,10 @@
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettingsDescription": "Configure how this AI gateway restricts usage based on spending or token limits",
|
||||
"sidebarApiKeys": "API Keys",
|
||||
"sidebarOrgs": "Organizations",
|
||||
"sidebarProvisioning": "Provisioning",
|
||||
"sidebarSettings": "Settings",
|
||||
"sidebarAllUsers": "All Users",
|
||||
"sidebarAllUsers": "Users",
|
||||
"sidebarIdentityProviders": "Identity Providers",
|
||||
"sidebarLicense": "License",
|
||||
"sidebarClients": "Clients",
|
||||
@@ -2898,7 +2921,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 private resources.",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one 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",
|
||||
@@ -3477,7 +3500,8 @@
|
||||
},
|
||||
"priority": "Priority",
|
||||
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
||||
"instanceName": "Instance Name",
|
||||
"instanceName": "Server ID",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "No Data",
|
||||
"machineClients": "Machine Clients",
|
||||
"install": "Install",
|
||||
"downloadInstaller": "Download Installer",
|
||||
"run": "Run",
|
||||
"envFile": "Environment File",
|
||||
"serviceFile": "Service File",
|
||||
@@ -3974,11 +3999,12 @@
|
||||
"disconnected": "Disconnected",
|
||||
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||
"approvalsEmptyStateHowToTitle": "How to Enable",
|
||||
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||
"approvalsEmptyStatePreviewDescription": "When enabled, pending device requests will appear here for review.",
|
||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||
"domainErrorTitle": "We are having trouble verifying your domain",
|
||||
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
|
||||
"resourceLauncherResourceDetails": "Resource Details",
|
||||
"resourceLauncherSitesDescription": "The resource is accessible via the following sites.",
|
||||
"resourceLauncherViewSiteAsAdmin": "View Site as Admin",
|
||||
"resourceLauncherFilterBySite": "Filter by Site",
|
||||
"resourceLauncherSshCommand": "SSH Command",
|
||||
"resourceLauncherSshCommandDescription": "Use the Pangolin CLI to open an SSH session to this resource.",
|
||||
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
|
||||
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
|
||||
"sessionToolbarShow": "Show toolbar",
|
||||
"sessionToolbarHide": "Hide toolbar",
|
||||
"actionUpdateSiteApprovals": "Update Site Approvals"
|
||||
"actionUpdateSiteApprovals": "Update Site Approvals",
|
||||
"check": "Check"
|
||||
}
|
||||
|
||||
+52
-20
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Objetivos en este sitio",
|
||||
"siteSetting": "Ajustes {siteName}",
|
||||
"siteNewtTunnel": "Sitio nuevo (recomendado)",
|
||||
"pangolinSite": "Sitio de Pangolin",
|
||||
"siteNewtTunnelDescription": "La forma más fácil de crear un punto de entrada en cualquier red. Sin configuración extra.",
|
||||
"siteWg": "Wirex Guardia Básica",
|
||||
"siteWgDescription": "Utilice cualquier cliente Wirex Guard para establecer un túnel. Se requiere una configuración manual de NAT.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Borrar Clave API",
|
||||
"apiKeysManage": "Administrar claves API",
|
||||
"apiKeysDescription": "Las claves API se utilizan para autenticar con la API de integración",
|
||||
"orgsManage": "Administrar Organizaciones",
|
||||
"orgsDescription": "Ver y gestionar todas las organizaciones en esta instancia",
|
||||
"provisioningKeysTitle": "Clave de aprovisionamiento",
|
||||
"provisioningKeysManage": "Administrar Claves de Aprovisionamiento",
|
||||
"provisioningKeysDescription": "Las claves de aprovisionamiento se utilizan para autenticar la provisión automatizada del sitio para su organización.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Nombre (opcional)",
|
||||
"accessControls": "Controles de acceso",
|
||||
"userDescription2": "Administrar la configuración de este usuario",
|
||||
"userGeneralSettingsDescription": "Gestionar los roles y configuraciones de este usuario en la organización",
|
||||
"accessRoleErrorAdd": "No se pudo agregar el usuario al rol",
|
||||
"accessRoleErrorAddDescription": "Ocurrió un error mientras se añadía el usuario al rol.",
|
||||
"userSaved": "Usuario guardado",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Nombre de servidor TLS inválido. Utilice el formato de nombre de dominio o guarde en blanco para eliminar el nombre de servidor TLS.",
|
||||
"proxyEnableSSL": "Activar TLS",
|
||||
"proxyEnableSSLDescription": "Habilita el cifrado SSL/TLS para conexiones seguras HTTPS a los objetivos.",
|
||||
"target": "Target",
|
||||
"target": "Destino",
|
||||
"configureTarget": "Configurar objetivos",
|
||||
"targetErrorFetch": "Error al recuperar los objetivos",
|
||||
"targetErrorFetchDescription": "Se ha producido un error al recuperar los objetivos",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Comando desconocido",
|
||||
"newtErrorFetchReleases": "No se pudo obtener la información del lanzamiento: {err}",
|
||||
"newtErrorFetchLatest": "Error obteniendo la última versión: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Punto final",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Secreto",
|
||||
"newtVersion": "Versión",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Generar código de restablecimiento de contraseña",
|
||||
"passwordResetCodeGenerated": "Código de restablecimiento de contraseña generado",
|
||||
"passwordResetCodeGeneratedDescription": "Comparte este código con el usuario. Pueden usarlo para restablecer su contraseña.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "URL de reinicio",
|
||||
"passwordNew": "Nueva contraseña",
|
||||
"passwordNewConfirm": "Confirmar nueva contraseña",
|
||||
"changePassword": "Cambiar Contraseña",
|
||||
@@ -1383,8 +1387,8 @@
|
||||
"pangolinDashboard": "Tablero - Pangolin",
|
||||
"noResults": "No se han encontrado resultados.",
|
||||
"terabytes": "TB {count}",
|
||||
"gigabytes": "{count} GB",
|
||||
"megabytes": "{count} MB",
|
||||
"gigabytes": "{count, plural, one {{count} GB} other {{count} GB}}",
|
||||
"megabytes": "{count, plural, one {{count} MB} other {{count} MB}}",
|
||||
"tagsEntered": "Etiquetas introducidas",
|
||||
"tagsEnteredDescription": "Estas son las etiquetas que has introducido.",
|
||||
"tagsWarnCannotBeLessThanZero": "maxTags y minTags no pueden ser menores que 0",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Error al cerrar sesión",
|
||||
"signingAs": "Conectado como",
|
||||
"serverAdmin": "Admin Servidor",
|
||||
"promoteServerAdmin": "Promover a Admin del Servidor",
|
||||
"promoteServerAdminTitle": "Promover a Admin del Servidor",
|
||||
"promoteServerAdminQuestion": "¿Estás seguro de que quieres promover a {selectedUser} a admin del servidor?",
|
||||
"promoteServerAdminMessage": "Los administradores del servidor tienen los máximos privilegios y pueden gestionar el servidor.",
|
||||
"promoteServerAdminWarning": "Esto se puede deshacer en cualquier momento degradando al usuario.",
|
||||
"promoteServerAdminConfirm": "Promover a Admin del Servidor",
|
||||
"promoteServerAdminSuccess": "Usuario Promovido",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} ahora es administrador del servidor.",
|
||||
"promoteServerAdminError": "Error al promover al usuario",
|
||||
"demoteServerAdmin": "Degradar de Admin del Servidor",
|
||||
"demoteServerAdminTitle": "Degradar de Admin del Servidor",
|
||||
"demoteServerAdminQuestion": "¿Estás seguro de que quieres degradar a {selectedUser} de administrador del servidor?",
|
||||
"demoteServerAdminMessage": "{selectedUser} perderá todos los privilegios de administrador del servidor.",
|
||||
"demoteServerAdminWarning": "Esto se puede deshacer en cualquier momento promoviendo al usuario.",
|
||||
"demoteServerAdminConfirm": "Degradar de Admin del Servidor",
|
||||
"demoteServerAdminSuccess": "Usuario Degradado",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} ya no es administrador del servidor.",
|
||||
"demoteServerAdminError": "Error al degradar al usuario",
|
||||
"managedSelfhosted": "Autogestionado",
|
||||
"otpEnable": "Activar doble factor",
|
||||
"otpDisable": "Desactivar doble factor",
|
||||
@@ -1924,7 +1946,7 @@
|
||||
"aiProviderCapabilitiesSelect": "Seleccionar capacidades",
|
||||
"aiProviderCapabilitiesEmpty": "No se encontraron capacidades",
|
||||
"aiProviderCapabilitiesSearch": "Buscar capacidades...",
|
||||
"aiCapabilityOpenaiChat": "OpenAI Chat Completions",
|
||||
"aiCapabilityOpenaiChat": "Completaciones de Chat OpenAI",
|
||||
"aiCapabilityOpenaiChatDescription": "Admite /v1/chat/completions",
|
||||
"aiCapabilityOpenaiResponses": "Respuestas de OpenAI",
|
||||
"aiCapabilityOpenaiResponsesDescription": "Admite /v1/responses",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Presupuesto",
|
||||
"resourceBudgetSettingsDescription": "Configure cómo este portal de IA restringe el uso en función de los límites de gasto o tokens",
|
||||
"sidebarApiKeys": "Claves API",
|
||||
"sidebarOrgs": "Organizaciones",
|
||||
"sidebarProvisioning": "Aprovisionamiento",
|
||||
"sidebarSettings": "Ajustes",
|
||||
"sidebarAllUsers": "Todos los usuarios",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Obtén Olm funcionando en tu sistema",
|
||||
"clientOlmCredentials": "Credenciales",
|
||||
"clientOlmCredentialsDescription": "Así es como el cliente se autentificará con el servidor",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Punto final",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Secreto",
|
||||
"clientCredentialsSave": "Guardar las credenciales",
|
||||
@@ -2907,7 +2930,7 @@
|
||||
"privateResourceAllowIcmpPing": "Permitir ping ICMP",
|
||||
"privateResourceNetworkAccess": "Acceso de red",
|
||||
"privateResourceNetworkAccessDescription": "Controlar el acceso a puertos TCP/UDP y si se permite ping ICMP para este recurso.",
|
||||
"hostSettings": "Host",
|
||||
"hostSettings": "Anfitrión",
|
||||
"cidrSettings": "CIDR",
|
||||
"createInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
||||
"createInternalResourceDialogName": "Nombre",
|
||||
@@ -3067,7 +3090,7 @@
|
||||
"rulesMatchRegion": "Seleccione una agrupación regional de países",
|
||||
"rulesErrorInvalidRegion": "Región no válida",
|
||||
"rulesErrorInvalidRegionDescription": "Por favor, seleccione una región válida.",
|
||||
"regionAfrica": "Africa",
|
||||
"regionAfrica": "África",
|
||||
"regionNorthernAfrica": "África septentrional",
|
||||
"regionEasternAfrica": "África oriental",
|
||||
"regionMiddleAfrica": "África central",
|
||||
@@ -3089,7 +3112,7 @@
|
||||
"regionNorthernEurope": "Europa septentrional",
|
||||
"regionSouthernEurope": "Europa meridional",
|
||||
"regionWesternEurope": "Europa Occidental",
|
||||
"regionOceania": "Oceania",
|
||||
"regionOceania": "Oceanía",
|
||||
"regionAustraliaAndNewZealand": "Australia y Nueva Zelanda",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Eliminar",
|
||||
"idpGoogleConfiguration": "Configuración de Google",
|
||||
"idpGoogleConfigurationDescription": "Configurar las credenciales de Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Tu ID de cliente de Google OAuth2",
|
||||
"idpGoogleClientSecretDescription": "Secreto del cliente de Google OAuth2",
|
||||
"idpAzureConfiguration": "Configuración de Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Configurar credenciales de Azure Entra ID OAuth2",
|
||||
"idpTenantId": "ID del inquilino",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "iD del inquilino",
|
||||
"idpAzureTenantIdDescription": "ID de inquilino Azure (encontrado en la descripción de Azure Active Directory)",
|
||||
"idpAzureClientIdDescription": "ID de cliente de registro de Azure App",
|
||||
"idpAzureClientSecretDescription": "Azure App Registro Cliente secreto",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "ID de cliente de registro de Azure App",
|
||||
"idpAzureClientSecretDescription2": "Azure App Registro Cliente secreto",
|
||||
"idpGoogleDescription": "Proveedor OAuth2/OIDC de Google",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Proveedor OAuth2/OIDC de Google",
|
||||
"subnet": "Subred",
|
||||
"utilitySubnet": "Subred de Utilidad",
|
||||
"subnetDescription": "La subred para la configuración de red de esta organización.",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Prioridad",
|
||||
"priorityDescription": "Las rutas de prioridad más alta son evaluadas primero. Prioridad = 100 significa orden automático (decisiones del sistema). Utilice otro número para hacer cumplir la prioridad manual.",
|
||||
"instanceName": "Nombre de instancia",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Configurar ruta coincidente",
|
||||
"pathMatchModalDescription": "Configurar cómo deben coincidir las peticiones entrantes en función de su ruta.",
|
||||
"pathMatchType": "Tipo de partida",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Permitido por regla",
|
||||
"allowedNoAuth": "No se permite autorización",
|
||||
"validAccessToken": "Token de Acceso Válido",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Autenticación Básica del Encabezado",
|
||||
"validPincode": "Definir Pincode",
|
||||
"validPassword": "Contraseña válida",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Válido hasta",
|
||||
"validSSO": "Clave válida",
|
||||
"validVirtualAPIKey": "Clave API Virtual Válida",
|
||||
"view": "Ver",
|
||||
"configManaged": "Configuración Gestionada",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Regenerar las credenciales invalidará las anteriores y causará una desconexión. Asegúrese de actualizar cualquier configuración que use estas credenciales.",
|
||||
"confirm": "Confirmar",
|
||||
"regenerateCredentialsConfirmation": "¿Está seguro que desea regenerar las credenciales?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Punto final",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Clave secreta",
|
||||
"niceId": "ID bonita",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Se ha producido un error al actualizar el ID de Niza.",
|
||||
"niceIdCannotBeEmpty": "El ID de Niza no puede estar vacío",
|
||||
"enterIdentifier": "Introducir identificador",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Ruta del identificador",
|
||||
"deviceLoginUseDifferentAccount": "¿No tú? Utilice una cuenta diferente.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo está solicitando acceso a esta cuenta.",
|
||||
"loginSelectAuthenticationMethod": "Seleccione un método de autenticación para continuar.",
|
||||
"noData": "Sin datos",
|
||||
"machineClients": "Clientes de la máquina",
|
||||
"install": "Instalar",
|
||||
"downloadInstaller": "Error al descargar",
|
||||
"run": "Ejecutar",
|
||||
"envFile": "Archivo de Entorno",
|
||||
"serviceFile": "Archivo de Servicio",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Desconectado",
|
||||
"approvalsEmptyStateTitle": "Aprobaciones de dispositivo no habilitadas",
|
||||
"approvalsEmptyStateDescription": "Habilita las aprobaciones de dispositivos para que los roles requieran aprobación del administrador antes de que los usuarios puedan conectar nuevos dispositivos.",
|
||||
"approvalsEmptyStateHowToTitle": "Cómo habilitar",
|
||||
"approvalsEmptyStateStep1Title": "Ir a roles",
|
||||
"approvalsEmptyStateStep1Description": "Navega a la configuración de roles de tu organización para configurar las aprobaciones de dispositivos.",
|
||||
"approvalsEmptyStateStep2Title": "Habilitar aprobaciones de dispositivo",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Ver como Administrador",
|
||||
"resourceLauncherResourceDetailsDescription": "Información de conexión y estado de este recurso.",
|
||||
"resourceLauncherResourceDetails": "Detalles del recurso",
|
||||
"resourceLauncherSitesDescription": "El recurso es accesible a través de los siguientes sitios.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Ver Sitio como Administrador",
|
||||
"resourceLauncherFilterBySite": "Filtrar por Sitio",
|
||||
"resourceLauncherSshCommand": "Comando SSH",
|
||||
"resourceLauncherSshCommandDescription": "Usa el CLI de Pangolin para abrir una sesión SSH para este recurso.",
|
||||
"resourceLauncherAuthMethodsDescription": "Métodos de autenticación habilitados para este recurso.",
|
||||
"resourceLauncherPrivateClientRequired": "Conéctese con un cliente en su dispositivo para acceder a este recurso en privado.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Se requiere conexión del cliente",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Modo teclado Unicode",
|
||||
"sessionToolbarShow": "Mostrar barra de herramientas",
|
||||
"sessionToolbarHide": "Ocultar barra de herramientas",
|
||||
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio"
|
||||
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio",
|
||||
"check": "Comprobar"
|
||||
}
|
||||
|
||||
+51
-19
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Cibles sur ce site",
|
||||
"siteSetting": "Paramètres de {siteName}",
|
||||
"siteNewtTunnel": "Site Newt (Recommandé)",
|
||||
"pangolinSite": "Site Pangolin",
|
||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans n'importe quel réseau. Pas de configuration supplémentaire.",
|
||||
"siteWg": "WireGuard basique",
|
||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Supprimer la clé d'API",
|
||||
"apiKeysManage": "Gérer les clés d'API",
|
||||
"apiKeysDescription": "Les clés d'API sont utilisées pour s'authentifier avec l'API d'intégration",
|
||||
"orgsManage": "Gérer les organisations",
|
||||
"orgsDescription": "Voir et gérer toutes les organisations sur cette instance",
|
||||
"provisioningKeysTitle": "Clé de provisioning",
|
||||
"provisioningKeysManage": "Gérer les clés de provisioning",
|
||||
"provisioningKeysDescription": "Les clés de provisioning sont utilisées pour authentifier la fourniture automatique de sites pour votre organisation.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Nom (Optionnel)",
|
||||
"accessControls": "Contrôles d'accès",
|
||||
"userDescription2": "Gérer les paramètres de cet utilisateur",
|
||||
"userGeneralSettingsDescription": "Gérer les rôles et paramètres de cet utilisateur dans l'organisation",
|
||||
"accessRoleErrorAdd": "Échec de l'ajout de l'utilisateur au rôle",
|
||||
"accessRoleErrorAddDescription": "Une erreur s'est produite lors de l'ajout de l'utilisateur au rôle.",
|
||||
"userSaved": "Utilisateur enregistré",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Lorsque les règles sont désactivées, tout le trafic passe par l'authentification.",
|
||||
"policyAccessRulesFallthroughOn": "Lorsqu'aucune règle ne correspond, le trafic passe par l'authentification.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/administrateur/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Enregistrer les règles",
|
||||
"resourceErrorCreate": "Erreur lors de la création de la ressource",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Commande inconnue",
|
||||
"newtErrorFetchReleases": "Échec de la récupération des informations de version : {err}",
|
||||
"newtErrorFetchLatest": "Erreur lors de la récupération de la dernière version : {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Point de terminaison",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Secrète",
|
||||
"newtVersion": "Version",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Générer le code de réinitialisation du mot de passe",
|
||||
"passwordResetCodeGenerated": "Code de réinitialisation du mot de passe généré",
|
||||
"passwordResetCodeGeneratedDescription": "Partagez ce code avec l'utilisateur. Il peut l'utiliser pour réinitialiser son mot de passe.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "Réinitialiser l'URL",
|
||||
"passwordNew": "Nouveau mot de passe",
|
||||
"passwordNewConfirm": "Confirmer le nouveau mot de passe",
|
||||
"changePassword": "Changer le mot de passe",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Erreur lors de la déconnexion",
|
||||
"signingAs": "Connecté en tant que",
|
||||
"serverAdmin": "Admin Serveur",
|
||||
"promoteServerAdmin": "Promouvoir en tant qu'administrateur serveur",
|
||||
"promoteServerAdminTitle": "Promouvoir en tant qu'administrateur serveur",
|
||||
"promoteServerAdminQuestion": "Êtes-vous sûr de vouloir promouvoir {selectedUser} en tant qu'administrateur serveur ?",
|
||||
"promoteServerAdminMessage": "Les administrateurs serveurs ont les privilèges les plus élevés et peuvent gérer le serveur.",
|
||||
"promoteServerAdminWarning": "Cela peut être annulé à tout moment en rétrogradant l'utilisateur.",
|
||||
"promoteServerAdminConfirm": "Promouvoir en tant qu'administrateur serveur",
|
||||
"promoteServerAdminSuccess": "Utilisateur promu",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} est maintenant un administrateur serveur.",
|
||||
"promoteServerAdminError": "Échec de la promotion de l'utilisateur",
|
||||
"demoteServerAdmin": "Rétrograder d'administrateur serveur",
|
||||
"demoteServerAdminTitle": "Rétrograder d'administrateur serveur",
|
||||
"demoteServerAdminQuestion": "Êtes-vous sûr de vouloir rétrograder {selectedUser} de l'administrateur serveur ?",
|
||||
"demoteServerAdminMessage": "{selectedUser} perdra tous les privilèges d'administrateur serveur.",
|
||||
"demoteServerAdminWarning": "Cela peut être annulé à tout moment en promouvant l'utilisateur.",
|
||||
"demoteServerAdminConfirm": "Rétrograder d'administrateur serveur",
|
||||
"demoteServerAdminSuccess": "Utilisateur rétrogradé",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} n'est plus un administrateur serveur.",
|
||||
"demoteServerAdminError": "Échec de la rétrogradation de l'utilisateur",
|
||||
"managedSelfhosted": "Gestion autonome",
|
||||
"otpEnable": "Activer l'authentification à deux facteurs",
|
||||
"otpDisable": "Désactiver l'authentification à deux facteurs",
|
||||
@@ -1600,7 +1622,7 @@
|
||||
"commandPaletteSearching": "Recherche en cours...",
|
||||
"commandPaletteNavigation": "Navigation",
|
||||
"commandPaletteOrganizations": "Organisations",
|
||||
"commandPaletteSites": "Sites",
|
||||
"commandPaletteSites": "Nœuds",
|
||||
"commandPaletteResources": "Ressource",
|
||||
"commandPaletteUsers": "Utilisateurs",
|
||||
"commandPaletteClients": "Liste des clients",
|
||||
@@ -1992,7 +2014,7 @@
|
||||
"aiProviderModelsBudgetConfigured": "Budget configuré",
|
||||
"aiProviderModelsEditTitle": "Modifier le modèle",
|
||||
"aiProviderModelsEditDescription": "Mettez à jour la clé du modèle ou configurez son budget.",
|
||||
"aiProviderModelsBudgetTab": "Budget",
|
||||
"aiProviderModelsBudgetTab": "Configurer le budget de ce fournisseur pour restreindre l'utilisation en fonction des dépenses ou des limites de jetons",
|
||||
"aiProviderModelsKeyLabel": "Clé du modèle",
|
||||
"aiProviderModelsKeyRequired": "Entrez une clé de modèle",
|
||||
"aiProviderModelsKeyDuplicate": "Cette clé de modèle est déjà sur une liste",
|
||||
@@ -2092,9 +2114,10 @@
|
||||
"aiUsageUnnamedVirtualApiKey": "Clé sans nom",
|
||||
"aiUsageLoading": "Chargement...",
|
||||
"aiUsageNoData": "Pas de données",
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettings": "Configurer le budget de ce fournisseur pour restreindre l'utilisation en fonction des dépenses ou des limites de jetons",
|
||||
"resourceBudgetSettingsDescription": "Configurez comment ce portail AI limite l'utilisation en fonction des dépenses ou des limites de jetons",
|
||||
"sidebarApiKeys": "Clés API",
|
||||
"sidebarOrgs": "Organisations",
|
||||
"sidebarProvisioning": "Mise en place",
|
||||
"sidebarSettings": "Réglages",
|
||||
"sidebarAllUsers": "Tous les utilisateurs",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Faites fonctionner Olm sur votre système",
|
||||
"clientOlmCredentials": "Identifiants",
|
||||
"clientOlmCredentialsDescription": "C'est ainsi que le client s'authentifie avec le serveur",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Point de terminaison",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Secrète",
|
||||
"clientCredentialsSave": "Enregistrer les informations d'identification",
|
||||
@@ -3026,7 +3049,7 @@
|
||||
"description": "Entrez les identifiants du noeud existant que vous souhaitez adopter",
|
||||
"nodeIdLabel": "Nœud ID",
|
||||
"nodeIdDescription": "L'ID du noeud existant que vous voulez adopter",
|
||||
"secretLabel": "Secret",
|
||||
"secretLabel": "Clé secrète",
|
||||
"secretDescription": "La clé secrète du noeud existant",
|
||||
"submitButton": "Noeud d'Adopt"
|
||||
},
|
||||
@@ -3034,7 +3057,7 @@
|
||||
"title": "Informations d'identification générées",
|
||||
"description": "Utilisez ces identifiants générés pour configurer le noeud",
|
||||
"nodeIdTitle": "Nœud ID",
|
||||
"secretTitle": "Secret",
|
||||
"secretTitle": "Clé secrète",
|
||||
"saveCredentialsTitle": "Ajouter des identifiants à la config",
|
||||
"saveCredentialsDescription": "Ajoutez ces informations d'identification à votre fichier de configuration du nœud Pangolin auto-hébergé pour compléter la connexion.",
|
||||
"submitButton": "Créer un noeud"
|
||||
@@ -3163,7 +3186,7 @@
|
||||
"roleMappingRemoveRule": "Supprimer",
|
||||
"idpGoogleConfiguration": "Configuration Google",
|
||||
"idpGoogleConfigurationDescription": "Configurer les identifiants Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Votre identifiant client Google OAuth2",
|
||||
"idpGoogleClientSecretDescription": "Secret client OAuth2 de Google",
|
||||
"idpAzureConfiguration": "Configuration de l'entra ID Azure",
|
||||
"idpAzureConfigurationDescription": "Configurer les identifiants OAuth2 Azure Entra ID",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "ID client d'enregistrement de l'application Azure",
|
||||
"idpAzureClientSecretDescription2": "Secret du client d'enregistrement de l'application Azure",
|
||||
"idpGoogleDescription": "Fournisseur Google OAuth2/OIDC",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/Fournisseur OIDC",
|
||||
"subnet": "Sous-réseau",
|
||||
"utilitySubnet": "Routeur utilitaire",
|
||||
"subnetDescription": "Le sous-réseau de la configuration réseau de cette organisation.",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Priorité",
|
||||
"priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.",
|
||||
"instanceName": "Nom de l'instance",
|
||||
"clearInstanceName": "Réinitialiser l'association de serveur",
|
||||
"pathMatchModalTitle": "Configurer le chemin correspondant",
|
||||
"pathMatchModalDescription": "Définissez comment les requêtes entrantes doivent être trouvées en fonction de leur chemin.",
|
||||
"pathMatchType": "Type de correspondance",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Autorisé par la règle",
|
||||
"allowedNoAuth": "Aucune authentification autorisée",
|
||||
"validAccessToken": "Jeton d'accès valide",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validHeaderAuth": "Authentification d'en-tête valide",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validPassword": "Mot de passe valide",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Email valide",
|
||||
"validSSO": "SSO valide",
|
||||
"validVirtualAPIKey": "Clé API Virtuelle Valide",
|
||||
"view": "Afficher",
|
||||
"configManaged": "Configuration gérée",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Abandonné par la règle",
|
||||
"noSessions": "Aucune session",
|
||||
"temporaryRequestToken": "Jeton de requête temporaire",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Pas d'authentification valide",
|
||||
"ip": "IP",
|
||||
"reason": "Raison",
|
||||
"requestLogs": "Journal des Requêtes HTTP",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "La régénération des identifiants invalidera les identifiants précédents et provoquera une déconnexion. Assurez-vous de mettre à jour toutes les configurations qui utilisent ces identifiants.",
|
||||
"confirm": "Confirmer",
|
||||
"regenerateCredentialsConfirmation": "Voulez-vous vraiment régénérer les identifiants ?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Point de terminaison",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Clé privée",
|
||||
"niceId": "Joli ID",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Aucune donnée",
|
||||
"machineClients": "Clients Machines",
|
||||
"install": "Installer",
|
||||
"downloadInstaller": "Télécharger l'installateur",
|
||||
"run": "Exécuter",
|
||||
"envFile": "Fichier Environnement",
|
||||
"serviceFile": "Fichier de Service",
|
||||
@@ -3891,7 +3916,7 @@
|
||||
"deviceMessageArchive": "Le périphérique sera archivé et retiré de la liste des périphériques actifs.",
|
||||
"deviceArchiveConfirm": "Dispositif d'archivage",
|
||||
"archiveDevice": "Dispositif d'archivage",
|
||||
"archive": "Archive",
|
||||
"archive": "Archiver",
|
||||
"deviceUnarchived": "Appareil désarchivé",
|
||||
"deviceUnarchivedDescription": "L'appareil a été désarchivé avec succès.",
|
||||
"errorUnarchivingDevice": "Erreur lors de la désarchivage du périphérique",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Version du noyau",
|
||||
"deviceModel": "Modèle de l'appareil",
|
||||
"serialNumber": "Numéro de série",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Nom d'hôte",
|
||||
"firstSeen": "Première vue",
|
||||
"lastSeen": "Dernière vue",
|
||||
"biometricsEnabled": "biométrique activée",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Déconnecté",
|
||||
"approvalsEmptyStateTitle": "Approbations de l'appareil non activées",
|
||||
"approvalsEmptyStateDescription": "Activer les autorisations de l'appareil pour les rôles qui nécessitent l'approbation de l'administrateur avant que les utilisateurs puissent connecter de nouveaux appareils.",
|
||||
"approvalsEmptyStateHowToTitle": "Comment activer",
|
||||
"approvalsEmptyStateStep1Title": "Aller aux Rôles",
|
||||
"approvalsEmptyStateStep1Description": "Accédez aux paramètres de rôles de votre organisation pour configurer les autorisations de l'appareil.",
|
||||
"approvalsEmptyStateStep2Title": "Activer les autorisations de l'appareil",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Voir en tant qu'admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Informations de connexion et statut pour cette ressource.",
|
||||
"resourceLauncherResourceDetails": "Détails de la ressource",
|
||||
"resourceLauncherSitesDescription": "La ressource est accessible via les sites suivants.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Voir le site en tant qu'admin",
|
||||
"resourceLauncherFilterBySite": "Filtrer par site",
|
||||
"resourceLauncherSshCommand": "Commande SSH",
|
||||
"resourceLauncherSshCommandDescription": "Utilisez le CLI Pangolin pour ouvrir une session SSH vers cette ressource.",
|
||||
"resourceLauncherAuthMethodsDescription": "Méthodes d'authentification activées pour cette ressource.",
|
||||
"resourceLauncherPrivateClientRequired": "Connectez-vous avec un client sur votre appareil pour accéder à cette ressource en privé.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Connexion client requise",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Mode clavier Unicode",
|
||||
"sessionToolbarShow": "Afficher la barre d'outils",
|
||||
"sessionToolbarHide": "Masquer la barre d'outils",
|
||||
"actionUpdateSiteApprovals": "Mettre à jour les approbations de site"
|
||||
"actionUpdateSiteApprovals": "Mettre à jour les approbations de site",
|
||||
"check": "Vérifier"
|
||||
}
|
||||
|
||||
+83
-51
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Obiettivi su questo sito",
|
||||
"siteSetting": "Impostazioni del sito {siteName}",
|
||||
"siteNewtTunnel": "Nuovo Sito (Consigliato)",
|
||||
"pangolinSite": "Sito Pangolin",
|
||||
"siteNewtTunnelDescription": "Modo più semplice per creare un entrypoint in qualsiasi rete. Nessuna configurazione aggiuntiva.",
|
||||
"siteWg": "WireGuard Base",
|
||||
"siteWgDescription": "Usa un qualsiasi client WireGuard per stabilire un tunnel. Impostazione NAT manuale richiesta.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Elimina Chiave API",
|
||||
"apiKeysManage": "Gestisci Chiavi API",
|
||||
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
|
||||
"orgsManage": "Gestisci Organizzazioni",
|
||||
"orgsDescription": "Visualizza e gestisci tutte le organizzazioni su questa istanza",
|
||||
"provisioningKeysTitle": "Chiave di provisioning",
|
||||
"provisioningKeysManage": "Gestisci Chiavi di provisioning",
|
||||
"provisioningKeysDescription": "Le chiavi di provisioning vengono utilizzate per autenticare il provisioning automatico del sito per la tua organizzazione.",
|
||||
@@ -716,11 +719,12 @@
|
||||
"nameOptional": "Nome (Opzionale)",
|
||||
"accessControls": "Controlli di Accesso",
|
||||
"userDescription2": "Gestisci le impostazioni di questo utente",
|
||||
"userGeneralSettingsDescription": "Gestisci i ruoli e le impostazioni di questo utente nell'organizzazione",
|
||||
"accessRoleErrorAdd": "Impossibile aggiungere l'utente al ruolo",
|
||||
"accessRoleErrorAddDescription": "Si è verificato un errore durante l'aggiunta dell'utente al ruolo.",
|
||||
"userSaved": "Utente salvato",
|
||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||
"autoProvisioned": "Auto Provisioned",
|
||||
"autoProvisioned": "Provisioning Automatico",
|
||||
"autoProvisionSettings": "Impostazioni Automatiche di provisioning",
|
||||
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Nome Server TLS non valido. Usa il formato nome dominio o salva vuoto per rimuovere il Nome Server TLS.",
|
||||
"proxyEnableSSL": "Abilita TLS",
|
||||
"proxyEnableSSLDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alle risorse interne target.",
|
||||
"target": "Target",
|
||||
"target": "Destinazione",
|
||||
"configureTarget": "Configura Risorse Interne",
|
||||
"targetErrorFetch": "Impossibile recuperare i target",
|
||||
"targetErrorFetchDescription": "Si è verificato un errore durante il recupero dei target",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Quando le regole sono disabilitate, tutto il traffico passa all'autenticazione.",
|
||||
"policyAccessRulesFallthroughOn": "Quando nessuna regola corrisponde, il traffico passa all'autenticazione.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/amministratore/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Salva Regole",
|
||||
"resourceErrorCreate": "Errore nella creazione della risorsa",
|
||||
@@ -1247,7 +1251,7 @@
|
||||
"inviteAlready": "Sembra che sei stato invitato!",
|
||||
"inviteAlreadyDescription": "Per accettare l'invito, devi accedere o creare un account.",
|
||||
"signupQuestion": "Hai già un account?",
|
||||
"login": "Log In",
|
||||
"login": "Accedi",
|
||||
"resourceNotFound": "Risorsa Non Trovata",
|
||||
"resourceNotFoundDescription": "La risorsa che stai cercando di accedere non esiste.",
|
||||
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Genera Codice Di Ripristino Password",
|
||||
"passwordResetCodeGenerated": "Codice Di Reimpostazione Password Generato",
|
||||
"passwordResetCodeGeneratedDescription": "Condividi questo codice con l'utente. Possono usarlo per reimpostare la password.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "URL di Reimpostazione",
|
||||
"passwordNew": "Nuova Password",
|
||||
"passwordNewConfirm": "Conferma Nuova Password",
|
||||
"changePassword": "Cambia Password",
|
||||
@@ -1376,7 +1380,7 @@
|
||||
"pageNotFound": "Pagina Non Trovata",
|
||||
"pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.",
|
||||
"overview": "Panoramica",
|
||||
"home": "Home",
|
||||
"home": "Pagina Iniziale",
|
||||
"settings": "Impostazioni",
|
||||
"usersAll": "Tutti Gli Utenti",
|
||||
"license": "Licenza",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Errore durante il logout",
|
||||
"signingAs": "Accesso come",
|
||||
"serverAdmin": "Amministratore Server",
|
||||
"promoteServerAdmin": "Promuovi a amministratore del server",
|
||||
"promoteServerAdminTitle": "Promuovi a Amministratore del Server",
|
||||
"promoteServerAdminQuestion": "Sei sicuro di voler promuovere {selectedUser} a amministratore del server?",
|
||||
"promoteServerAdminMessage": "Gli amministratori del server hanno i massimi privilegi e possono gestire il server.",
|
||||
"promoteServerAdminWarning": "Questo può essere annullato in qualsiasi momento retrocedendo l'utente.",
|
||||
"promoteServerAdminConfirm": "Promuovi a Amministratore del Server",
|
||||
"promoteServerAdminSuccess": "Utente Promosso",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} è ora un amministratore del server.",
|
||||
"promoteServerAdminError": "Impossibile promuovere l'utente",
|
||||
"demoteServerAdmin": "Retrocedere da amministratore del server",
|
||||
"demoteServerAdminTitle": "Retrocedere da Amministratore del Server",
|
||||
"demoteServerAdminQuestion": "Sei sicuro di voler retrocedere {selectedUser} da amministratore del server?",
|
||||
"demoteServerAdminMessage": "{selectedUser} perderà tutti i privilegi di amministratore del server.",
|
||||
"demoteServerAdminWarning": "Questo può essere annullato in qualsiasi momento promuovendo l'utente.",
|
||||
"demoteServerAdminConfirm": "Retrocedere da amministratore del server",
|
||||
"demoteServerAdminSuccess": "Utente retrocesso",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} non è più un amministratore del server.",
|
||||
"demoteServerAdminError": "Impossibile retrocedere l'utente",
|
||||
"managedSelfhosted": "Gestito Auto-Ospitato",
|
||||
"otpEnable": "Abilita Autenticazione a Due Fattori",
|
||||
"otpDisable": "Disabilita Autenticazione a Due Fattori",
|
||||
@@ -1661,7 +1683,7 @@
|
||||
"orgErrorNoProvided": "Nessuna organizzazione fornita",
|
||||
"apiKeysErrorNoUpdate": "Nessuna chiave API da aggiornare",
|
||||
"sidebarOverview": "Panoramica",
|
||||
"sidebarHome": "Home",
|
||||
"sidebarHome": "Pagina Iniziale",
|
||||
"sidebarSites": "Siti",
|
||||
"sidebarApprovals": "Richieste Di Approvazione",
|
||||
"sidebarResources": "Risorse",
|
||||
@@ -1682,7 +1704,7 @@
|
||||
"commandAiProviders": "Provider AI",
|
||||
"sidebarVirtualApiKeys": "Chiavi API Virtuali",
|
||||
"sidebarMyApiKeys": "Le Tue Chiavi API",
|
||||
"sidebarAccount": "Launcher",
|
||||
"sidebarAccount": "Avvio",
|
||||
"commandVirtualApiKeys": "Chiavi API Virtuali",
|
||||
"virtualApiKeysTitle": "Gestisci Chiavi API Virtuali",
|
||||
"virtualApiKeysDescription": "Crea e gestisci chiavi API manuali per l'accesso ai gateway AI pubblici",
|
||||
@@ -2032,7 +2054,7 @@
|
||||
"aiUsageTabOverview": "Panoramica",
|
||||
"aiUsageTabProviders": "Provider",
|
||||
"aiUsageTabResources": "Risorse",
|
||||
"aiUsageFilterProvider": "Provider",
|
||||
"aiUsageFilterProvider": "Fornitore",
|
||||
"aiUsageFilterModel": "Modello",
|
||||
"aiUsageFilterResource": "Risorsa",
|
||||
"aiUsageFilterRole": "Ruolo",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettingsDescription": "Configura come questo gateway AI limita l'uso basato sulla spesa o sui limiti di token",
|
||||
"sidebarApiKeys": "Chiavi API",
|
||||
"sidebarOrgs": "Organizzazioni",
|
||||
"sidebarProvisioning": "Accantonamento",
|
||||
"sidebarSettings": "Impostazioni",
|
||||
"sidebarAllUsers": "Tutti Gli Utenti",
|
||||
@@ -2105,7 +2128,7 @@
|
||||
"sidebarMachineClients": "Macchine",
|
||||
"sidebarDomains": "Domini",
|
||||
"sidebarGeneral": "Gestisci",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarLogAndAnalytics": "Log & Analisi",
|
||||
"sidebarBluePrints": "Progetti",
|
||||
"sidebarAlerting": "Allerta",
|
||||
"sidebarHealthChecks": "Controlli di salute",
|
||||
@@ -2142,7 +2165,7 @@
|
||||
"commandLogsStreaming": "Streaming di Eventi",
|
||||
"commandManagement": "Gestione",
|
||||
"commandAlerting": "Avvisi",
|
||||
"commandProvisioning": "Provisioning",
|
||||
"commandProvisioning": "Accantonamento",
|
||||
"commandBluePrints": "Modelli",
|
||||
"commandApiKeys": "Chiavi API",
|
||||
"commandBillingAndLicenses": "Fatturazione & Licenze",
|
||||
@@ -2158,7 +2181,7 @@
|
||||
"alertingSearchRules": "Cerca regole…",
|
||||
"alertingAddRule": "Crea Regola",
|
||||
"alertingColumnSource": "Fonte",
|
||||
"alertingColumnTrigger": "Trigger",
|
||||
"alertingColumnTrigger": "Attivazione",
|
||||
"alertingColumnActions": "Azioni",
|
||||
"alertingColumnEnabled": "Abilitato",
|
||||
"alertingDeleteQuestion": "Si prega di confermare di voler eliminare questa regola di allerta.",
|
||||
@@ -2196,7 +2219,7 @@
|
||||
"alertingSelectResources": "Seleziona risorse…",
|
||||
"alertingResourcesSelected": "{count} risorse selezionate",
|
||||
"alertingResourcesEmpty": "Nessuna risorsa con target nei primi 10 risultati.",
|
||||
"alertingSectionTrigger": "Trigger",
|
||||
"alertingSectionTrigger": "Attivazione",
|
||||
"alertingTrigger": "Quando allertare",
|
||||
"alertingTriggerSiteOnline": "Sito online",
|
||||
"alertingTriggerSiteOffline": "Sito offline",
|
||||
@@ -2270,7 +2293,7 @@
|
||||
"alertingNodeNotConfigured": "Non ancora configurato",
|
||||
"alertingNodeActionsCount": "{count, plural, one {# azione} other {# azioni}}",
|
||||
"alertingNodeRoleSource": "Fonte",
|
||||
"alertingNodeRoleTrigger": "Trigger",
|
||||
"alertingNodeRoleTrigger": "Attivazione",
|
||||
"alertingNodeRoleAction": "Azione",
|
||||
"alertingTabRules": "Regole di Allerta",
|
||||
"alertingTabHealthChecks": "Controlli di Salute",
|
||||
@@ -2298,7 +2321,7 @@
|
||||
"standaloneHcSaved": "Controllo di salute salvato",
|
||||
"standaloneHcColumnHealth": "Salute",
|
||||
"standaloneHcColumnMode": "Modalità",
|
||||
"standaloneHcColumnTarget": "Target",
|
||||
"standaloneHcColumnTarget": "Obiettivo",
|
||||
"standaloneHcHealthStateHealthy": "Sano",
|
||||
"standaloneHcHealthStateUnhealthy": "Non Sano",
|
||||
"standaloneHcHealthStateUnknown": "Sconosciuto",
|
||||
@@ -2353,7 +2376,7 @@
|
||||
"containerImage": "Immagine",
|
||||
"containerState": "Stato",
|
||||
"containerNetworks": "Reti",
|
||||
"containerHostnameIp": "Hostname/IP",
|
||||
"containerHostnameIp": "Nome host/IP",
|
||||
"containerLabels": "Etichette",
|
||||
"containerLabelsCount": "{count, plural, one {# etichetta} other {# etichette}}",
|
||||
"containerLabelsTitle": "Etichette Del Contenitore",
|
||||
@@ -2424,7 +2447,7 @@
|
||||
"billing": "Fatturazione",
|
||||
"orgBillingDescription": "Gestisci le informazioni di fatturazione e gli abbonamenti",
|
||||
"github": "GitHub",
|
||||
"pangolinHosted": "Pangolin Hosted",
|
||||
"pangolinHosted": "Ospitato su Pangolin",
|
||||
"fossorial": "Fossoriale",
|
||||
"completeAccountSetup": "Completa la Configurazione dell'Account",
|
||||
"completeAccountSetupDescription": "Imposta la tua password per iniziare",
|
||||
@@ -2605,7 +2628,7 @@
|
||||
"multiSelectFilterCount": "{count} selezionato",
|
||||
"createDomainCnameRecords": "Record CNAME",
|
||||
"createDomainARecords": "Record A",
|
||||
"createDomainRecordNumber": "Record {number}",
|
||||
"createDomainRecordNumber": "Record {numero}",
|
||||
"createDomainTxtRecords": "Record TXT",
|
||||
"createDomainSaveTheseRecords": "Salva Questi Record",
|
||||
"createDomainSaveTheseRecordsDescription": "Assicurati di salvare questi record DNS poiché non li vedrai più.",
|
||||
@@ -2730,7 +2753,7 @@
|
||||
"requireDeviceApproval": "Richiede Approvazioni Dispositivo",
|
||||
"requireDeviceApprovalDescription": "Gli utenti con questo ruolo hanno bisogno di nuovi dispositivi approvati da un amministratore prima di poter connettersi e accedere alle risorse.",
|
||||
"sshSettings": "SSH",
|
||||
"inferenceSettings": "AI Gateway",
|
||||
"inferenceSettings": "Gateway AI",
|
||||
"sshAccess": "Accesso SSH",
|
||||
"rdpSettings": "RDP",
|
||||
"vncSettings": "VNC",
|
||||
@@ -2742,7 +2765,7 @@
|
||||
"vncServerDescription": "Configura la destinazione e la porta del server VNC",
|
||||
"sshServerMode": "Modalità",
|
||||
"sshServerModeStandard": "Server SSH Standard",
|
||||
"sshServerModePangolin": "Pangolin SSH",
|
||||
"sshServerModePangolin": "SSH di Pangolin",
|
||||
"sshServerModeStandardDescription": "Instrada comandi sulla rete a un server SSH come OpenSSH.",
|
||||
"sshServerModeNative": "Server SSH Nativo",
|
||||
"sshServerModeNativeDescription": "Esegue comandi direttamente sull'host tramite il connettore del sito. Non è richiesta la configurazione di rete.",
|
||||
@@ -2854,7 +2877,7 @@
|
||||
"resourcesTableClients": "Client",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
||||
"resourcesTableHealthy": "Sano",
|
||||
"resourcesTableDegraded": "Degraded",
|
||||
"resourcesTableDegraded": "Degradato",
|
||||
"resourcesTableUnhealthy": "Non Sano",
|
||||
"resourcesTableUnknown": "Sconosciuto",
|
||||
"resourcesTableNotMonitored": "Non monitorato",
|
||||
@@ -2886,7 +2909,7 @@
|
||||
"editInternalResourceDialogModeCidr": "CIDR",
|
||||
"editInternalResourceDialogModeHttp": "HTTP",
|
||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||
"editInternalResourceDialogModeInference": "Gateway AI",
|
||||
"editInternalResourceDialogModeSsh": "SSH",
|
||||
"editInternalResourceDialogScheme": "Metodo HTTP",
|
||||
"editInternalResourceDialogEnableSsl": "Abilita TLS",
|
||||
@@ -2946,7 +2969,7 @@
|
||||
"createInternalResourceDialogModeHttp": "HTTP",
|
||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||
"createInternalResourceDialogModeSsh": "SSH",
|
||||
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||
"createInternalResourceDialogModeInference": "Gateway AI",
|
||||
"scheme": "Metodo HTTP",
|
||||
"createInternalResourceDialogScheme": "Metodo HTTP",
|
||||
"createInternalResourceDialogEnableSsl": "Abilita TLS",
|
||||
@@ -3093,11 +3116,11 @@
|
||||
"regionAustraliaAndNewZealand": "Australia e Nuova Zelanda",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"regionPolynesia": "Polinesia",
|
||||
"managedSelfHosted": {
|
||||
"title": "Gestito Auto-Ospitato",
|
||||
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
|
||||
"introTitle": "Managed Self-Hosted Pangolin",
|
||||
"introTitle": "Gestito Auto-Ospitato",
|
||||
"introDescription": "è un'opzione di distribuzione progettata per le persone che vogliono la semplicità e l'affidabilità extra mantenendo i loro dati privati e self-hosted.",
|
||||
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin - i tunnel, la terminazione TLS e il traffico rimangono tutti sul tuo server. La differenza è che la gestione e il monitoraggio sono gestiti attraverso il nostro cruscotto cloud, che sblocca una serie di vantaggi:",
|
||||
"benefitSimplerOperations": {
|
||||
@@ -3143,7 +3166,7 @@
|
||||
"idpTypeLabel": "Tipo Provider Identità",
|
||||
"roleMappingExpressionPlaceholder": "es. contiene(gruppi, 'admin') && 'Admin' <unk> <unk> 'Membro'",
|
||||
"roleMappingModeFixedRoles": "Ruoli Fissi",
|
||||
"roleMappingModeMappingBuilder": "Mapping Builder",
|
||||
"roleMappingModeMappingBuilder": "Costruttore di Mappature",
|
||||
"roleMappingModeRawExpression": "Espressione Raw",
|
||||
"roleMappingFixedRolesPlaceholderSelect": "Seleziona uno o più ruoli",
|
||||
"roleMappingFixedRolesPlaceholderFreeform": "Digita nomi dei ruoli (corrispondenza esatta per organizzazione)",
|
||||
@@ -3163,8 +3186,8 @@
|
||||
"roleMappingRemoveRule": "Rimuovi",
|
||||
"idpGoogleConfiguration": "Configurazione Google",
|
||||
"idpGoogleConfigurationDescription": "Configura le credenziali di Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 Client Secret",
|
||||
"idpGoogleClientIdDescription": "Il Tuo Client Id Google OAuth2",
|
||||
"idpGoogleClientSecretDescription": "Il Tuo Client Google OAuth2 Secret",
|
||||
"idpAzureConfiguration": "Configurazione Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Configura le credenziali OAuth2 di Azure Entra ID",
|
||||
"idpTenantId": "ID Tenant",
|
||||
@@ -3181,8 +3204,8 @@
|
||||
"idpTenantIdLabel": "ID Tenant",
|
||||
"idpAzureClientIdDescription2": "Azure App Id Registrazione Client",
|
||||
"idpAzureClientSecretDescription2": "Azure App Registrazione Client Segreto",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpGoogleDescription": "Provider OAuth2/OIDC generico",
|
||||
"idpAzureDescription": "provider Microsoft Azure OAuth2/OIDC",
|
||||
"subnet": "Sottorete",
|
||||
"utilitySubnet": "Sottorete di utilità",
|
||||
"subnetDescription": "La sottorete per la configurazione di rete di questa organizzazione.",
|
||||
@@ -3255,7 +3278,7 @@
|
||||
"mustUpgradeToUse": "Devi aggiornare il tuo abbonamento per utilizzare questa funzionalità.",
|
||||
"subscriptionRequiredTierToUse": "Questa funzione richiede <tierLink>{tier}</tierLink> o superiore.",
|
||||
"upgradeToTierToUse": "Aggiorna ad <tierLink>{tier}</tierLink> o superiore per utilizzare questa funzionalità.",
|
||||
"subscriptionTierTier1": "Home",
|
||||
"subscriptionTierTier1": "Casa",
|
||||
"subscriptionTierTier2": "Squadra",
|
||||
"subscriptionTierTier3": "Business",
|
||||
"subscriptionTierEnterprise": "Impresa",
|
||||
@@ -3337,7 +3360,7 @@
|
||||
"resourceHeaderAuthSetupTitleDescription": "Imposta le credenziali di autenticazione di base (nome utente e password) per proteggere questa risorsa con Autenticazione intestazione HTTP. Accedi usando il formato https://username:password@resource.example.com",
|
||||
"resourceHeaderAuthSubmit": "Imposta Autenticazione Intestazione",
|
||||
"actionSetResourceHeaderAuth": "Imposta Autenticazione Intestazione",
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"enterpriseEdition": "Edizione Enterprise",
|
||||
"unlicensed": "Senza Licenza",
|
||||
"beta": "Beta",
|
||||
"manageUserDevices": "Dispositivi Utente",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Priorità",
|
||||
"priorityDescription": "I percorsi prioritari più alti sono valutati prima. Priorità = 100 significa ordinamento automatico (decidi di sistema). Usa un altro numero per applicare la priorità manuale.",
|
||||
"instanceName": "Nome Istanza",
|
||||
"clearInstanceName": "Reimposta Associazione Server",
|
||||
"pathMatchModalTitle": "Configura Corrispondenza Percorso",
|
||||
"pathMatchModalDescription": "Impostare come le richieste in arrivo devono essere abbinate in base al loro percorso.",
|
||||
"pathMatchType": "Tipo di Corrispondenza",
|
||||
@@ -3488,7 +3512,7 @@
|
||||
"clear": "Pulisci",
|
||||
"saveChanges": "Salva Modifiche",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchDefaultPlaceholder": "/percorso",
|
||||
"pathMatchPrefixHelp": "Esempio: /api corrisponde /api, /api/users etc.",
|
||||
"pathMatchExactHelp": "Esempio: /api corrisponde solo /api",
|
||||
"pathMatchRegexHelp": "Esempio: ^/api/.* corrisponde /api/anything",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Consentito dalla regola",
|
||||
"allowedNoAuth": "Non Consentito Auth",
|
||||
"validAccessToken": "Token Di Accesso Valido",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Autenticazione header valida",
|
||||
"validPincode": "Codice PIN valido",
|
||||
"validPassword": "Password Valida",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Email valida",
|
||||
"validSSO": "SSO valido",
|
||||
"validVirtualAPIKey": "Chiave API Virtuale Valida",
|
||||
"view": "Visualizza",
|
||||
"configManaged": "Gestione Configurazione",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Eliminato dalla regola",
|
||||
"noSessions": "Nessuna Sessione",
|
||||
"temporaryRequestToken": "Token Di Richiesta Temporaneo",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Nessuna Autenticazione Valida",
|
||||
"ip": "IP",
|
||||
"reason": "Motivo",
|
||||
"requestLogs": "Log Richieste HTTP",
|
||||
@@ -3569,7 +3593,7 @@
|
||||
"commandLogsAi": "Log delle Sessioni",
|
||||
"sidebarLogsAiUsage": "Analytics sull'Uso",
|
||||
"commandLogsAiUsage": "Analytics sull'Uso",
|
||||
"provider": "Provider",
|
||||
"provider": "Fornitore",
|
||||
"capability": "Capacità",
|
||||
"model": "Modello",
|
||||
"virtualApiKey": "Chiave API Virtuale",
|
||||
@@ -3645,7 +3669,7 @@
|
||||
"loadingDNSRecords": "Caricamento record DNS...",
|
||||
"olmUpdateAvailableInfo": "È disponibile una versione aggiornata di Olm. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
||||
"updateAvailableInfo": "È disponibile una versione aggiornata. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
||||
"client": "Client",
|
||||
"client": "Cliente",
|
||||
"proxyProtocol": "Impostazioni Protocollo Proxy",
|
||||
"proxyProtocolDescription": "Configurare il protocollo proxy per preservare gli indirizzi IP client per i servizi TCP.",
|
||||
"enableProxyProtocol": "Abilita Protocollo Proxy",
|
||||
@@ -3702,7 +3726,7 @@
|
||||
"deviceAuthorize": "Autorizza {applicationName}",
|
||||
"deviceConnected": "Dispositivo Connesso!",
|
||||
"deviceAuthorizedMessage": "Il dispositivo è autorizzato ad accedere al tuo account. Ritorna all'applicazione client.",
|
||||
"pangolinCloud": "Pangolin Cloud",
|
||||
"pangolinCloud": "Cloud di Pangolin",
|
||||
"viewDevices": "Visualizza Dispositivi",
|
||||
"viewDevicesDescription": "Gestisci i tuoi dispositivi connessi",
|
||||
"noDevices": "Nessun dispositivo trovato",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Si è verificato un errore durante l'aggiornamento del Nice ID.",
|
||||
"niceIdCannotBeEmpty": "Il Nice ID non può essere vuoto",
|
||||
"enterIdentifier": "Inserisci identificatore",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Identificatore",
|
||||
"deviceLoginUseDifferentAccount": "Non tu? Usa un account diverso.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Un dispositivo sta richiedendo l'accesso a questo account.",
|
||||
"loginSelectAuthenticationMethod": "Selezionare un metodo di autenticazione per continuare.",
|
||||
"noData": "Nessun Dato",
|
||||
"machineClients": "Machine Clients",
|
||||
"machineClients": "Client Macchina",
|
||||
"install": "Installa",
|
||||
"downloadInstaller": "Scarica Installatore",
|
||||
"run": "Esegui",
|
||||
"envFile": "File di ambiente",
|
||||
"serviceFile": "File di servizio",
|
||||
@@ -3930,7 +3955,7 @@
|
||||
"signupOrgTip": "Stai cercando di accedere tramite il provider di identità della tua organizzazione?",
|
||||
"signupOrgLink": "Accedi o registrati con la tua organizzazione",
|
||||
"verifyEmailLogInWithDifferentAccount": "Usa un account diverso",
|
||||
"logIn": "Log In",
|
||||
"logIn": "Accedi",
|
||||
"deviceInformation": "Informazioni Sul Dispositivo",
|
||||
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
|
||||
"deviceSecurity": "Sicurezza Del Dispositivo",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Versione Del Kernel",
|
||||
"deviceModel": "Modello Di Dispositivo",
|
||||
"serialNumber": "Numero D'Ordine",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Nome host",
|
||||
"firstSeen": "Prima Visto",
|
||||
"lastSeen": "Visto L'Ultima",
|
||||
"biometricsEnabled": "Biometria Abilitata",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Disconnesso",
|
||||
"approvalsEmptyStateTitle": "Approvazioni Dispositivo Non Abilitato",
|
||||
"approvalsEmptyStateDescription": "Abilita le approvazioni del dispositivo per i ruoli per richiedere l'approvazione dell'amministratore prima che gli utenti possano collegare nuovi dispositivi.",
|
||||
"approvalsEmptyStateHowToTitle": "Come Abilitare",
|
||||
"approvalsEmptyStateStep1Title": "Vai ai ruoli",
|
||||
"approvalsEmptyStateStep1Description": "Vai alle impostazioni dei ruoli della tua organizzazione per configurare le approvazioni del dispositivo.",
|
||||
"approvalsEmptyStateStep2Title": "Abilita Approvazioni Dispositivo",
|
||||
@@ -4061,7 +4087,7 @@
|
||||
"httpDestAuthBearerPlaceholder": "La tua chiave API o token",
|
||||
"httpDestAuthBasicTitle": "Autenticazione Base",
|
||||
"httpDestAuthBasicDescription": "Aggiunge un'intestazione Authorization: Basic '<credentials>'. Fornire le credenziali come username:password.",
|
||||
"httpDestAuthBasicPlaceholder": "username:password",
|
||||
"httpDestAuthBasicPlaceholder": "nomeutente:password",
|
||||
"httpDestAuthCustomTitle": "Intestazione Personalizzata",
|
||||
"httpDestAuthCustomDescription": "Specifica un nome e un valore di intestazione HTTP personalizzati per l'autenticazione (ad esempio X-API-Key).",
|
||||
"httpDestAuthCustomHeaderNamePlaceholder": "Nome intestazione (es. X-API-Key)",
|
||||
@@ -4079,7 +4105,7 @@
|
||||
"httpDestBodyTemplateHint": "Usa le variabili del modello per fare riferimento ai campi dell'evento nel tuo payload.",
|
||||
"httpDestPayloadFormatTitle": "Formato Payload",
|
||||
"httpDestPayloadFormatDescription": "Come gli eventi sono serializzati in ogni organismo di richiesta.",
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"httpDestFormatJsonArrayTitle": "Array JSON",
|
||||
"httpDestFormatJsonArrayDescription": "Una richiesta per lotto, corpo è un array JSON. Compatibile con la maggior parte dei webhooks generici e Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Una richiesta per lotto, corpo è newline-delimited JSON - un oggetto per linea, nessun array esterno. Richiesto da Splunk HEC, Elastic / OpenSearch, e Grafana Loki.",
|
||||
@@ -4114,7 +4140,7 @@
|
||||
"healthCheckTabConnection": "Connessione",
|
||||
"healthCheckTabAdvanced": "Avanzato",
|
||||
"healthCheckStrategyNotAvailable": "Questa strategia non è disponibile. Contatta le vendite per abilitare questa funzionalità.",
|
||||
"uptime30d": "Uptime (30d)",
|
||||
"uptime30d": "Uptime (30g)",
|
||||
"idpAddActionCreateNew": "Crea nuovo provider di identità",
|
||||
"idpAddActionImportFromOrg": "Importa da un'altra organizzazione",
|
||||
"idpImportDialogTitle": "Importa Provider di Identità",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Visualizza come Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Informazioni e stato della connessione per questa risorsa.",
|
||||
"resourceLauncherResourceDetails": "Dettagli Risorsa",
|
||||
"resourceLauncherSitesDescription": "La risorsa è accessibile tramite i seguenti siti.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Visualizza Sito come Admin",
|
||||
"resourceLauncherFilterBySite": "Filtra per Sito",
|
||||
"resourceLauncherSshCommand": "Comando SSH",
|
||||
"resourceLauncherSshCommandDescription": "Usa il CLI di Pangolin per aprire una sessione SSH a questa risorsa.",
|
||||
"resourceLauncherAuthMethodsDescription": "Metodi di autenticazione abilitati per questa risorsa.",
|
||||
"resourceLauncherPrivateClientRequired": "Connettiti con un client sul tuo dispositivo per accedere a questa risorsa privatamente.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Connessione Client Richiesta",
|
||||
@@ -4262,7 +4293,7 @@
|
||||
"resourceLauncherTcp": "TCP",
|
||||
"resourceLauncherUdp": "UDP",
|
||||
"resourceLauncherUnlabeled": "Non Etichettato",
|
||||
"resourceLauncherAiGateway": "AI Gateway",
|
||||
"resourceLauncherAiGateway": "Gateway AI",
|
||||
"resourceLauncherNoSite": "Nessun Sito",
|
||||
"resourceLauncherAvailableModels": "Modelli Disponibili",
|
||||
"resourceLauncherAvailableModelsDescription": "Modelli che puoi usare con questo gateway AI.",
|
||||
@@ -4330,7 +4361,7 @@
|
||||
"sshErrorSignKeyFailed": "Impossibile firmare la chiave SSH per l'autenticazione push PAM. Ti sei autenticato come utente?",
|
||||
"sshTerminalError": "Errore: {error}",
|
||||
"sshConnectionClosedCode": "Connessione chiusa (codice {code})",
|
||||
"sshPrivateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"sshPrivateKeyPlaceholder": "-----INIZIO CHIAVE PRIVATA OPENSSH-----",
|
||||
"sshPrivateKeyRequired": "È richiesta una chiave privata",
|
||||
"vncTitle": "VNC",
|
||||
"vncSignInDescription": "Inserisci le tue credenziali VNC per connetterti",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Modalità tastiera Unicode",
|
||||
"sessionToolbarShow": "Mostra barra degli strumenti",
|
||||
"sessionToolbarHide": "Nascondi barra degli strumenti",
|
||||
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito"
|
||||
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito",
|
||||
"check": "Controlla"
|
||||
}
|
||||
|
||||
+39
-7
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "이 사이트의 대상",
|
||||
"siteSetting": "{siteName} 설정",
|
||||
"siteNewtTunnel": "뉴트 사이트 (추천)",
|
||||
"pangolinSite": "판골린 사이트",
|
||||
"siteNewtTunnelDescription": "네트워크의 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
|
||||
"siteWg": "기본 WireGuard",
|
||||
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "API 키 삭제",
|
||||
"apiKeysManage": "API 키 관리",
|
||||
"apiKeysDescription": "API 키는 통합 API와 인증하는 데 사용됩니다.",
|
||||
"orgsManage": "조직 관리",
|
||||
"orgsDescription": "이 인스턴스에서 모든 조직을 보고 관리합니다",
|
||||
"provisioningKeysTitle": "프로비저닝 키",
|
||||
"provisioningKeysManage": "프로비저닝 키 관리",
|
||||
"provisioningKeysDescription": "프로비저닝 키는 조직의 자동 사이트 프로비저닝 인증에 사용됩니다.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "이름 (선택 사항)",
|
||||
"accessControls": "접근 제어",
|
||||
"userDescription2": "이 사용자의 설정 관리",
|
||||
"userGeneralSettingsDescription": "조직 내에서 이 사용자의 역할 및 설정을 관리하세요",
|
||||
"accessRoleErrorAdd": "사용자를 역할에 추가하는 데 실패했습니다.",
|
||||
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
||||
"userSaved": "사용자 저장됨",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "규칙이 비활성화되면, 모든 트래픽은 인증으로 넘어갑니다.",
|
||||
"policyAccessRulesFallthroughOn": "매칭되는 규칙이 없으면, 트래픽은 인증으로 넘어갑니다.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/관리자/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "규칙 저장",
|
||||
"resourceErrorCreate": "리소스 생성 오류",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "로그아웃 중 오류 발생",
|
||||
"signingAs": "로그인한 사용자",
|
||||
"serverAdmin": "서버 관리자",
|
||||
"promoteServerAdmin": "서버 관리자 승격",
|
||||
"promoteServerAdminTitle": "서버 관리자 승격",
|
||||
"promoteServerAdminQuestion": "{selectedUser}을 서버 관리자로 승격하시겠습니까?",
|
||||
"promoteServerAdminMessage": "서버 관리자는 최고 권한을 갖고 서버를 관리할 수 있습니다.",
|
||||
"promoteServerAdminWarning": "언제든지 사용자를 강등하여 이 작업을 되돌릴 수 있습니다.",
|
||||
"promoteServerAdminConfirm": "서버 관리자 승격",
|
||||
"promoteServerAdminSuccess": "사용자가 승격되었습니다",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser}님이 이제 서버 관리자가 되었습니다.",
|
||||
"promoteServerAdminError": "사용자 승격에 실패했습니다",
|
||||
"demoteServerAdmin": "서버 관리자에서 강등",
|
||||
"demoteServerAdminTitle": "서버 관리자에서 강등",
|
||||
"demoteServerAdminQuestion": "{selectedUser}을 서버 관리자에서 강등하시겠습니까?",
|
||||
"demoteServerAdminMessage": "{selectedUser}은 모든 서버 관리자 권한을 잃게 됩니다",
|
||||
"demoteServerAdminWarning": "언제든지 사용자를 승격하여 이 작업을 되돌릴 수 있습니다.",
|
||||
"demoteServerAdminConfirm": "서버 관리자에서 강등",
|
||||
"demoteServerAdminSuccess": "사용자가 강등되었습니다",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser}님이 더 이상 서버 관리자가 아닙니다.",
|
||||
"demoteServerAdminError": "사용자 강등에 실패했습니다",
|
||||
"managedSelfhosted": "관리 자체 호스팅",
|
||||
"otpEnable": "이중 인증 활성화",
|
||||
"otpDisable": "이중 인증 비활성화",
|
||||
@@ -1852,8 +1874,8 @@
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||
"aiProviderTypeCustom": "Custom",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI 게이트웨이",
|
||||
"aiProviderTypeCustom": "사용자 정의",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||
@@ -1861,7 +1883,7 @@
|
||||
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI 게이트웨이 API",
|
||||
"aiProviderTypeCustomDescription": "자체 엔드포인트를 가져오거나 사이트 타겟을 통해 라우트하십시오",
|
||||
"aiProviderUpstreamUrl": "상류 URL",
|
||||
"aiProviderUpstreamUrlDescription": "공급자 API의 기본 URL",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "예산",
|
||||
"resourceBudgetSettingsDescription": "이 AI 게이트웨이가 지출 또는 토큰 제한에 따라 사용을 제한하는 방법을 구성하십시오",
|
||||
"sidebarApiKeys": "API 키",
|
||||
"sidebarOrgs": "조직",
|
||||
"sidebarProvisioning": "프로비저닝",
|
||||
"sidebarSettings": "설정",
|
||||
"sidebarAllUsers": "모든 사용자",
|
||||
@@ -2742,7 +2765,7 @@
|
||||
"vncServerDescription": "VNC 서버의 목적지 및 포트를 구성합니다",
|
||||
"sshServerMode": "모드",
|
||||
"sshServerModeStandard": "표준 SSH 서버",
|
||||
"sshServerModePangolin": "Pangolin SSH",
|
||||
"sshServerModePangolin": "판골린 SSH",
|
||||
"sshServerModeStandardDescription": "네트워크를 통해 OpenSSH와 같은 SSH 서버로 명령을 전달합니다.",
|
||||
"sshServerModeNative": "네이티브 SSH 서버",
|
||||
"sshServerModeNativeDescription": "사이트 커넥터를 통해 호스트에서 직접 명령을 실행합니다. 네트워크 구성이 필요 없습니다.",
|
||||
@@ -3348,7 +3371,7 @@
|
||||
"manageMachineClientsDescription": "서버와 시스템이 리소스에 개인적으로 연결하는 데 사용하는 클라이언트를 생성하고 관리하십시오",
|
||||
"machineClientsBannerTitle": "서버 및 자동 시스템",
|
||||
"machineClientsBannerDescription": "머신 클라이언트는 특정 사용자와 연결되지 않은 서버 및 자동화된 시스템을 위한 것입니다. 이들은 ID와 비밀을 통해 인증하며, Pangolin CLI, Olm CLI, 또는 Olm 컨테이너로 실행될 수 있습니다.",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerPangolinCLI": "판골린 CLI",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Olm 컨테이너",
|
||||
"clientsTableUserClients": "사용자",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "우선순위",
|
||||
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
|
||||
"instanceName": "인스턴스 이름",
|
||||
"clearInstanceName": "서버 연결 해제",
|
||||
"pathMatchModalTitle": "경로 매칭 설정",
|
||||
"pathMatchModalDescription": "경로별로 들어오는 요청을 어떻게 매칭할지 설정합니다.",
|
||||
"pathMatchType": "일치 유형",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "데이터 없음",
|
||||
"machineClients": "기계 클라이언트",
|
||||
"install": "설치",
|
||||
"downloadInstaller": "설치 프로그램 다운로드",
|
||||
"run": "실행",
|
||||
"envFile": "환경 파일",
|
||||
"serviceFile": "서비스 파일",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "연결 해제됨",
|
||||
"approvalsEmptyStateTitle": "장치 승인 비활성화됨",
|
||||
"approvalsEmptyStateDescription": "사용자가 새 장치를 연결하기 전에 관리자의 승인을 필요로 하도록 역할에 대해 장치 승인을 활성화하세요.",
|
||||
"approvalsEmptyStateHowToTitle": "활성화 방법",
|
||||
"approvalsEmptyStateStep1Title": "역할로 이동",
|
||||
"approvalsEmptyStateStep1Description": "조직의 역할 설정으로 이동하여 장치 승인을 구성하십시오.",
|
||||
"approvalsEmptyStateStep2Title": "장치 승인 활성화",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "관리자로 보기",
|
||||
"resourceLauncherResourceDetailsDescription": "이 리소스에 대한 연결 정보 및 상태입니다.",
|
||||
"resourceLauncherResourceDetails": "리소스 세부 정보",
|
||||
"resourceLauncherSitesDescription": "리소스는 다음 사이트를 통해 액세스할 수 있습니다.",
|
||||
"resourceLauncherViewSiteAsAdmin": "관리자로 사이트 보기",
|
||||
"resourceLauncherFilterBySite": "사이트별 필터",
|
||||
"resourceLauncherSshCommand": "SSH 명령",
|
||||
"resourceLauncherSshCommandDescription": "Pangolin CLI를 사용하여 이 리소스에 대한 SSH 세션을 엽니다.",
|
||||
"resourceLauncherAuthMethodsDescription": "이 리소스에 활성화된 인증 방법입니다.",
|
||||
"resourceLauncherPrivateClientRequired": "이 리소스에 개인적으로 접근하려면 기기에서 클라이언트로 연결하십시오.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "클라이언트 연결 필요",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
|
||||
"sessionToolbarShow": "툴바 보기",
|
||||
"sessionToolbarHide": "툴바 숨기기",
|
||||
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
|
||||
"actionUpdateSiteApprovals": "사이트 승인 업데이트",
|
||||
"check": "확인"
|
||||
}
|
||||
|
||||
+50
-18
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Mål på dette nettstedet",
|
||||
"siteSetting": "{siteName} Innstillinger",
|
||||
"siteNewtTunnel": "Nyhetsnettsted (anbefalt)",
|
||||
"pangolinSite": "Pangolin Site",
|
||||
"siteNewtTunnelDescription": "Lekkeste måte å lage et inngangspunkt til ethvert nettverk. Ingen ekstra oppsett på.",
|
||||
"siteWg": "Grunnleggende WireGuard",
|
||||
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
|
||||
@@ -184,7 +185,7 @@
|
||||
"accessToken": "Tilgangsnøkkel",
|
||||
"usageExamples": "Brukseksempler",
|
||||
"tokenId": "Token-ID",
|
||||
"requestHeades": "Request Headers",
|
||||
"requestHeades": "Forespørselshoder",
|
||||
"queryParameter": "Forespørsel Params",
|
||||
"importantNote": "Viktig merknad",
|
||||
"shareImportantDescription": "Av sikkerhetsgrunner anbefales det å bruke headere fremfor query parametere der det er mulig, da query parametere kan logges i serverlogger eller nettleserhistorikk.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Slett API-nøkkel",
|
||||
"apiKeysManage": "Administrer API-nøkler",
|
||||
"apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API",
|
||||
"orgsManage": "Administrer organisasjoner",
|
||||
"orgsDescription": "Vis og administrer alle organisasjoner på denne instansen",
|
||||
"provisioningKeysTitle": "Foreløpig nøkkel",
|
||||
"provisioningKeysManage": "Behandle bestemmende nøkler",
|
||||
"provisioningKeysDescription": "Bestemmelsesnøkler brukes til å godkjenne automatisert nettstedsløsning for din organisasjon.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Navn (valgfritt)",
|
||||
"accessControls": "Tilgangskontroller",
|
||||
"userDescription2": "Administrer innstillingene for denne brukeren",
|
||||
"userGeneralSettingsDescription": "Administrer denne brukerens roller og innstillinger i organisasjonen",
|
||||
"accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle",
|
||||
"accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.",
|
||||
"userSaved": "Bruker lagret",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
|
||||
"proxyEnableSSL": "Aktiver TLS",
|
||||
"proxyEnableSSLDescription": "Aktivere SSL/TLS-kryptering for sikker HTTPS tilkobling til målene.",
|
||||
"target": "Target",
|
||||
"target": "Mål",
|
||||
"configureTarget": "Konfigurer mål",
|
||||
"targetErrorFetch": "Kunne ikke hente mål",
|
||||
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Når regler er deaktivert, går all trafikk gjennom til autentisering.",
|
||||
"policyAccessRulesFallthroughOn": "Når ingen regler samsvarer, fortsetter trafikken til autentisering.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/administrator/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Lagre Regler",
|
||||
"resourceErrorCreate": "Feil under oppretting av ressurs",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Ukjent kommando",
|
||||
"newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}",
|
||||
"newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Endepunkt",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Sikkerhetsnøkkel",
|
||||
"newtVersion": "Versjon",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Lag tilbakestillingskode for passord",
|
||||
"passwordResetCodeGenerated": "Passord tilbakestillingskoden er generert",
|
||||
"passwordResetCodeGeneratedDescription": "Del denne koden med brukeren. De kan bruke den til å tilbakestille passordet.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "Tilbakestill URL",
|
||||
"passwordNew": "Nytt passord",
|
||||
"passwordNewConfirm": "Bekreft nytt passord",
|
||||
"changePassword": "Endre passord",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Feil ved utlogging",
|
||||
"signingAs": "Logget inn som",
|
||||
"serverAdmin": "Serveradministrator",
|
||||
"promoteServerAdmin": "Forfrem til Server Admin",
|
||||
"promoteServerAdminTitle": "Forfrem til Server Admin",
|
||||
"promoteServerAdminQuestion": "Er du sikker på at du vil forfremme {selectedUser} til server admin?",
|
||||
"promoteServerAdminMessage": "Server administratorer har de høyeste rettighetene og kan administrere serveren.",
|
||||
"promoteServerAdminWarning": "Dette kan angres når som helst ved å degradere brukeren.",
|
||||
"promoteServerAdminConfirm": "Forfrem til Server Admin",
|
||||
"promoteServerAdminSuccess": "Bruker forfremmet",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} er nå server admin.",
|
||||
"promoteServerAdminError": "Kunne ikke forfremme bruker",
|
||||
"demoteServerAdmin": "Degradér fra server admin",
|
||||
"demoteServerAdminTitle": "Degradér fra Server Admin",
|
||||
"demoteServerAdminQuestion": "Er du sikker på at du vil degradere {selectedUser} fra server admin?",
|
||||
"demoteServerAdminMessage": "{selectedUser} vil miste alle rettigheter som server admin.",
|
||||
"demoteServerAdminWarning": "Dette kan angres når som helst ved å forfremme brukeren.",
|
||||
"demoteServerAdminConfirm": "Degradér fra server admin",
|
||||
"demoteServerAdminSuccess": "Bruker degradert",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} er ikke lenger server admin.",
|
||||
"demoteServerAdminError": "Kunne ikke degradere bruker",
|
||||
"managedSelfhosted": "Administrert selv-hostet",
|
||||
"otpEnable": "Aktiver tofaktor",
|
||||
"otpDisable": "Deaktiver tofaktor",
|
||||
@@ -1677,7 +1699,7 @@
|
||||
"sidebarInvitations": "Invitasjoner",
|
||||
"sidebarRoles": "Roller",
|
||||
"sidebarShareableLinks": "Delbare lenker",
|
||||
"sidebarAiGateway": "AI Gateway",
|
||||
"sidebarAiGateway": "AI Portal",
|
||||
"sidebarAiProviders": "Leverandører",
|
||||
"commandAiProviders": "AI-leverandører",
|
||||
"sidebarVirtualApiKeys": "Virtuelle API-nøkler",
|
||||
@@ -1837,7 +1859,7 @@
|
||||
"aiBudgetPeriodYearly": "Årlig",
|
||||
"aiBudgetPeriodLifetime": "Livstid",
|
||||
"aiBudgetUnitUsd": "USD",
|
||||
"aiBudgetUnitTokens": "Tokens",
|
||||
"aiBudgetUnitTokens": "Token",
|
||||
"aiBudgetConflictError": "Et budsjett for denne tilbakestillingsperioden og forbrukstype eksisterer allerede",
|
||||
"aiBudgetInvalidAmountError": "Angi et maksimalt forbruk større enn 0",
|
||||
"aiBudgetUpdated": "Budsjetter oppdatert",
|
||||
@@ -1852,7 +1874,7 @@
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||
"aiProviderTypeVercelAiGateway": "Offentlige AI-portene",
|
||||
"aiProviderTypeCustom": "Tilpasset",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
@@ -1905,7 +1927,7 @@
|
||||
"aiProviderBudgetAmount": "Budsjettbeløp",
|
||||
"aiProviderBudgetUnit": "Budsjettenhet",
|
||||
"aiProviderBudgetUnitUsd": "USD",
|
||||
"aiProviderBudgetUnitTokens": "Tokens",
|
||||
"aiProviderBudgetUnitTokens": "Token",
|
||||
"aiProviderEnabled": "Aktivert",
|
||||
"aiProviderEnabledDescription": "Deaktiver denne leverandøren fullstendig på tvers av alle ressurser",
|
||||
"aiProviderErrorCreate": "Kunne ikke opprette AI-leverandør",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Budsjett",
|
||||
"resourceBudgetSettingsDescription": "Konfigurer hvordan denne AI-portalen begrenser bruk basert på utgifter eller token-grenser",
|
||||
"sidebarApiKeys": "API-nøkler",
|
||||
"sidebarOrgs": "Organisasjoner",
|
||||
"sidebarProvisioning": "Levering",
|
||||
"sidebarSettings": "Innstillinger",
|
||||
"sidebarAllUsers": "Alle brukere",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt",
|
||||
"clientOlmCredentials": "Legitimasjon",
|
||||
"clientOlmCredentialsDescription": "Dette er hvordan klienten vil godkjenne med serveren",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Endepunkt",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Sikkerhetsnøkkel",
|
||||
"clientCredentialsSave": "Lagre brukeropplysninger",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.",
|
||||
"resourcesTableNoInternalResourcesFound": "Ingen private ressurser funnet.",
|
||||
"resourcesTableDestination": "Destinasjon",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Navn",
|
||||
"resourcesTableAliasAddress": "Alias adresse",
|
||||
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
|
||||
"resourcesTableClients": "Klienter",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Navn",
|
||||
"editInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et subnett konfigurert for å opprette private ressurser.",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Destinasjon",
|
||||
"createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Navn",
|
||||
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
|
||||
"internalResourceAliasLocalWarning": "Alias som slutter på .local kan forårsake oppløsningsproblemer på grunn av mDNS på enkelte nettverk.",
|
||||
"internalResourceDownstreamSchemeRequired": "Skjema er påkrevd for HTTP-ressurser",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Fjern",
|
||||
"idpGoogleConfiguration": "Google Konfigurasjon",
|
||||
"idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Din Google OAuth2-klient-ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2-klienten hemmelighet",
|
||||
"idpAzureConfiguration": "Azure Entra ID konfigurasjon",
|
||||
"idpAzureConfigurationDescription": "Konfigurer Azure Entra ID OAuth2 legitimasjon",
|
||||
"idpTenantId": "Leietaker-ID",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "Leietaker-ID",
|
||||
"idpAzureTenantIdDescription": "Azure leant ID (funnet i Azure Active Directory-oversikten)",
|
||||
"idpAzureClientIdDescription": "Azure App registrerings klient-ID",
|
||||
"idpAzureClientSecretDescription": "Azure App Registrering Klient Hemmelig",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Prioritet",
|
||||
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
|
||||
"instanceName": "Forekomst navn",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Konfigurere matching av sti",
|
||||
"pathMatchModalDescription": "Sett opp hvordan innkommende forespørsler skal matches basert på deres bane.",
|
||||
"pathMatchType": "Trefftype",
|
||||
@@ -3534,7 +3558,7 @@
|
||||
"allowedNoAuth": "Tillatt Ingen Auth",
|
||||
"validAccessToken": "Gyldig tilgangsnøkkel",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validPincode": "Gyldig PIN-kode",
|
||||
"validPassword": "Gyldig passord",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige og forårsake en frakobling. Sørg for å oppdatere alle konfigurasjoner som bruker disse legitimasjonene.",
|
||||
"confirm": "Bekreft",
|
||||
"regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimasjonene?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Endepunkt",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Hemmelig nøkkel",
|
||||
"niceId": "God ID",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Ingen data",
|
||||
"machineClients": "Maskinklienter",
|
||||
"install": "Installer",
|
||||
"downloadInstaller": "Download Installer",
|
||||
"run": "Kjør",
|
||||
"envFile": "Miljøfil",
|
||||
"serviceFile": "Tjenestefil",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Frakoblet",
|
||||
"approvalsEmptyStateTitle": "Enhetsgodkjenninger er ikke aktivert",
|
||||
"approvalsEmptyStateDescription": "Aktivere godkjenninger av enheter for at roller må godkjennes av admin før brukere kan koble til nye enheter.",
|
||||
"approvalsEmptyStateHowToTitle": "How to Enable",
|
||||
"approvalsEmptyStateStep1Title": "Gå til roller",
|
||||
"approvalsEmptyStateStep1Description": "Naviger til organisasjonens roller innstillinger for å konfigurere enhetsgodkjenninger.",
|
||||
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Vis som administrator",
|
||||
"resourceLauncherResourceDetailsDescription": "Tilkoblingsinformasjon og status for denne ressursen.",
|
||||
"resourceLauncherResourceDetails": "Ressursdetaljer",
|
||||
"resourceLauncherSitesDescription": "Ressursen er tilgjengelig via de følgende nettstedene.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Vis nettsted som administrator",
|
||||
"resourceLauncherFilterBySite": "Filtrer etter nettsted",
|
||||
"resourceLauncherSshCommand": "SSH-kommando",
|
||||
"resourceLauncherSshCommandDescription": "Bruk Pangolin CLI for å åpne en SSH-sesjon til denne ressursen.",
|
||||
"resourceLauncherAuthMethodsDescription": "Godkjenningsmetoder aktivert for denne ressursen.",
|
||||
"resourceLauncherPrivateClientRequired": "Koble til med en klient på enheten din for å få privat tilgang til denne ressursen.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Klienttilkobling kreves",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
|
||||
"sessionToolbarShow": "Vis verktøylinje",
|
||||
"sessionToolbarHide": "Skjul verktøylinje",
|
||||
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger"
|
||||
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger",
|
||||
"check": "Sjekk"
|
||||
}
|
||||
|
||||
+79
-47
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Doelen op deze site",
|
||||
"siteSetting": "{siteName} instellingen",
|
||||
"siteNewtTunnel": "Nieuwste site (Aanbevolen)",
|
||||
"pangolinSite": "Pangolin-site",
|
||||
"siteNewtTunnelDescription": "Makkelijkste manier om een ingangspunt in een netwerk te maken. Geen extra opzet.",
|
||||
"siteWg": "Basis WireGuard",
|
||||
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
||||
@@ -183,7 +184,7 @@
|
||||
"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",
|
||||
"tokenId": "Token-ID",
|
||||
"requestHeades": "Aanvraag van headers",
|
||||
"queryParameter": "Queryparameter",
|
||||
"importantNote": "Belangrijke opmerking",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "API sleutel verwijderen",
|
||||
"apiKeysManage": "API sleutels beheren",
|
||||
"apiKeysDescription": "API sleutels worden gebruikt om toegang te verifiëren met de integratie API ",
|
||||
"orgsManage": "Organisaties Beheren",
|
||||
"orgsDescription": "Bekijk en beheer alle organisaties op dit systeem",
|
||||
"provisioningKeysTitle": "Vertrekkende sleutel",
|
||||
"provisioningKeysManage": "Beheren van Provisioning Sleutels",
|
||||
"provisioningKeysDescription": "Provisionerende sleutels worden gebruikt om geautomatiseerde sitebepaling voor uw organisatie te verifiëren.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Naam (optioneel)",
|
||||
"accessControls": "Toegang Bediening",
|
||||
"userDescription2": "Beheer de instellingen van deze gebruiker",
|
||||
"userGeneralSettingsDescription": "Beheer de rollen en instellingen van deze gebruiker in de organisatie",
|
||||
"accessRoleErrorAdd": "Gebruiker aan rol toevoegen mislukt",
|
||||
"accessRoleErrorAddDescription": "Er is een fout opgetreden tijdens het toevoegen van de rol.",
|
||||
"userSaved": "Gebruiker opgeslagen",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Ongeldige TLS servernaam. Gebruik de domeinnaam of sla leeg op om de TLS servernaam te verwijderen.",
|
||||
"proxyEnableSSL": "Schakel TLS in",
|
||||
"proxyEnableSSLDescription": "SSL/TLS-versleuteling inschakelen voor beveiligde HTTPS-verbindingen naar de doelen.",
|
||||
"target": "Target",
|
||||
"target": "Doelwit",
|
||||
"configureTarget": "Doelstellingen configureren",
|
||||
"targetErrorFetch": "Ophalen van doelen mislukt",
|
||||
"targetErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de objecten",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Wanneer regels zijn uitgeschakeld, passeert al het verkeer naar authenticatie.",
|
||||
"policyAccessRulesFallthroughOn": "Wanneer geen regel overeenkomt, passeert het verkeer naar authenticatie.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/beheerder/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Regels opslaan",
|
||||
"resourceErrorCreate": "Fout bij maken document",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Onbekende opdracht",
|
||||
"newtErrorFetchReleases": "Kan release-informatie niet ophalen: {err}",
|
||||
"newtErrorFetchLatest": "Fout bij ophalen van laatste release: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Eindpunt",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Geheim",
|
||||
"newtVersion": "Versie",
|
||||
@@ -1161,7 +1165,7 @@
|
||||
"idpOidcConfigureDescription": "Configureer de eindpunten van de OAuth2/OIDC provider en referenties",
|
||||
"idpClientId": "Client ID",
|
||||
"idpClientIdDescription": "De OAuth2-client-ID van de identiteitsaanbieder",
|
||||
"idpClientSecret": "Client Secret",
|
||||
"idpClientSecret": "Clientgeheim",
|
||||
"idpClientSecretDescription": "Het OAuth2-clientgeheim van de identiteitsprovider",
|
||||
"idpAuthUrl": "URL autorisatie",
|
||||
"idpAuthUrlDescription": "De URL voor autorisatie OAuth2",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Herstelcode voor wachtwoord genereren",
|
||||
"passwordResetCodeGenerated": "Wachtwoord reset code gegenereerd",
|
||||
"passwordResetCodeGeneratedDescription": "Deel deze code met de gebruiker. Ze kunnen deze gebruiken om hun wachtwoord te resetten.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "Herstel-URL",
|
||||
"passwordNew": "Nieuw wachtwoord",
|
||||
"passwordNewConfirm": "Bevestig nieuw wachtwoord",
|
||||
"changePassword": "Wachtwoord wijzigen",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Fout bij uitloggen",
|
||||
"signingAs": "Ingelogd als",
|
||||
"serverAdmin": "Server beheer",
|
||||
"promoteServerAdmin": "Promoveer naar Server Admin",
|
||||
"promoteServerAdminTitle": "Promoveer naar Server Admin",
|
||||
"promoteServerAdminQuestion": "Weet u zeker dat u {selectedUser} naar server admin wilt promoveren?",
|
||||
"promoteServerAdminMessage": "Server admins hebben de hoogste privileges en kunnen de server beheren.",
|
||||
"promoteServerAdminWarning": "Dit kan op elk moment worden hersteld door de gebruiker te degraderen.",
|
||||
"promoteServerAdminConfirm": "Promoveer naar Server Admin",
|
||||
"promoteServerAdminSuccess": "Gebruiker Geprobeerd",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} is nu een server admin.",
|
||||
"promoteServerAdminError": "Het promoten van gebruiker is mislukt",
|
||||
"demoteServerAdmin": "Degradeer van Server admin",
|
||||
"demoteServerAdminTitle": "Degradeer van Server Admin",
|
||||
"demoteServerAdminQuestion": "Weet u zeker dat u {selectedUser} van server admin wilt degraderen?",
|
||||
"demoteServerAdminMessage": "{selectedUser} verliest alle server admin privileges.",
|
||||
"demoteServerAdminWarning": "Dit kan op elk moment worden hersteld door de gebruiker te promoveren.",
|
||||
"demoteServerAdminConfirm": "Degradeer van server admin",
|
||||
"demoteServerAdminSuccess": "Gebruiker Gedegradeerd",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} is niet langer een server admin.",
|
||||
"demoteServerAdminError": "Degradatie van gebruiker mislukt",
|
||||
"managedSelfhosted": "Beheerde Self-Hosted",
|
||||
"otpEnable": "Twee-factor inschakelen",
|
||||
"otpDisable": "Tweestapsverificatie uitschakelen",
|
||||
@@ -1670,7 +1692,7 @@
|
||||
"sidebarPolicies": "Gedeeld Beleid",
|
||||
"sidebarResourcePolicies": "Openbare Bronnen",
|
||||
"sidebarAccessControl": "Toegangs controle",
|
||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||
"sidebarLogsAndAnalytics": "Logboeken & Analytics",
|
||||
"sidebarTeam": "Team",
|
||||
"sidebarUsers": "Gebruikers",
|
||||
"sidebarAdmin": "Beheerder",
|
||||
@@ -1852,7 +1874,7 @@
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI-poort",
|
||||
"aiProviderTypeCustom": "Aangepast",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
@@ -1871,7 +1893,7 @@
|
||||
"aiProviderApiKeyDescription": "API-sleutel gebruikt om verzoeken naar deze provider te authenticeren",
|
||||
"aiProviderCustomHeadersDescription": "Headers verzonden bij elk verzoek naar deze provider. Nieuwe regel gescheiden: Header-Naam: waarde",
|
||||
"aiProviderApiKeyLastChars": "API-sleutel",
|
||||
"aiProviderAuthType": "Auth Type",
|
||||
"aiProviderAuthType": "Authenticatietype",
|
||||
"aiProviderAuthTypeSearch": "Zoek auth types...",
|
||||
"aiProviderAuthTypeNotFound": "Geen auth type gevonden",
|
||||
"aiProviderAuthTypeBearer": "Bearer",
|
||||
@@ -1885,7 +1907,7 @@
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autorisatie: Bearer-sleutel. Gebruikt door Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeNone": "Geen Auth",
|
||||
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||
"aiProviderAuthTypePassthrough": "Transparant",
|
||||
"aiProviderAuthTypeDescription": "Hoe de upstream API verzoeken authenticeert",
|
||||
"aiProviderAuthTypePassthroughDescription": "Stuur de API Keys headers van de beller door naar de upstream",
|
||||
"aiProviderAuthTypeNoneDescription": "Stuur geen authenticatieheaders naar de upstream",
|
||||
@@ -1992,7 +2014,7 @@
|
||||
"aiProviderModelsBudgetConfigured": "Budget geconfigureerd",
|
||||
"aiProviderModelsEditTitle": "Model bewerken",
|
||||
"aiProviderModelsEditDescription": "Werk de modelsleutel of budgetconfiguratie bij.",
|
||||
"aiProviderModelsBudgetTab": "Budget",
|
||||
"aiProviderModelsBudgetTab": "AI-budget",
|
||||
"aiProviderModelsKeyLabel": "Model sleutel",
|
||||
"aiProviderModelsKeyRequired": "Voer een modelsleutel in",
|
||||
"aiProviderModelsKeyDuplicate": "Deze modelsleutel staat al op een lijst",
|
||||
@@ -2092,9 +2114,10 @@
|
||||
"aiUsageUnnamedVirtualApiKey": "Naamloze sleutel",
|
||||
"aiUsageLoading": "Laden...",
|
||||
"aiUsageNoData": "Geen data",
|
||||
"resourceBudgetSettings": "Budget",
|
||||
"resourceBudgetSettings": "AI-budget",
|
||||
"resourceBudgetSettingsDescription": "Configureer hoe deze AI-gateway het gebruik beperkt op basis van uitgaven of tokenlimieten",
|
||||
"sidebarApiKeys": "API sleutels",
|
||||
"sidebarOrgs": "Organisaties",
|
||||
"sidebarProvisioning": "Provisie",
|
||||
"sidebarSettings": "Instellingen",
|
||||
"sidebarAllUsers": "Alle gebruikers",
|
||||
@@ -2105,7 +2128,7 @@
|
||||
"sidebarMachineClients": "Machines",
|
||||
"sidebarDomains": "Domeinen",
|
||||
"sidebarGeneral": "Beheren",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarLogAndAnalytics": "Logboeken & Analytics",
|
||||
"sidebarBluePrints": "Blauwdrukken",
|
||||
"sidebarAlerting": "Waarschuwingen",
|
||||
"sidebarHealthChecks": "Gezondheidscontroles",
|
||||
@@ -2176,7 +2199,7 @@
|
||||
"alertingRuleEnabled": "Regel ingeschakeld",
|
||||
"alertingSectionSource": "Bron",
|
||||
"alertingSourceType": "Brontype",
|
||||
"alertingSourceSite": "Site",
|
||||
"alertingSourceSite": "Referentie",
|
||||
"alertingSourceHealthCheck": "Gezondheidscontrole",
|
||||
"alertingPickSites": "Sites",
|
||||
"alertingPickHealthChecks": "Gezondheidscontroles",
|
||||
@@ -2198,7 +2221,7 @@
|
||||
"alertingResourcesEmpty": "Geen bronnen met doelen in de eerste 10 resultaten.",
|
||||
"alertingSectionTrigger": "Trigger",
|
||||
"alertingTrigger": "Wanneer te waarschuwen",
|
||||
"alertingTriggerSiteOnline": "Site online",
|
||||
"alertingTriggerSiteOnline": "Site Online Tijd",
|
||||
"alertingTriggerSiteOffline": "Site offline",
|
||||
"alertingTriggerSiteToggle": "Site status wijzigt",
|
||||
"alertingTriggerHcHealthy": "Gezondheidscontrole gezond",
|
||||
@@ -2230,7 +2253,7 @@
|
||||
"alertingWebhookMethod": "HTTP-methode",
|
||||
"alertingWebhookSecret": "Ondertekengeheim (optioneel)",
|
||||
"alertingWebhookSecretPlaceholder": "HMAC-geheim",
|
||||
"alertingWebhookHeaders": "Headers",
|
||||
"alertingWebhookHeaders": "Kopteksten",
|
||||
"alertingAddHeader": "Header toevoegen",
|
||||
"alertingSelectSites": "Selecteer sites…",
|
||||
"alertingSitesSelected": "{count} sites geselecteerd",
|
||||
@@ -2448,7 +2471,7 @@
|
||||
"sidebarCollapse": "Inklappen",
|
||||
"sidebarExpand": "Uitklappen",
|
||||
"productUpdateMoreInfo": "Nog {noOfUpdates} updates",
|
||||
"productUpdateInfo": "{noOfUpdates} updates",
|
||||
"productUpdateInfo": "Nog {noOfUpdates} updates",
|
||||
"productUpdateWhatsNew": "Wat is nieuw",
|
||||
"productUpdateTitle": "Update Producten",
|
||||
"productUpdateEmpty": "Geen updates",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Laat Olm draaien op uw systeem",
|
||||
"clientOlmCredentials": "Aanmeldgegevens",
|
||||
"clientOlmCredentialsDescription": "Dit is hoe de client zich zal verifiëren met de server",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Eindpunt",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Geheim",
|
||||
"clientCredentialsSave": "Sla de aanmeldgegevens op",
|
||||
@@ -2730,7 +2753,7 @@
|
||||
"requireDeviceApproval": "Vereist goedkeuring van apparaat",
|
||||
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
|
||||
"sshSettings": "SSH",
|
||||
"inferenceSettings": "AI Gateway",
|
||||
"inferenceSettings": "AI-gateway",
|
||||
"sshAccess": "SSH Toegang",
|
||||
"rdpSettings": "RDP",
|
||||
"vncSettings": "VNC",
|
||||
@@ -2886,7 +2909,7 @@
|
||||
"editInternalResourceDialogModeCidr": "CIDR",
|
||||
"editInternalResourceDialogModeHttp": "HTTP",
|
||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||
"editInternalResourceDialogModeInference": "AI-gateway",
|
||||
"editInternalResourceDialogModeSsh": "SSH",
|
||||
"editInternalResourceDialogScheme": "Schema",
|
||||
"editInternalResourceDialogEnableSsl": "Schakel TLS in",
|
||||
@@ -2907,13 +2930,13 @@
|
||||
"privateResourceAllowIcmpPing": "Sta ICMP Ping toe",
|
||||
"privateResourceNetworkAccess": "Netwerktoegang",
|
||||
"privateResourceNetworkAccessDescription": "Beheer TCP/UDP poorttoegang en of ICMP-ping is toegestaan voor deze bron.",
|
||||
"hostSettings": "Host",
|
||||
"hostSettings": "Hostnaam",
|
||||
"cidrSettings": "CIDR",
|
||||
"createInternalResourceDialogResourceProperties": "Bron-eigenschappen",
|
||||
"createInternalResourceDialogName": "Naam",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSite": "Referentie",
|
||||
"selectSite": "Selecteer site...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, one {# locatie} other {# locaties}}",
|
||||
"labelsSelectorLabelsCount": "{count, plural, one {# label} other {# labels}}",
|
||||
"noSitesFound": "Geen sites gevonden.",
|
||||
"createInternalResourceDialogProtocol": "Protocol",
|
||||
@@ -2946,7 +2969,7 @@
|
||||
"createInternalResourceDialogModeHttp": "HTTP",
|
||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||
"createInternalResourceDialogModeSsh": "SSH",
|
||||
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||
"createInternalResourceDialogModeInference": "AI-gateway",
|
||||
"scheme": "Schema",
|
||||
"createInternalResourceDialogScheme": "Schema",
|
||||
"createInternalResourceDialogEnableSsl": "Schakel TLS in",
|
||||
@@ -3089,11 +3112,11 @@
|
||||
"regionNorthernEurope": "Noord-Europa",
|
||||
"regionSouthernEurope": "Zuid-Europa",
|
||||
"regionWesternEurope": "West-Europa",
|
||||
"regionOceania": "Oceania",
|
||||
"regionOceania": "Oceanië",
|
||||
"regionAustraliaAndNewZealand": "Australië en Nieuw-Zeeland",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"regionMelanesia": "Melanesië",
|
||||
"regionMicronesia": "Micronesië",
|
||||
"regionPolynesia": "Polynesië",
|
||||
"managedSelfHosted": {
|
||||
"title": "Beheerde Self-Hosted",
|
||||
"description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Verwijderen",
|
||||
"idpGoogleConfiguration": "Google Configuratie",
|
||||
"idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Uw Google OAuth2-client-ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 Clientgeheim",
|
||||
"idpAzureConfiguration": "Azure Entra ID configuratie",
|
||||
"idpAzureConfigurationDescription": "Azure Entra ID OAuth2 referenties configureren",
|
||||
"idpTenantId": "Tenant-ID",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "tenant-ID",
|
||||
"idpAzureTenantIdDescription": "Azure tenant ID (gevonden in Azure Active Directory overzicht)",
|
||||
"idpAzureClientIdDescription": "Azure App registratie Client ID",
|
||||
"idpAzureClientSecretDescription": "Azure App registratie client geheim",
|
||||
@@ -3181,7 +3204,7 @@
|
||||
"idpTenantIdLabel": "Tenant-ID",
|
||||
"idpAzureClientIdDescription2": "Azure App registratie Client ID",
|
||||
"idpAzureClientSecretDescription2": "Azure App registratie client geheim",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||
"idpGoogleDescription": "Algemene OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"subnet": "Subnet",
|
||||
"utilitySubnet": "Hulpmiddel Subnet",
|
||||
@@ -3348,7 +3371,7 @@
|
||||
"manageMachineClientsDescription": "Creëer en beheer clients die servers en systemen gebruiken om privé verbinding te maken met bronnen",
|
||||
"machineClientsBannerTitle": "Servers & Geautomatiseerde Systemen",
|
||||
"machineClientsBannerDescription": "Machineclients zijn bedoeld voor servers en geautomatiseerde systemen die niet aan een specifieke gebruiker zijn gekoppeld. Ze verifiëren met een ID en geheim, en kunnen draaien met Pangolin CLI, Olm CLI, of Olm als een container.",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerPangolinCLI": "Pangoline Cloud",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Olm-container",
|
||||
"clientsTableUserClients": "Gebruiker",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Prioriteit",
|
||||
"priorityDescription": "routes met hogere prioriteit worden eerst geëvalueerd. Prioriteit = 100 betekent automatisch bestellen (systeem beslist de). Gebruik een ander nummer om handmatige prioriteit af te dwingen.",
|
||||
"instanceName": "Naam instantie",
|
||||
"clearInstanceName": "Serverassociatie resetten",
|
||||
"pathMatchModalTitle": "Configureren van overeenkomende pad",
|
||||
"pathMatchModalDescription": "Stel in hoe inkomende verzoeken moeten worden gekoppeld aan hun pad.",
|
||||
"pathMatchType": "Wedstrijd Type",
|
||||
@@ -3510,7 +3534,7 @@
|
||||
"pathRewriteExact": "Exacte",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Verwijder",
|
||||
"pathRewriteStripLabel": "strip",
|
||||
"pathRewriteStripLabel": "verwijder",
|
||||
"sidebarEnableEnterpriseLicense": "Activeer Enterprise Licentie",
|
||||
"cannotbeUndone": "Dit kan niet ongedaan worden gemaakt.",
|
||||
"toConfirm": "om te bevestigen.",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Toegestaan door regel",
|
||||
"allowedNoAuth": "Toegestaan geen authenticatie",
|
||||
"validAccessToken": "Geldige toegangstoken",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Geldige headerauthenticatie",
|
||||
"validPincode": "Geldige pincode",
|
||||
"validPassword": "Geldig wachtwoord",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Geldige e-mail",
|
||||
"validSSO": "Geldige SSO",
|
||||
"validVirtualAPIKey": "Geldige Virtuele API-sleutel",
|
||||
"view": "Bekijk",
|
||||
"configManaged": "Configuratie Beheerd",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Achtergelaten door regel",
|
||||
"noSessions": "Geen sessies",
|
||||
"temporaryRequestToken": "Tijdelijk verzoek token",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Geen geldige auth",
|
||||
"ip": "IP-adres",
|
||||
"reason": "Reden",
|
||||
"requestLogs": "HTTP-aanvraaglogboeken",
|
||||
@@ -3638,7 +3662,7 @@
|
||||
"auto": "Automatisch",
|
||||
"TTL": "TTL",
|
||||
"howToAddRecords": "Hoe voeg ik Records toe",
|
||||
"dnsRecord": "DNS Records",
|
||||
"dnsRecord": "DNS-records",
|
||||
"required": "vereist",
|
||||
"domainSettingsUpdated": "Domeininstellingen succesvol bijgewerkt",
|
||||
"orgOrDomainIdMissing": "Organisatie of domein ID ontbreekt",
|
||||
@@ -3750,23 +3774,24 @@
|
||||
"regenerateCredentialsWarning": "Het opnieuw genereren van inloggegevens zal de vorige ongeldig maken en een slechte verbinding veroorzaken. Zorg ervoor dat u alle configuraties die deze inloggegevens gebruiken bijwerkt.",
|
||||
"confirm": "Bevestigen",
|
||||
"regenerateCredentialsConfirmation": "Weet u zeker dat u de inloggegevens opnieuw wilt genereren?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Eindpunt",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Geheime sleutel",
|
||||
"niceId": "Leuk ID",
|
||||
"niceIdUpdated": "Leuke ID bijgewerkt",
|
||||
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
|
||||
"niceIdUpdatedSuccessfully": "Mooi ID succesvol bijgewerkt",
|
||||
"niceIdUpdateError": "Fout bij bijwerken ID Nice",
|
||||
"niceIdUpdateErrorDescription": "Fout opgetreden tijdens het bijwerken van de ID van Nice.",
|
||||
"niceIdCannotBeEmpty": "Nice ID mag niet leeg zijn",
|
||||
"enterIdentifier": "ID invoeren",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "ID pad",
|
||||
"deviceLoginUseDifferentAccount": "Niet u? Gebruik een ander account.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Een apparaat vraagt om toegang tot dit account.",
|
||||
"loginSelectAuthenticationMethod": "Selecteer een verificatiemethode om door te gaan.",
|
||||
"noData": "Geen gegevens",
|
||||
"machineClients": "Machine Clienten",
|
||||
"install": "Installeren",
|
||||
"downloadInstaller": "Download Installer",
|
||||
"run": "Uitvoeren",
|
||||
"envFile": "Omgevingsbestand",
|
||||
"serviceFile": "Servicebestand",
|
||||
@@ -3835,7 +3860,7 @@
|
||||
"internalResourceAuthDaemonStrategy": "SSH Auth Daemon locatie",
|
||||
"internalResourceAuthDaemonStrategyDescription": "Kies waar de SSH authenticatie daemon wordt uitgevoerd: op de website (Newt) of op een externe host.",
|
||||
"internalResourceAuthDaemonDescription": "De SSH authenticatie daemon zorgt voor SSH sleutelondertekening en PAM authenticatie voor deze resource. Kies of het wordt uitgevoerd op de website (Nieuw) of op een afzonderlijke externe host. Zie <docsLink>de documentatie</docsLink> voor meer.",
|
||||
"internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
|
||||
"internalResourceAuthDaemonDocsUrl": "https:\\/\\/docs.pangolin.net",
|
||||
"internalResourceAuthDaemonStrategyPlaceholder": "Selecteer strategie",
|
||||
"internalResourceAuthDaemonStrategyLabel": "Locatie",
|
||||
"internalResourceAuthDaemonSite": "In de site",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Kernel versie",
|
||||
"deviceModel": "Apparaat model",
|
||||
"serialNumber": "Serienummer",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Hostnaam",
|
||||
"firstSeen": "Eerst gezien",
|
||||
"lastSeen": "Laatst gezien op",
|
||||
"biometricsEnabled": "Biometrie ingeschakeld",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Losgekoppeld",
|
||||
"approvalsEmptyStateTitle": "Apparaat goedkeuringen niet ingeschakeld",
|
||||
"approvalsEmptyStateDescription": "Apparaatgoedkeuringen voor rollen inschakelen om goedkeuring van de beheerder te vereisen voordat gebruikers nieuwe apparaten kunnen koppelen.",
|
||||
"approvalsEmptyStateHowToTitle": "Hoe te activeren",
|
||||
"approvalsEmptyStateStep1Title": "Ga naar rollen",
|
||||
"approvalsEmptyStateStep1Description": "Navigeer naar de rolinstellingen van uw organisatie om apparaatgoedkeuringen te configureren.",
|
||||
"approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen",
|
||||
@@ -4237,7 +4263,7 @@
|
||||
"resourceLauncherSortDescending": "Aflopend sorteren",
|
||||
"resourceLauncherSettings": "Instellingen",
|
||||
"resourceLauncherGroupBy": "Groep Op",
|
||||
"resourceLauncherGroupBySite": "Site",
|
||||
"resourceLauncherGroupBySite": "Referentie",
|
||||
"resourceLauncherGroupByLabel": "Label",
|
||||
"resourceLauncherGroupByNone": "Geen",
|
||||
"resourceLauncherLayout": "Lay-out",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Bekijk als Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Verbindingsinformatie en -status voor deze bron.",
|
||||
"resourceLauncherResourceDetails": "Brongegevens",
|
||||
"resourceLauncherSitesDescription": "De bron is toegankelijk via de volgende sites.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Bekijk site als Admin",
|
||||
"resourceLauncherFilterBySite": "Filter op site",
|
||||
"resourceLauncherSshCommand": "SSH Commando",
|
||||
"resourceLauncherSshCommandDescription": "Gebruik de Pangolin CLI om een SSH-sessie te openen naar deze bron.",
|
||||
"resourceLauncherAuthMethodsDescription": "Authenticatiemethoden ingeschakeld voor deze bron.",
|
||||
"resourceLauncherPrivateClientRequired": "Maak verbinding met een client op uw apparaat om deze bron privé te benaderen.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Client Verbinding Vereist",
|
||||
@@ -4262,7 +4293,7 @@
|
||||
"resourceLauncherTcp": "TCP",
|
||||
"resourceLauncherUdp": "UDP",
|
||||
"resourceLauncherUnlabeled": "Geen label",
|
||||
"resourceLauncherAiGateway": "AI Gateway",
|
||||
"resourceLauncherAiGateway": "AI-gateway",
|
||||
"resourceLauncherNoSite": "Geen Site",
|
||||
"resourceLauncherAvailableModels": "Beschikbare Modellen",
|
||||
"resourceLauncherAvailableModelsDescription": "Modellen die u met deze AI-gateway kunt gebruiken.",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus",
|
||||
"sessionToolbarShow": "Toon werkbalk",
|
||||
"sessionToolbarHide": "Verberg werkbalk",
|
||||
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken"
|
||||
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken",
|
||||
"check": "Controleren"
|
||||
}
|
||||
|
||||
+69
-37
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Cele na tej stronie",
|
||||
"siteSetting": "Ustawienia {siteName}",
|
||||
"siteNewtTunnel": "Newt Site (Rekomendowane)",
|
||||
"pangolinSite": "Witryna Pangolin",
|
||||
"siteNewtTunnelDescription": "Najprostszy sposób na stworzenie punktu wejścia w sieci. Nie ma dodatkowej konfiguracji.",
|
||||
"siteWg": "Podstawowy WireGuard",
|
||||
"siteWgDescription": "Użyj dowolnego klienta WireGuard do utworzenia tunelu. Wymagana jest ręczna konfiguracja NAT.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Usuń klucz API",
|
||||
"apiKeysManage": "Zarządzaj kluczami API",
|
||||
"apiKeysDescription": "Klucze API służą do uwierzytelniania z API integracji",
|
||||
"orgsManage": "Zarządzaj organizacjami",
|
||||
"orgsDescription": "Zobacz i zarządzaj wszystkimi organizacjami w tej instancji",
|
||||
"provisioningKeysTitle": "Klucz Zaopatrzenia",
|
||||
"provisioningKeysManage": "Zarządzaj kluczami zaopatrzenia",
|
||||
"provisioningKeysDescription": "Klucze zaopatrzenia są używane do uwierzytelniania zautomatyzowanego zaopatrzenia twojej organizacji.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Nazwa (Opcjonalnie)",
|
||||
"accessControls": "Kontrola dostępu",
|
||||
"userDescription2": "Zarządzaj ustawieniami tego użytkownika",
|
||||
"userGeneralSettingsDescription": "Zarządzaj rolami i ustawieniami tego użytkownika w organizacji",
|
||||
"accessRoleErrorAdd": "Nie udało się dodać użytkownika do roli",
|
||||
"accessRoleErrorAddDescription": "Wystąpił błąd podczas dodawania użytkownika do roli.",
|
||||
"userSaved": "Użytkownik zapisany",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Nieprawidłowa nazwa serwera TLS. Użyj formatu nazwy domeny lub zapisz pusty, aby usunąć nazwę serwera TLS.",
|
||||
"proxyEnableSSL": "Włącz TLS",
|
||||
"proxyEnableSSLDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z celami.",
|
||||
"target": "Target",
|
||||
"target": "Cel",
|
||||
"configureTarget": "Konfiguruj Targety",
|
||||
"targetErrorFetch": "Nie udało się pobrać celów",
|
||||
"targetErrorFetchDescription": "Wystąpił błąd podczas pobierania celów",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Gdy reguły są wyłączone, cały ruch przechodzi do uwierzytelniania.",
|
||||
"policyAccessRulesFallthroughOn": "Gdy żadna reguła nie pasuje, ruch przechodzi do uwierzytelniania.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/administrator/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Zapisz zasady",
|
||||
"resourceErrorCreate": "Błąd podczas tworzenia zasobu",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Nieznane polecenie",
|
||||
"newtErrorFetchReleases": "Nie udało się pobrać informacji o wydaniu: {err}",
|
||||
"newtErrorFetchLatest": "Błąd podczas pobierania najnowszego wydania: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Koniec punktu pracy",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Sekret",
|
||||
"newtVersion": "Wersja",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Generuj kod resetowania hasła",
|
||||
"passwordResetCodeGenerated": "Wygenerowany kod resetowania hasła",
|
||||
"passwordResetCodeGeneratedDescription": "Udostępnij ten kod użytkownikowi. Mogą go użyć do zresetowania hasła.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "URL resetowania",
|
||||
"passwordNew": "Nowe hasło",
|
||||
"passwordNewConfirm": "Potwierdź nowe hasło",
|
||||
"changePassword": "Zmień hasło",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Błąd podczas wylogowywania",
|
||||
"signingAs": "Zalogowany jako",
|
||||
"serverAdmin": "Administrator serwera",
|
||||
"promoteServerAdmin": "Promuj na administratora serwera",
|
||||
"promoteServerAdminTitle": "Promuj na Administratora Serwera",
|
||||
"promoteServerAdminQuestion": "Czy na pewno chcesz promować {selectedUser} na administratora serwera?",
|
||||
"promoteServerAdminMessage": "Administratorzy serwera mają najwyższe uprawnienia i mogą zarządzać serwerem.",
|
||||
"promoteServerAdminWarning": "Można to cofnąć w dowolnym momencie przez zdegradowanie użytkownika.",
|
||||
"promoteServerAdminConfirm": "Promuj na Administratora Serwera",
|
||||
"promoteServerAdminSuccess": "Użytkownik Promowany",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} jest teraz administratorem serwera.",
|
||||
"promoteServerAdminError": "Nie udało się promować użytkownika",
|
||||
"demoteServerAdmin": "Zdegraduj z administratora serwera",
|
||||
"demoteServerAdminTitle": "Zdegraduj z Administratora Serwera",
|
||||
"demoteServerAdminQuestion": "Czy na pewno chcesz zdegradować {selectedUser} z administratora serwera?",
|
||||
"demoteServerAdminMessage": "{selectedUser} straci wszystkie uprawnienia administratora serwera.",
|
||||
"demoteServerAdminWarning": "Można to cofnąć w dowolnym momencie przez promowanie użytkownika.",
|
||||
"demoteServerAdminConfirm": "Zdegraduj z administratora serwera",
|
||||
"demoteServerAdminSuccess": "Użytkownik zdyskredytowany",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} nie jest już administratorem serwera.",
|
||||
"demoteServerAdminError": "Nie udało się zdegradować użytkownika",
|
||||
"managedSelfhosted": "Zarządzane Samodzielnie-Hostingowane",
|
||||
"otpEnable": "Włącz uwierzytelnianie dwuskładnikowe",
|
||||
"otpDisable": "Wyłącz uwierzytelnianie dwuskładnikowe",
|
||||
@@ -1682,7 +1704,7 @@
|
||||
"commandAiProviders": "Dostawcy AI",
|
||||
"sidebarVirtualApiKeys": "Wirtualne Klucze API",
|
||||
"sidebarMyApiKeys": "Twoje Klucze API",
|
||||
"sidebarAccount": "Launcher",
|
||||
"sidebarAccount": "Uruchamiacz",
|
||||
"commandVirtualApiKeys": "Wirtualne Klucze API",
|
||||
"virtualApiKeysTitle": "Zarządzaj Wirtualnymi Kluczami API",
|
||||
"virtualApiKeysDescription": "Utwórz i zarządzaj ręcznymi kluczami API dla dostępu do publicznych bram AI",
|
||||
@@ -1852,16 +1874,16 @@
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||
"aiProviderTypeVercelAiGateway": "Bramka Vercel AI",
|
||||
"aiProviderTypeCustom": "Niestandardowy",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||
"aiProviderTypeOpenaiDescription": "API OpenAI",
|
||||
"aiProviderTypeAnthropicDescription": "API Anthropic",
|
||||
"aiProviderTypeGoogleGeminiDescription": "API Google Gemini",
|
||||
"aiProviderTypeVertexAiDescription": "API Google Vertex AI",
|
||||
"aiProviderTypeBedrockDescription": "API Amazon Bedrock Runtime",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "API Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouterDescription": "API OpenRouter",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "API Bramki Vercel AI",
|
||||
"aiProviderTypeCustomDescription": "Przynieś swój własny endpoint lub trasę przez cele witryny",
|
||||
"aiProviderUpstreamUrl": "URL do góry",
|
||||
"aiProviderUpstreamUrlDescription": "Podstawowy URL dla API dostawcy",
|
||||
@@ -1882,7 +1904,7 @@
|
||||
"aiProviderAuthTypeXGoogApiKeyDescription": "nagłówek x-goog-klucz-api. Używane przez Google Gemini",
|
||||
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||
"aiProviderAuthTypeHecDescription": "Autoryzacja: klucz Splunk. Używane przez Splunk HTTP Event Collector",
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Bramka Cloudflare AI",
|
||||
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autoryzacja: klucz Bearer. Używane przez Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeNone": "Bez autoryzacji",
|
||||
"aiProviderAuthTypePassthrough": "Przejście",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Budżet",
|
||||
"resourceBudgetSettingsDescription": "Skonfiguruj, jak ta brama AI ogranicza użycie na podstawie limitów wydatków lub tokenów",
|
||||
"sidebarApiKeys": "Klucze API",
|
||||
"sidebarOrgs": "Organizacje",
|
||||
"sidebarProvisioning": "Dostarczanie",
|
||||
"sidebarSettings": "Ustawienia",
|
||||
"sidebarAllUsers": "Wszyscy użytkownicy",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Uruchom Olm na swoim systemie",
|
||||
"clientOlmCredentials": "Dane logowania",
|
||||
"clientOlmCredentialsDescription": "W ten sposób klient będzie uwierzytelniał się z serwerem",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Koniec punktu",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Sekret",
|
||||
"clientCredentialsSave": "Zapisz dane logowania",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Nie znaleziono zasobów proxy.",
|
||||
"resourcesTableNoInternalResourcesFound": "Nie znaleziono prywatnych zasobów.",
|
||||
"resourcesTableDestination": "Miejsce docelowe",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Pseudonim",
|
||||
"resourcesTableAliasAddress": "Adres aliasu",
|
||||
"resourcesTableAliasAddressInfo": "Ten adres jest częścią podsieci użyteczności organizacji. Jest używany do rozwiązywania rekordów aliasu przy użyciu wewnętrznej rozdzielczości DNS.",
|
||||
"resourcesTableClients": "Klientami",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Pseudonim",
|
||||
"editInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Brak dostępnych stron",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Musisz mieć co najmniej jedną lokalizację Newt z skonfigurowaną podsiecią, aby tworzyć prywatne zasoby.",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Miejsce docelowe",
|
||||
"createInternalResourceDialogDestinationHostDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Zakres CIDR zasobu w sieci witryny.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Pseudonim",
|
||||
"createInternalResourceDialogAliasDescription": "Opcjonalny wewnętrzny alias DNS dla tego zasobu.",
|
||||
"internalResourceAliasLocalWarning": "Alias kończący się na .local może powodować problemy z rozpoznawaniem z powodu mDNS w niektórych sieciach.",
|
||||
"internalResourceDownstreamSchemeRequired": "Schemat jest wymagany dla zasobów HTTP",
|
||||
@@ -3091,9 +3114,9 @@
|
||||
"regionWesternEurope": "Europa Zachodnia",
|
||||
"regionOceania": "Oceania",
|
||||
"regionAustraliaAndNewZealand": "Australia i Nowa Zelandia",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"regionMelanesia": "Melanezja",
|
||||
"regionMicronesia": "Mikronezja",
|
||||
"regionPolynesia": "Polinezja",
|
||||
"managedSelfHosted": {
|
||||
"title": "Zarządzane Samodzielnie-Hostingowane",
|
||||
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Usuń",
|
||||
"idpGoogleConfiguration": "Konfiguracja Google",
|
||||
"idpGoogleConfigurationDescription": "Skonfiguruj dane logowania Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Identyfikator klienta Google OAuth2",
|
||||
"idpGoogleClientSecretDescription": "Klucz tajny klienta Google OAuth2",
|
||||
"idpAzureConfiguration": "Konfiguracja Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Skonfiguruj poświadczenia Aure Entra ID OAuth2",
|
||||
"idpTenantId": "ID Najemcy",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "identyfikator-najemcy",
|
||||
"idpAzureTenantIdDescription": "Identyfikator dzierżawcy azure (znaleziony w Azuure Active Directory podglądu)",
|
||||
"idpAzureClientIdDescription": "Identyfikator klienta aplikacji Azure",
|
||||
"idpAzureClientSecretDescription": "Klucz tajny klienta aplikacji Azure",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "Identyfikator klienta aplikacji Azure",
|
||||
"idpAzureClientSecretDescription2": "Klucz tajny klienta aplikacji Azure",
|
||||
"idpGoogleDescription": "Dostawca Google OAuth2/OIDC",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Dostawca Microsoft Azure OAuth2/OIDC",
|
||||
"subnet": "Podsieć",
|
||||
"utilitySubnet": "Użyteczna podsieć",
|
||||
"subnetDescription": "Podsieć dla konfiguracji sieci tej organizacji.",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Priorytet",
|
||||
"priorityDescription": "Najpierw oceniane są trasy priorytetowe. Priorytet = 100 oznacza automatyczne zamawianie (decyzje systemowe). Użyj innego numeru, aby wyegzekwować ręczny priorytet.",
|
||||
"instanceName": "Nazwa instancji",
|
||||
"clearInstanceName": "Reset Server Association",
|
||||
"pathMatchModalTitle": "Skonfiguruj dopasowanie ścieżki",
|
||||
"pathMatchModalDescription": "Skonfiguruj sposób dopasowania przychodzących żądań na podstawie ich ścieżki.",
|
||||
"pathMatchType": "Typ dopasowania",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Dozwolone przez regułę",
|
||||
"allowedNoAuth": "Dozwolone Brak Auth",
|
||||
"validAccessToken": "Ważny token dostępu",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Ważny Nagłówek Auth",
|
||||
"validPincode": "Prawidłowy kod PIN",
|
||||
"validPassword": "Prawidłowe hasło",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Ważny email",
|
||||
"validSSO": "Prawidłowe SSO",
|
||||
"validVirtualAPIKey": "Ważny Wirtualny Klucz API",
|
||||
"view": "Zobacz",
|
||||
"configManaged": "Konfiguracja zarządzana",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Upuszczone przez regułę",
|
||||
"noSessions": "Brak sesji",
|
||||
"temporaryRequestToken": "Tymczasowy token żądania",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Brak Ważnej Autoryzacji",
|
||||
"ip": "IP",
|
||||
"reason": "Powód",
|
||||
"requestLogs": "Dzienniki żądań HTTP",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Regeneracja poświadczeń spowoduje unieważnienie poprzednich danych i spowoduje rozłączenie. Upewnij się, że aktualizacja wszystkich konfiguracji, które używają tych poświadczeń.",
|
||||
"confirm": "Potwierdź",
|
||||
"regenerateCredentialsConfirmation": "Czy na pewno chcesz wygenerować dane logowania?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Koniec punktu",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Sekretny klucz",
|
||||
"niceId": "Niepoprawne ID",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Wystąpił błąd podczas aktualizowania Nicei ID.",
|
||||
"niceIdCannotBeEmpty": "Niepoprawny identyfikator nie może być pusty",
|
||||
"enterIdentifier": "Wprowadź identyfikator",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Identyfikator",
|
||||
"deviceLoginUseDifferentAccount": "Nie ty? Użyj innego konta.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Urządzenie żąda dostępu do tego konta.",
|
||||
"loginSelectAuthenticationMethod": "Wybierz metodę uwierzytelniania aby kontynuować.",
|
||||
"noData": "Brak danych",
|
||||
"machineClients": "Klienci maszyn",
|
||||
"install": "Zainstaluj",
|
||||
"downloadInstaller": "Download Installer",
|
||||
"run": "Uruchom",
|
||||
"envFile": "Plik środowiska",
|
||||
"serviceFile": "Plik serwisu",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Wersja jądra",
|
||||
"deviceModel": "Model urządzenia",
|
||||
"serialNumber": "Numer seryjny",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Nazwa hosta",
|
||||
"firstSeen": "Widziany po raz pierwszy",
|
||||
"lastSeen": "Ostatnio widziane",
|
||||
"biometricsEnabled": "Biometria włączona",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Rozłączony",
|
||||
"approvalsEmptyStateTitle": "Zatwierdzanie urządzenia nie włączone",
|
||||
"approvalsEmptyStateDescription": "Włącz zatwierdzanie urządzeń dla ról aby wymagać zgody administratora, zanim użytkownicy będą mogli podłączyć nowe urządzenia.",
|
||||
"approvalsEmptyStateHowToTitle": "How to Enable",
|
||||
"approvalsEmptyStateStep1Title": "Przejdź do ról",
|
||||
"approvalsEmptyStateStep1Description": "Przejdź do ustawień ról swojej organizacji, aby skonfigurować zatwierdzenia urządzenia.",
|
||||
"approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia",
|
||||
@@ -4016,8 +4042,8 @@
|
||||
"s3DestTabFormat": "Format",
|
||||
"s3DestNameLabel": "Nazwa",
|
||||
"s3DestNamePlaceholder": "Moje miejsce docelowe S3",
|
||||
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||
"s3DestAccessKeyIdLabel": "AWS Identyfikator dostępu do klucza",
|
||||
"s3DestSecretAccessKeyLabel": "AWS Sekretny Klucz Dostępu",
|
||||
"s3DestSecretAccessKeyPlaceholder": "Twój AWS Secret Access Key",
|
||||
"s3DestRegionLabel": "Region AWS",
|
||||
"s3DestBucketLabel": "Nazwa kubła",
|
||||
@@ -4166,7 +4192,7 @@
|
||||
"webhookUrlLabel": "URL",
|
||||
"webhookHeaderKeyPlaceholder": "Klucz",
|
||||
"webhookHeaderValuePlaceholder": "Wartość",
|
||||
"alertLabel": "Alert",
|
||||
"alertLabel": "Alarm",
|
||||
"domainPickerWildcardSubdomainNotAllowed": "Uniwersalne subdomeny nie są dozwolone.",
|
||||
"domainPickerWildcardCertWarning": "Uniwersalne zasoby mogą wymagać dodatkowej konfiguracji, aby działać poprawnie.",
|
||||
"domainPickerWildcardCertWarningLink": "Dowiedz się więcej",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Przeglądaj jako Administrator",
|
||||
"resourceLauncherResourceDetailsDescription": "Informacje i status połączenia dla tego zasobu.",
|
||||
"resourceLauncherResourceDetails": "Szczegóły zasobów",
|
||||
"resourceLauncherSitesDescription": "Zasoby są dostępne na następujących stronach.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Przeglądaj stronę jako Administrator",
|
||||
"resourceLauncherFilterBySite": "Filtruj według strony",
|
||||
"resourceLauncherSshCommand": "Komenda SSH",
|
||||
"resourceLauncherSshCommandDescription": "Użyj CLI Pangolin, aby otworzyć sesję SSH dla tego zasobu.",
|
||||
"resourceLauncherAuthMethodsDescription": "Metody uwierzytelniania włączone dla tego zasobu.",
|
||||
"resourceLauncherPrivateClientRequired": "Połącz się z klientem na swoim urządzeniu, aby uzyskać dostęp do tego zasobu prywatnie.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Wymagane połączenie z klientem",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode",
|
||||
"sessionToolbarShow": "Pokaż pasek narzędzi",
|
||||
"sessionToolbarHide": "Ukryj pasek narzędzi",
|
||||
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę"
|
||||
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę",
|
||||
"check": "Sprawdź"
|
||||
}
|
||||
|
||||
+63
-31
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Alvos neste site",
|
||||
"siteSetting": "Configurações do {siteName}",
|
||||
"siteNewtTunnel": "Novo Site (Recomendado)",
|
||||
"pangolinSite": "Site Pangolin",
|
||||
"siteNewtTunnelDescription": "Maneira mais fácil de criar um ponto de entrada em qualquer rede. Nenhuma configuração extra.",
|
||||
"siteWg": "WireGuard Básico",
|
||||
"siteWgDescription": "Use qualquer cliente do WireGuard para estabelecer um túnel. Configuração manual NAT é necessária.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Excluir Chave API",
|
||||
"apiKeysManage": "Gerir Chaves API",
|
||||
"apiKeysDescription": "As chaves API são usadas para autenticar com a API de integração",
|
||||
"orgsManage": "Gerir Organizações",
|
||||
"orgsDescription": "Ver e gerir todas as organizações nesta instância",
|
||||
"provisioningKeysTitle": "Chave de provisionamento",
|
||||
"provisioningKeysManage": "Gerenciar chaves de provisionamento",
|
||||
"provisioningKeysDescription": "Chaves de provisionamento são usadas para autenticar o provisionamento automatizado do site para sua organização.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Nome (Opcional)",
|
||||
"accessControls": "Controlos de Acesso",
|
||||
"userDescription2": "Gerir as configurações deste utilizador",
|
||||
"userGeneralSettingsDescription": "Gerir os papéis e configurações deste usuário na organização",
|
||||
"accessRoleErrorAdd": "Falha ao adicionar utilizador à função",
|
||||
"accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar utilizador à função.",
|
||||
"userSaved": "Usuário salvo",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Nome do Servidor TLS inválido. Use o formato de nome de domínio ou salve vazio para remover o Nome do Servidor TLS.",
|
||||
"proxyEnableSSL": "Ativar TLS",
|
||||
"proxyEnableSSLDescription": "Habilitar criptografia SSL/TLS para conexões HTTPS seguras aos alvos.",
|
||||
"target": "Target",
|
||||
"target": "Alvo",
|
||||
"configureTarget": "Configurar Alvos",
|
||||
"targetErrorFetch": "Falha ao buscar alvos",
|
||||
"targetErrorFetchDescription": "Ocorreu um erro ao buscar alvos",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Quando as regras estão desativadas, todo o tráfego passa para a autenticação.",
|
||||
"policyAccessRulesFallthroughOn": "Quando nenhuma regra corresponde, o tráfego passa para a autenticação.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/administrador/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Guardar Regras",
|
||||
"resourceErrorCreate": "Erro ao criar recurso",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Gerar código de redefinição de senha",
|
||||
"passwordResetCodeGenerated": "Código de redefinição de senha gerado",
|
||||
"passwordResetCodeGeneratedDescription": "Compartilhe este código com o usuário. Eles podem usá-lo para redefinir sua senha.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "Redefinir URL",
|
||||
"passwordNew": "Nova Palavra-passe",
|
||||
"passwordNewConfirm": "Confirmar Nova Palavra-passe",
|
||||
"changePassword": "Mudar a senha",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Erro ao terminar sessão",
|
||||
"signingAs": "Sessão iniciada como",
|
||||
"serverAdmin": "Administrador do Servidor",
|
||||
"promoteServerAdmin": "Promover a Administrador do Servidor",
|
||||
"promoteServerAdminTitle": "Promover a Administrador do Servidor",
|
||||
"promoteServerAdminQuestion": "Tem certeza que deseja promover {selectedUser} a administrador do servidor?",
|
||||
"promoteServerAdminMessage": "Os administradores de servidor têm os privilégios mais altos e podem gerir o servidor.",
|
||||
"promoteServerAdminWarning": "Isso pode ser desfeito a qualquer momento ao rebaixar o usuário.",
|
||||
"promoteServerAdminConfirm": "Promover a Administrador do Servidor",
|
||||
"promoteServerAdminSuccess": "Usuário Promovido",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} agora é um administrador do servidor.",
|
||||
"promoteServerAdminError": "Falha ao promover o usuário",
|
||||
"demoteServerAdmin": "Rebaixar de Administrador do Servidor",
|
||||
"demoteServerAdminTitle": "Rebaixar de Administrador do Servidor",
|
||||
"demoteServerAdminQuestion": "Tem certeza que deseja rebaixar {selectedUser} de administrador do servidor?",
|
||||
"demoteServerAdminMessage": "{selectedUser} perderá todos os privilégios de administrador do servidor.",
|
||||
"demoteServerAdminWarning": "Isso pode ser desfeito a qualquer momento ao promover o usuário.",
|
||||
"demoteServerAdminConfirm": "Rebaixar de administrador do servidor",
|
||||
"demoteServerAdminSuccess": "Usuário Rebaixado",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} não é mais um administrador do servidor.",
|
||||
"demoteServerAdminError": "Falha ao rebaixar o usuário",
|
||||
"managedSelfhosted": "Gerenciado Auto-Hospedado",
|
||||
"otpEnable": "Ativar Autenticação de Dois Fatores",
|
||||
"otpDisable": "Desativar Autenticação de Dois Fatores",
|
||||
@@ -1797,7 +1819,7 @@
|
||||
"aiClientConfigTabManual": "Configuração Manual",
|
||||
"aiClientConfigPreset": "Configuração",
|
||||
"aiClientConfigStep": "Etapa {number}",
|
||||
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
|
||||
"aiClientConfigEndpointPlaceholder": "https://exemplo.recurso.url.com",
|
||||
"aiClientConfigPlaceholderWarning": "Este snippet inclui valores de exemplo, como um nome de modelo ou URL de recurso. Substitua-os pelos seus antes de usá-lo.",
|
||||
"aiClientConfigRevealError": "Não foi possível carregar sua chave API.",
|
||||
"aiClientConfigRevealRetry": "Tente novamente",
|
||||
@@ -1837,7 +1859,7 @@
|
||||
"aiBudgetPeriodYearly": "Anual",
|
||||
"aiBudgetPeriodLifetime": "Por toda a vida",
|
||||
"aiBudgetUnitUsd": "USD",
|
||||
"aiBudgetUnitTokens": "Tokens",
|
||||
"aiBudgetUnitTokens": "Identificadores",
|
||||
"aiBudgetConflictError": "Já existe um orçamento para este período de reinicialização e tipo de gasto",
|
||||
"aiBudgetInvalidAmountError": "Digite um gasto máximo maior que 0",
|
||||
"aiBudgetUpdated": "Orçamentos atualizados",
|
||||
@@ -1848,7 +1870,7 @@
|
||||
"aiProviderTypeOpenai": "OpenAI",
|
||||
"aiProviderTypeAnthropic": "Antropic",
|
||||
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||
"aiProviderTypeVertexAi": "Vertex AI",
|
||||
"aiProviderTypeVertexAi": "Vertex IA",
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
@@ -1905,7 +1927,7 @@
|
||||
"aiProviderBudgetAmount": "Valor do Orçamento",
|
||||
"aiProviderBudgetUnit": "Unidade de Orçamento",
|
||||
"aiProviderBudgetUnitUsd": "USD",
|
||||
"aiProviderBudgetUnitTokens": "Tokens",
|
||||
"aiProviderBudgetUnitTokens": "Identificadores",
|
||||
"aiProviderEnabled": "Ativado",
|
||||
"aiProviderEnabledDescription": "Desativar totalmente este provedor em todos os recursos",
|
||||
"aiProviderErrorCreate": "Falha ao criar provedor de IA",
|
||||
@@ -2053,7 +2075,7 @@
|
||||
"aiUsageTokenTypeReasoning": "Raciocínio",
|
||||
"aiUsageRequests": "Solicitações",
|
||||
"aiUsageCost": "Custo",
|
||||
"aiUsageTokens": "Tokens",
|
||||
"aiUsageTokens": "Identificadores",
|
||||
"aiUsageOther": "Outro",
|
||||
"aiUsageTotalRequests": "Total de Solicitações",
|
||||
"aiUsageTotalTokens": "Total de Tokens",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Orçamento",
|
||||
"resourceBudgetSettingsDescription": "Configure como este gateway de IA restringe o uso com base em gastos ou limites de tokens",
|
||||
"sidebarApiKeys": "Chaves API",
|
||||
"sidebarOrgs": "Organizações",
|
||||
"sidebarProvisioning": "Provisionamento",
|
||||
"sidebarSettings": "Configurações",
|
||||
"sidebarAllUsers": "Todos os utilizadores",
|
||||
@@ -2353,7 +2376,7 @@
|
||||
"containerImage": "Imagem:",
|
||||
"containerState": "Estado:",
|
||||
"containerNetworks": "Redes",
|
||||
"containerHostnameIp": "Hostname/IP",
|
||||
"containerHostnameIp": "Nome do host/IP",
|
||||
"containerLabels": "Marcadores",
|
||||
"containerLabelsCount": "{count, plural, one {# rótulo} other {# rótulos}}",
|
||||
"containerLabelsTitle": "Etiquetas do Contêiner",
|
||||
@@ -2661,7 +2684,7 @@
|
||||
"keepMeInTheLoop": "Mantenha-me à disposição com notícias, atualizações e novos recursos por e-mail."
|
||||
},
|
||||
"siteRequired": "Site é obrigatório.",
|
||||
"olmTunnel": "Olm Tunnel",
|
||||
"olmTunnel": "Túnel Olm",
|
||||
"olmTunnelDescription": "Use Olm para conectividade do cliente",
|
||||
"errorCreatingClient": "Erro ao criar cliente",
|
||||
"clientDefaultsNotFound": "Padrões do cliente não encontrados",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Nenhum recurso de proxy encontrado.",
|
||||
"resourcesTableNoInternalResourcesFound": "Nenhum recurso privado encontrado.",
|
||||
"resourcesTableDestination": "Destino",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Apelido",
|
||||
"resourcesTableAliasAddress": "Endereço do Pseudônimo",
|
||||
"resourcesTableAliasAddressInfo": "Este endereço faz parte da sub-rede de utilitários da organização. É usado para resolver registros de alias usando resolução de DNS interno.",
|
||||
"resourcesTableClients": "Clientes",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Apelido",
|
||||
"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 privados.",
|
||||
@@ -2907,7 +2930,7 @@
|
||||
"privateResourceAllowIcmpPing": "Permitir ICMP Ping",
|
||||
"privateResourceNetworkAccess": "Acesso à Rede",
|
||||
"privateResourceNetworkAccessDescription": "Controlar o acesso à porta TCP/UDP e se o ICMP ping é permitido para este recurso.",
|
||||
"hostSettings": "Host",
|
||||
"hostSettings": "Servidor",
|
||||
"cidrSettings": "CIDR",
|
||||
"createInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
||||
"createInternalResourceDialogName": "Nome",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Destino",
|
||||
"createInternalResourceDialogDestinationHostDescription": "O endereço IP ou o nome do host do recurso na rede do site.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "A faixa CIDR do recurso na rede do site.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Apelido",
|
||||
"createInternalResourceDialogAliasDescription": "Um alias de DNS interno opcional para este recurso.",
|
||||
"internalResourceAliasLocalWarning": "Os aliases terminando em .local podem causar problemas de resolução devido ao mDNS em algumas redes.",
|
||||
"internalResourceDownstreamSchemeRequired": "Esquema é obrigatório para recursos HTTP",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Remover",
|
||||
"idpGoogleConfiguration": "Configuração do Google",
|
||||
"idpGoogleConfigurationDescription": "Configurar as credenciais do Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "ID de Cliente OAuth2 do Google",
|
||||
"idpGoogleClientSecretDescription": "Segredo de cliente OAuth2 do Google",
|
||||
"idpAzureConfiguration": "Configuração de ID do Azure Entra",
|
||||
"idpAzureConfigurationDescription": "Configurar credenciais do Azure Entra ID OAuth2",
|
||||
"idpTenantId": "ID do Inquilino",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "id do inquilino",
|
||||
"idpAzureTenantIdDescription": "ID do tenant Azure (encontrado na visão geral do diretório ativo Azure)",
|
||||
"idpAzureClientIdDescription": "ID cliente de registro do aplicativo Azure",
|
||||
"idpAzureClientSecretDescription": "Segredo cliente de registro do Azure App",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "ID cliente de registro do aplicativo Azure",
|
||||
"idpAzureClientSecretDescription2": "Segredo cliente de registro do Azure App",
|
||||
"idpGoogleDescription": "Provedor Google OAuth2/OIDC",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Provedor Microsoft Azure OAuth2/OIDC",
|
||||
"subnet": "Sub-rede",
|
||||
"utilitySubnet": "Sub-rede de utilidade",
|
||||
"subnetDescription": "A sub-rede para a configuração de rede dessa organização.",
|
||||
@@ -3228,7 +3251,7 @@
|
||||
"domainPickerFreeDomainsPaidFeature": "Os domínios fornecidos são um recurso pago. Assine para obter um domínio incluído no seu plano - não há necessidade de trazer o seu próprio.",
|
||||
"domainPickerVerified": "Verificada",
|
||||
"domainPickerUnverified": "Não verificado",
|
||||
"domainPickerManual": "Manual",
|
||||
"domainPickerManual": "Manualmente",
|
||||
"domainPickerInvalidSubdomainStructure": "Caracteres inválidos serão sanitizados ao serem salvos.",
|
||||
"domainPickerError": "ERRO",
|
||||
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Prioridade",
|
||||
"priorityDescription": "Rotas de alta prioridade são avaliadas primeiro. Prioridade = 100 significa ordem automática (decisões do sistema). Use outro número para aplicar prioridade manual.",
|
||||
"instanceName": "Nome da Instância",
|
||||
"clearInstanceName": "Resetar Associação do Servidor",
|
||||
"pathMatchModalTitle": "Configurar Correspondência de Caminho",
|
||||
"pathMatchModalDescription": "Configure como as solicitações de entrada devem ser correspondidas com base no caminho.",
|
||||
"pathMatchType": "Tipo de Correspondência",
|
||||
@@ -3524,7 +3548,7 @@
|
||||
"searchLogs": "Pesquisar registros...",
|
||||
"action": "Acão",
|
||||
"actor": "Ator",
|
||||
"timestamp": "Timestamp",
|
||||
"timestamp": "Carimbo de data/hora",
|
||||
"accessLogs": "Logs de Acesso",
|
||||
"exportCsv": "Exportar como CSV",
|
||||
"exportError": "Erro desconhecido ao exportar CSV",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Permitido por regra",
|
||||
"allowedNoAuth": "Não Permitido Nenhuma Autenticação",
|
||||
"validAccessToken": "Token de acesso válido",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Autenticação de Cabeçalho Válido",
|
||||
"validPincode": "Código PIN Válido",
|
||||
"validPassword": "Senha válida",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Email válido",
|
||||
"validSSO": "SSO Válido",
|
||||
"validVirtualAPIKey": "Chave de API Virtual Válida",
|
||||
"view": "Visualizar",
|
||||
"configManaged": "Configuração Gerenciada",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Derrubado pela regra",
|
||||
"noSessions": "Sem Sessões",
|
||||
"temporaryRequestToken": "Token de solicitação temporária",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Sem Autenticação Válida",
|
||||
"ip": "PI",
|
||||
"reason": "Motivo",
|
||||
"requestLogs": "Registros de Pedidos HTTP",
|
||||
@@ -3574,7 +3598,7 @@
|
||||
"model": "Modelo",
|
||||
"virtualApiKey": "Chave de API Virtual",
|
||||
"noVirtualApiKey": "Sem chave de API virtual",
|
||||
"stream": "Stream",
|
||||
"stream": "Transmissão",
|
||||
"streaming": "Transmitindo",
|
||||
"nonStreaming": "Não-streaming",
|
||||
"statusCode": "Código de Status",
|
||||
@@ -3751,7 +3775,7 @@
|
||||
"confirm": "Confirmar",
|
||||
"regenerateCredentialsConfirmation": "Você tem certeza que deseja recriar as credenciais?",
|
||||
"endpoint": "Endpoint",
|
||||
"Id": "Id",
|
||||
"Id": "ID",
|
||||
"SecretKey": "Chave secreta",
|
||||
"niceId": "Belo ID",
|
||||
"niceIdUpdated": "Bom ID atualizado",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Ocorreu um erro ao atualizar a ID de Nice.",
|
||||
"niceIdCannotBeEmpty": "Bom ID não pode estar vazio",
|
||||
"enterIdentifier": "Inserir identificador",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Identificador",
|
||||
"deviceLoginUseDifferentAccount": "Não é você? Use uma conta diferente.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Um dispositivo está solicitando acesso a essa conta.",
|
||||
"loginSelectAuthenticationMethod": "Selecione um método de autenticação para continuar.",
|
||||
"noData": "Nenhum dado encontrado",
|
||||
"machineClients": "Clientes de máquina",
|
||||
"install": "Instale",
|
||||
"downloadInstaller": "Baixar Instalador",
|
||||
"run": "Executar",
|
||||
"envFile": "Arquivo de Ambiente",
|
||||
"serviceFile": "Arquivo de Serviço",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Versão do Kernel",
|
||||
"deviceModel": "Modelo do dispositivo",
|
||||
"serialNumber": "Número de Série",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Nome do Host",
|
||||
"firstSeen": "Visto primeiro",
|
||||
"lastSeen": "Visto por último",
|
||||
"biometricsEnabled": "Biometria habilitada",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Desconectado",
|
||||
"approvalsEmptyStateTitle": "Aprovações do dispositivo não habilitado",
|
||||
"approvalsEmptyStateDescription": "Habilitar aprovações do dispositivo para cargos que exigem aprovação do administrador antes que os usuários possam conectar novos dispositivos.",
|
||||
"approvalsEmptyStateHowToTitle": "Como Habilitar",
|
||||
"approvalsEmptyStateStep1Title": "Ir para Funções",
|
||||
"approvalsEmptyStateStep1Description": "Navegue até as configurações dos papéis da sua organização para configurar as aprovações de dispositivo.",
|
||||
"approvalsEmptyStateStep2Title": "Habilitar Aprovações do Dispositivo",
|
||||
@@ -4114,7 +4140,7 @@
|
||||
"healthCheckTabConnection": "Conexão",
|
||||
"healthCheckTabAdvanced": "Avançado",
|
||||
"healthCheckStrategyNotAvailable": "Esta estratégia não está disponível. Por favor, contacte vendas para ativar esta funcionalidade.",
|
||||
"uptime30d": "Uptime (30d)",
|
||||
"uptime30d": "Tempo de atividade (30d)",
|
||||
"idpAddActionCreateNew": "Criar novo provedor de identidade",
|
||||
"idpAddActionImportFromOrg": "Importar de outra organização",
|
||||
"idpImportDialogTitle": "Importar Provedor de Identidade",
|
||||
@@ -4134,7 +4160,7 @@
|
||||
"idpUnassociatedDescription": "Provedor de identidade desassociado desta organização com sucesso",
|
||||
"idpUnassociateMenu": "Desassociar",
|
||||
"idpDeleteAllOrgsMenu": "Excluir",
|
||||
"publicIpEndpoint": "Endpoint",
|
||||
"publicIpEndpoint": "Ponto de extremidade",
|
||||
"lastTriggeredAt": "Último Gatilho",
|
||||
"reject": "Rejeitar",
|
||||
"uptimeDaysAgo": "há {count} dias",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Visualizar como Administrador",
|
||||
"resourceLauncherResourceDetailsDescription": "Informações de conexão e status para este recurso.",
|
||||
"resourceLauncherResourceDetails": "Detalhes do Recurso",
|
||||
"resourceLauncherSitesDescription": "O recurso está acessível através dos seguintes sites.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Visualizar Site como Administrador",
|
||||
"resourceLauncherFilterBySite": "Filtrar por Site",
|
||||
"resourceLauncherSshCommand": "Comando SSH",
|
||||
"resourceLauncherSshCommandDescription": "Use o CLI do Pangolin para abrir uma sessão SSH para este recurso.",
|
||||
"resourceLauncherAuthMethodsDescription": "Métodos de autenticação habilitados para este recurso.",
|
||||
"resourceLauncherPrivateClientRequired": "Conecte com um cliente no seu dispositivo para acessar este recurso privadamente.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Conexão do Cliente Obrigatória",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Modo de teclado Unicode",
|
||||
"sessionToolbarShow": "Mostrar barra de ferramentas",
|
||||
"sessionToolbarHide": "Ocultar barra de ferramentas",
|
||||
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site"
|
||||
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site",
|
||||
"check": "Verificar"
|
||||
}
|
||||
|
||||
+63
-31
@@ -51,7 +51,7 @@
|
||||
"inviteNotAccepted": "Приглашение не принято",
|
||||
"authCreateAccount": "Создайте учётную запись для начала работы",
|
||||
"authNoAccount": "Нет учётной записи?",
|
||||
"email": "Email",
|
||||
"email": "Электронная почта",
|
||||
"password": "Пароль",
|
||||
"confirmPassword": "Подтвердите пароль",
|
||||
"createAccount": "Создать учётную запись",
|
||||
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Цели на этом сайте",
|
||||
"siteSetting": "Настройки {siteName}",
|
||||
"siteNewtTunnel": "Новый сайт (рекомендуется)",
|
||||
"pangolinSite": "Сайт Pangolin",
|
||||
"siteNewtTunnelDescription": "Самый простой способ создать точку входа в любую сеть. Дополнительная настройка не требуется.",
|
||||
"siteWg": "Базовый WireGuard",
|
||||
"siteWgDescription": "Используйте любой клиент WireGuard для открытия туннеля. Требуется ручная настройка NAT.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "Удаление ключа API",
|
||||
"apiKeysManage": "Управление ключами API",
|
||||
"apiKeysDescription": "Ключи API используются для аутентификации в интеграционном API",
|
||||
"orgsManage": "Управление организациями",
|
||||
"orgsDescription": "Просмотр и управление всеми организациями в этой инстанции",
|
||||
"provisioningKeysTitle": "Ключ подготовки",
|
||||
"provisioningKeysManage": "Управление ключами подготовки",
|
||||
"provisioningKeysDescription": "Ключи подготовки используются для аутентификации автоматического обеспечения сайта для вашей организации.",
|
||||
@@ -552,7 +555,7 @@
|
||||
"licenseBannerDescription": "Откройте доступ к корпоративным функциям для вашей локально размещаемой версии Pangolin. Приобретите лицензионный ключ, чтобы активировать премиум-функции, затем добавьте его ниже.",
|
||||
"licenseBannerGetLicense": "Получить лицензию",
|
||||
"licenseBannerViewDocs": "Посмотреть документацию",
|
||||
"communityEdition": "Community Edition",
|
||||
"communityEdition": "Издание для сообщества",
|
||||
"licenseAboutDescription": "Это для бизнес и корпоративных пользователей, использующих Pangolin в коммерческой среде. Если вы используете Pangolin для личного использования, вы можете игнорировать этот раздел.",
|
||||
"licenseKeyActivated": "Лицензионный ключ активирован",
|
||||
"licenseKeyActivatedDescription": "Лицензионный ключ был успешно активирован.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "Имя (необязательно)",
|
||||
"accessControls": "Контроль доступа",
|
||||
"userDescription2": "Управление настройками этого пользователя",
|
||||
"userGeneralSettingsDescription": "Управление ролями и настройками этого пользователя в организации",
|
||||
"accessRoleErrorAdd": "Не удалось добавить пользователя в роль",
|
||||
"accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.",
|
||||
"userSaved": "Пользователь сохранён",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "Неверное имя TLS сервера. Используйте формат доменного имени или оставьте пустым для удаления имени TLS сервера.",
|
||||
"proxyEnableSSL": "Включить TLS",
|
||||
"proxyEnableSSLDescription": "Включить шифрование SSL/TLS для безопасных HTTPS соединений с целями.",
|
||||
"target": "Target",
|
||||
"target": "Цель",
|
||||
"configureTarget": "Настроить адресаты",
|
||||
"targetErrorFetch": "Не удалось получить цели",
|
||||
"targetErrorFetchDescription": "Произошла ошибка при получении целей",
|
||||
@@ -912,8 +916,8 @@
|
||||
"policyAccessRulesFallthroughOff": "Когда правила отключены, весь трафик проходит для аутентификации.",
|
||||
"policyAccessRulesFallthroughOn": "Когда правило не совпадает, трафик проходит для аутентификации.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesPlaceholderPath": "/админ/*",
|
||||
"rulesPlaceholderGeo": "РУ, КН",
|
||||
"rulesSave": "Сохранить правила",
|
||||
"resourceErrorCreate": "Ошибка при создании ресурса",
|
||||
"resourceErrorCreateDescription": "Произошла ошибка при создании ресурса",
|
||||
@@ -948,7 +952,7 @@
|
||||
"unknownCommand": "Неизвестная команда",
|
||||
"newtErrorFetchReleases": "Не удалось получить информацию о релизе: {err}",
|
||||
"newtErrorFetchLatest": "Ошибка при получении последнего релиза: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtEndpoint": "Конечная точка",
|
||||
"newtId": "ID",
|
||||
"newtSecretKey": "Секретный ключ",
|
||||
"newtVersion": "Версия",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "Сгенерировать код сброса пароля",
|
||||
"passwordResetCodeGenerated": "Код сброса пароля создан",
|
||||
"passwordResetCodeGeneratedDescription": "Поделитесь этим кодом с пользователем. Они могут использовать его для сброса пароля.",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "Сброс URL",
|
||||
"passwordNew": "Новый пароль",
|
||||
"passwordNewConfirm": "Подтвердите новый пароль",
|
||||
"changePassword": "Изменить пароль",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Ошибка при выходе",
|
||||
"signingAs": "Вы вошли как",
|
||||
"serverAdmin": "Администратор сервера",
|
||||
"promoteServerAdmin": "Повысить до администратора сервера",
|
||||
"promoteServerAdminTitle": "Повысить до администратора сервера",
|
||||
"promoteServerAdminQuestion": "Вы уверены, что хотите повысить {selectedUser} до администратора сервера?",
|
||||
"promoteServerAdminMessage": "Администраторы серверов имеют наивысшие привилегии и могут управлять сервером.",
|
||||
"promoteServerAdminWarning": "Это можно отменить в любое время, понизив пользователя.",
|
||||
"promoteServerAdminConfirm": "Повысить до администратора сервера",
|
||||
"promoteServerAdminSuccess": "Пользователь повышен",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} теперь администратор сервера.",
|
||||
"promoteServerAdminError": "Не удалось повысить пользователя",
|
||||
"demoteServerAdmin": "Понизить с администратора сервера",
|
||||
"demoteServerAdminTitle": "Понизить с администратора сервера",
|
||||
"demoteServerAdminQuestion": "Вы уверены, что хотите понизить {selectedUser} с администратора сервера?",
|
||||
"demoteServerAdminMessage": "{selectedUser} потеряет все привилегии администратора сервера.",
|
||||
"demoteServerAdminWarning": "Это можно отменить в любое время, повысив пользователя.",
|
||||
"demoteServerAdminConfirm": "Понизить с администратора сервера",
|
||||
"demoteServerAdminSuccess": "Пользователь понижен",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} больше не является администратором сервера.",
|
||||
"demoteServerAdminError": "Не удалось понизить пользователя",
|
||||
"managedSelfhosted": "Управляемый с самовывоза",
|
||||
"otpEnable": "Включить Двухфакторную Аутентификацию",
|
||||
"otpDisable": "Отключить двухфакторную аутентификацию",
|
||||
@@ -1854,9 +1876,9 @@
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Шлюз Vercel AI",
|
||||
"aiProviderTypeCustom": "Пользовательский",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||
"aiProviderTypeOpenaiDescription": "API OpenAI",
|
||||
"aiProviderTypeAnthropicDescription": "API Anthropic",
|
||||
"aiProviderTypeGoogleGeminiDescription": "API Google Gemini",
|
||||
"aiProviderTypeVertexAiDescription": "API Google Vertex AI",
|
||||
"aiProviderTypeBedrockDescription": "API Amazon Bedrock Runtime",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "API Microsoft Foundry",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Бюджет",
|
||||
"resourceBudgetSettingsDescription": "Настройте, как этот шлюз AI ограничивает использование на основе затрат или лимитов токенов",
|
||||
"sidebarApiKeys": "API ключи",
|
||||
"sidebarOrgs": "Организации",
|
||||
"sidebarProvisioning": "Подготовка",
|
||||
"sidebarSettings": "Настройки",
|
||||
"sidebarAllUsers": "Все пользователи",
|
||||
@@ -2357,7 +2380,7 @@
|
||||
"containerLabels": "Метки",
|
||||
"containerLabelsCount": "{count, plural, one {# метка} few {# метки} many {# меток} other {# меток}}",
|
||||
"containerLabelsTitle": "Метки контейнера",
|
||||
"containerLabelEmpty": "<empty>",
|
||||
"containerLabelEmpty": "<пусто>",
|
||||
"containerPorts": "Порты",
|
||||
"containerPortsMore": "+{count} ещё",
|
||||
"containerActions": "Действия",
|
||||
@@ -2424,7 +2447,7 @@
|
||||
"billing": "Выставление счетов",
|
||||
"orgBillingDescription": "Управление платежной информацией и подписками",
|
||||
"github": "GitHub",
|
||||
"pangolinHosted": "Pangolin Hosted",
|
||||
"pangolinHosted": "Панголин Hosted",
|
||||
"fossorial": "Fossorial",
|
||||
"completeAccountSetup": "Завершите настройку аккаунта",
|
||||
"completeAccountSetupDescription": "Установите ваш пароль, чтобы начать",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "Запустите Olm на вашей системе",
|
||||
"clientOlmCredentials": "Полномочия",
|
||||
"clientOlmCredentialsDescription": "Именно так клиент будет аутентифицироваться с сервером",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "Конечная точка",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "Секретный ключ",
|
||||
"clientCredentialsSave": "Сохранить учетные данные",
|
||||
@@ -2848,7 +2871,7 @@
|
||||
"resourcesTableNoProxyResourcesFound": "Проксированных ресурсов не найдено.",
|
||||
"resourcesTableNoInternalResourcesFound": "Частные ресурсы не найдены.",
|
||||
"resourcesTableDestination": "Пункт назначения",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "Псевдоним",
|
||||
"resourcesTableAliasAddress": "Псевдоним адреса",
|
||||
"resourcesTableAliasAddressInfo": "Этот адрес является частью вспомогательной подсети организации. Он используется для разрешения псевдонимов с использованием внутреннего разрешения DNS.",
|
||||
"resourcesTableClients": "Клиенты",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "IP или адрес хоста ресурса в сети сайта.",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "Псевдоним",
|
||||
"editInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
|
||||
"createInternalResourceDialogNoSitesAvailable": "Нет доступных сайтов",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "Вам необходимо иметь хотя бы один сайт Newt с настроенной подсетью для создания частных ресурсов.",
|
||||
@@ -2908,7 +2931,7 @@
|
||||
"privateResourceNetworkAccess": "Сетевой доступ",
|
||||
"privateResourceNetworkAccessDescription": "Управляйте доступом к портам TCP/UDP и настройте разрешение ICMP ping для данного ресурса.",
|
||||
"hostSettings": "Хост",
|
||||
"cidrSettings": "CIDR",
|
||||
"cidrSettings": "СИДР",
|
||||
"createInternalResourceDialogResourceProperties": "Свойства ресурса",
|
||||
"createInternalResourceDialogName": "Имя",
|
||||
"createInternalResourceDialogSite": "Сайт",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "Пункт назначения",
|
||||
"createInternalResourceDialogDestinationHostDescription": "IP адрес или имя хоста ресурса в сети сайта.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "Диапазон CIDR ресурса в сети сайта.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "Псевдоним",
|
||||
"createInternalResourceDialogAliasDescription": "Дополнительный внутренний DNS псевдоним для этого ресурса.",
|
||||
"internalResourceAliasLocalWarning": "Псевдонимы, оканчивающиеся на .local, могут вызывать проблемы с разрешением из-за mDNS в некоторых сетях.",
|
||||
"internalResourceDownstreamSchemeRequired": "Схема обязательна для HTTP ресурсов",
|
||||
@@ -3163,12 +3186,12 @@
|
||||
"roleMappingRemoveRule": "Удалить",
|
||||
"idpGoogleConfiguration": "Конфигурация Google",
|
||||
"idpGoogleConfigurationDescription": "Настройка учетных данных Google OAuth2",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "Ваш Google OAuth2 ID клиента",
|
||||
"idpGoogleClientSecretDescription": "Секрет клиента Google OAuth2",
|
||||
"idpAzureConfiguration": "Конфигурация Azure Entra ID",
|
||||
"idpAzureConfigurationDescription": "Настройка учетных данных Azure Entra ID OAuth2",
|
||||
"idpTenantId": "Идентификатор арендатора",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "идентификатор арендатора",
|
||||
"idpAzureTenantIdDescription": "ID арендатора Azure (найден в обзоре Active Directory Azure)",
|
||||
"idpAzureClientIdDescription": "Регистрационный номер клиента Azure App",
|
||||
"idpAzureClientSecretDescription": "Секрет регистрации клиента Azure App",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "Регистрационный номер клиента Azure App",
|
||||
"idpAzureClientSecretDescription2": "Секрет регистрации клиента Azure App",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC провайдер",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC провайдер",
|
||||
"subnet": "Подсеть",
|
||||
"utilitySubnet": "Утилита подсети",
|
||||
"subnetDescription": "Подсеть для конфигурации сети этой организации.",
|
||||
@@ -3415,7 +3438,7 @@
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Вы используете Pangolin для личного или делового использования?",
|
||||
"firstName": "First Name",
|
||||
"firstName": "Имя",
|
||||
"lastName": "Фамилия",
|
||||
"jobTitle": "Заголовок",
|
||||
"primaryUseQuestion": "Что вы планируете использовать Панголин в первую очередь?",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Приоритет",
|
||||
"priorityDescription": "Маршруты с более высоким приоритетом оцениваются первым. Приоритет = 100 означает автоматическое упорядочение (решение системы). Используйте другой номер для обеспечения ручного приоритета.",
|
||||
"instanceName": "Имя экземпляра",
|
||||
"clearInstanceName": "Сбросить ассоциацию сервера",
|
||||
"pathMatchModalTitle": "Настроить соответствие пути",
|
||||
"pathMatchModalDescription": "Настройка соответствия входящих запросов на основе их пути.",
|
||||
"pathMatchType": "Тип совпадения",
|
||||
@@ -3501,7 +3525,7 @@
|
||||
"pathRewriteStripPrefixOption": "Префикс вырезать - Удалить префикс",
|
||||
"pathRewriteValue": "Перезаписать значение",
|
||||
"pathRewriteRegexPlaceholder": "/new/$1",
|
||||
"pathRewriteDefaultPlaceholder": "/new-path",
|
||||
"pathRewriteDefaultPlaceholder": "/новый-путь",
|
||||
"pathRewritePrefixHelp": "Заменить соответствующий префикс этим значением",
|
||||
"pathRewriteExactHelp": "Замените весь путь этим значением, когда путь точно соответствует",
|
||||
"pathRewriteRegexHelp": "Использовать группы захвата типа $1, $2 для замены",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "Разрешено правилом",
|
||||
"allowedNoAuth": "Разрешено без авторизации",
|
||||
"validAccessToken": "Действительный маркер доступа",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "Действительная аутентификация заголовка",
|
||||
"validPincode": "Верный PIN-код",
|
||||
"validPassword": "Допустимый пароль",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "Действительный email",
|
||||
"validSSO": "Действительное SSO",
|
||||
"validVirtualAPIKey": "Действительный виртуальный API ключ",
|
||||
"view": "Просмотр",
|
||||
"configManaged": "Конфигурация управляется",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "Отброшено по правилам",
|
||||
"noSessions": "Нет сессий",
|
||||
"temporaryRequestToken": "Временный токен запроса",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "Нет действительных методов аутентификации",
|
||||
"ip": "IP",
|
||||
"reason": "Причина",
|
||||
"requestLogs": "HTTP Запросы Логи",
|
||||
@@ -3750,7 +3774,7 @@
|
||||
"regenerateCredentialsWarning": "Восстановление учётных данных приведет к аннулированию предыдущих учетных данных и отключению соединения. Убедитесь, что все конфигурации, использующие эти учетные данные.",
|
||||
"confirm": "Подтвердить",
|
||||
"regenerateCredentialsConfirmation": "Вы уверены, что хотите восстановить учетные данные?",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Конечная точка",
|
||||
"Id": "Id",
|
||||
"SecretKey": "Секретный ключ",
|
||||
"niceId": "Неплохой ID",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "Произошла ошибка при обновлении Nice ID.",
|
||||
"niceIdCannotBeEmpty": "Неправильный ID не может быть пустым",
|
||||
"enterIdentifier": "Введите идентификатор",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "Идентификатор",
|
||||
"deviceLoginUseDifferentAccount": "Не вы? Используйте другую учетную запись.",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "Устройство запрашивает доступ к этой учетной записи.",
|
||||
"loginSelectAuthenticationMethod": "Выберите метод аутентификации для продолжения.",
|
||||
"noData": "Нет данных",
|
||||
"machineClients": "Машинные клиенты",
|
||||
"install": "Установить",
|
||||
"downloadInstaller": "Скачать установщик",
|
||||
"run": "Запустить",
|
||||
"envFile": "Файл окружения",
|
||||
"serviceFile": "Сервисный файл",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "Версия ядра",
|
||||
"deviceModel": "Модель устройства",
|
||||
"serialNumber": "Серийный номер",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "Имя хоста",
|
||||
"firstSeen": "Первый раз виден",
|
||||
"lastSeen": "Последнее посещение",
|
||||
"biometricsEnabled": "Включены биометрические данные",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Отключено",
|
||||
"approvalsEmptyStateTitle": "Утверждения устройства не включены",
|
||||
"approvalsEmptyStateDescription": "Включите одобрение ролей для того, чтобы пользователи могли подключать новые устройства.",
|
||||
"approvalsEmptyStateHowToTitle": "Как включить",
|
||||
"approvalsEmptyStateStep1Title": "Перейти к ролям",
|
||||
"approvalsEmptyStateStep1Description": "Перейдите в настройки ролей вашей организации для настройки утверждений устройств.",
|
||||
"approvalsEmptyStateStep2Title": "Включить утверждения устройства",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Просмотр как администратор",
|
||||
"resourceLauncherResourceDetailsDescription": "Информация о подключении и статус для данного ресурса.",
|
||||
"resourceLauncherResourceDetails": "Детали ресурса",
|
||||
"resourceLauncherSitesDescription": "Ресурс доступен на следующих сайтах.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Просмотр сайта как администратор",
|
||||
"resourceLauncherFilterBySite": "Фильтр по сайту",
|
||||
"resourceLauncherSshCommand": "SSH Команда",
|
||||
"resourceLauncherSshCommandDescription": "Используйте Pangolin CLI для открытия SSH-сессии к этому ресурсу.",
|
||||
"resourceLauncherAuthMethodsDescription": "Методы аутентификации, включенные для этого ресурса.",
|
||||
"resourceLauncherPrivateClientRequired": "Подключитесь с клиентом на устройстве для доступа к этому ресурсу в частном порядке.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Требуется подключение клиента",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode",
|
||||
"sessionToolbarShow": "Показать панель инструментов",
|
||||
"sessionToolbarHide": "Скрыть панель инструментов",
|
||||
"actionUpdateSiteApprovals": "Обновить утверждения сайта"
|
||||
"actionUpdateSiteApprovals": "Обновить утверждения сайта",
|
||||
"check": "Проверить"
|
||||
}
|
||||
|
||||
+37
-5
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "Bu sitedeki hedefler",
|
||||
"siteSetting": "{siteName} Ayarları",
|
||||
"siteNewtTunnel": "Newt Site (Önerilen)",
|
||||
"pangolinSite": "Pangolin Sitesi",
|
||||
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
|
||||
"siteWg": "Temel WireGuard",
|
||||
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "API Anahtarını Sil",
|
||||
"apiKeysManage": "API Anahtarlarını Yönet",
|
||||
"apiKeysDescription": "API anahtarları entegrasyon API'sini doğrulamak için kullanılır",
|
||||
"orgsManage": "Örgütleri Yönet",
|
||||
"orgsDescription": "Bu örnekteki tüm örgütleri görüntüle ve yönet",
|
||||
"provisioningKeysTitle": "Tedarik Anahtarı",
|
||||
"provisioningKeysManage": "Tedarik Anahtarlarını Yönet",
|
||||
"provisioningKeysDescription": "Tedarik anahtarları, organizasyonunuz için otomatik site sağlama işlemini doğrulamak için kullanılır.",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "İsim (İsteğe Bağlı)",
|
||||
"accessControls": "Erişim Kontrolleri",
|
||||
"userDescription2": "Bu kullanıcı üzerindeki ayarları yönetin",
|
||||
"userGeneralSettingsDescription": "Bu kullanıcının örgütteki rollerini ve ayarlarını yönetin",
|
||||
"accessRoleErrorAdd": "Kullanıcıyı role ekleme başarısız oldu",
|
||||
"accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.",
|
||||
"userSaved": "Kullanıcı kaydedildi",
|
||||
@@ -758,7 +762,7 @@
|
||||
"proxyUpdatedDescription": "Proxy ayarları başarıyla güncellendi",
|
||||
"proxyErrorUpdate": "Proxy ayarları güncellenemedi",
|
||||
"proxyErrorUpdateDescription": "Proxy ayarlarını güncellerken bir hata oluştu",
|
||||
"targetAddr": "Host",
|
||||
"targetAddr": "Sunucu",
|
||||
"targetPort": "Bağlantı Noktası",
|
||||
"targetProtocol": "Protokol",
|
||||
"targetTlsSettings": "HTTPS & TLS Settings",
|
||||
@@ -876,7 +880,7 @@
|
||||
"policyAuthOrLogicBanner": "Ziyaretçiler aşağıdaki etkin yöntemlerden herhangi birini kullanarak kimlik doğrulaması yapabilirler. Hepsini tamamlamaları gerekmez.",
|
||||
"policyAuthMethodActive": "Etkin",
|
||||
"policyAuthMethodOff": "Kapalı",
|
||||
"policyAuthSsoTitle": "Platform SSO",
|
||||
"policyAuthSsoTitle": "Platform SSO'sunu Kullanın",
|
||||
"policyAuthSsoDescription": "Organizasyonunuzun kimlik sağlayıcısı üzerinden oturum açmayı zorunlu kılın",
|
||||
"policyAuthInferenceIdentityKeySignInUrl": "Giriş URL'si",
|
||||
"policyAuthInferenceIdentityKeyHelpNoUrl": "Her kullanıcının zaten bir kimlik API anahtarı vardır, bu nedenle yalnızca kullanıcı olmayan istemciler veya ortak erişim için sanal API anahtarları oluşturmanız gerekir. Kullanıcılar bu kaynağın URL'si üzerinden kimlik sağlayıcılarıyla oturum açarak anahtarlarını alabilirler, bu girişten sonra gösterilecektir.",
|
||||
@@ -912,7 +916,7 @@
|
||||
"policyAccessRulesFallthroughOff": "Kurallar devre dışı bırakıldığında, tüm trafik kimlik doğrulamasına geçer.",
|
||||
"policyAccessRulesFallthroughOn": "Herhangi bir kural eşleşmediğinde trafik kimlik doğrulamasına geçer.",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderPath": "/yönetici/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesSave": "Kuralları Kaydet",
|
||||
"resourceErrorCreate": "Kaynak oluşturma hatası",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "Çıkış yaparken hata",
|
||||
"signingAs": "Olarak giriş yapıldı",
|
||||
"serverAdmin": "Sunucu Yöneticisi",
|
||||
"promoteServerAdmin": "Sunucu yöneticisine terfi et",
|
||||
"promoteServerAdminTitle": "Sunucu Yöneticisine Terfi",
|
||||
"promoteServerAdminQuestion": "{selectedUser} adresini sunucu yöneticisi olarak terfi etmek istediğinizden emin misiniz?",
|
||||
"promoteServerAdminMessage": "Sunucu yöneticileri en yüksek ayrıcalıklara sahiptir ve sunucuyu yönetebilir.",
|
||||
"promoteServerAdminWarning": "Bu, kullanıcıyı indirerek herhangi bir zamanda geri alınabilir.",
|
||||
"promoteServerAdminConfirm": "Sunucu Yöneticisine Terfi",
|
||||
"promoteServerAdminSuccess": "Kullanıcı Terfi Edildi",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} artık bir sunucu yöneticisi.",
|
||||
"promoteServerAdminError": "Kullanıcıyı terfi ettirme başarısız oldu",
|
||||
"demoteServerAdmin": "Sunucu yöneticisinden negatif terfi et",
|
||||
"demoteServerAdminTitle": "Sunucu Yöneticisinden Negatif Terfi",
|
||||
"demoteServerAdminQuestion": "{selectedUser} adresini sunucu yöneticiliğinden indirmek istediğinizden emin misiniz?",
|
||||
"demoteServerAdminMessage": "{selectedUser} tüm sunucu yönetici ayrıcalıklarını kaybedecek.",
|
||||
"demoteServerAdminWarning": "Bu, kullanıcıyı terfi ettirerek herhangi bir zamanda geri alınabilir.",
|
||||
"demoteServerAdminConfirm": "Sunucu yöneticisinden negatif terfi et",
|
||||
"demoteServerAdminSuccess": "Kullanıcı indirildi",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} artık bir sunucu yöneticisi değil.",
|
||||
"demoteServerAdminError": "Kullanıcıyı indirirken başarısız oldu",
|
||||
"managedSelfhosted": "Yönetilen Self-Hosted",
|
||||
"otpEnable": "İki faktörlü özelliğini etkinleştir",
|
||||
"otpDisable": "İki faktörlü özelliğini devre dışı bırak",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "Bütçe",
|
||||
"resourceBudgetSettingsDescription": "Bu AI ağ geçidi harcama veya jeton limitlerine göre kullanım nasıl sınırlayacağını yapılandırın",
|
||||
"sidebarApiKeys": "API Anahtarları",
|
||||
"sidebarOrgs": "Organizasyonlar",
|
||||
"sidebarProvisioning": "Tedarik",
|
||||
"sidebarSettings": "Ayarlar",
|
||||
"sidebarAllUsers": "Tüm Kullanıcılar",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "Öncelik",
|
||||
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
|
||||
"instanceName": "Örnek İsmi",
|
||||
"clearInstanceName": "Sunucu Birliği Sıfırla",
|
||||
"pathMatchModalTitle": "Yol Eşleşmesini Yapılandır",
|
||||
"pathMatchModalDescription": "Gelen isteklerin yolu temel alarak nasıl eşleştirilmesi gerektiğini ayarlayın.",
|
||||
"pathMatchType": "Eşleşme Türü",
|
||||
@@ -3488,7 +3512,7 @@
|
||||
"clear": "Temizle",
|
||||
"saveChanges": "Değişiklikleri Kaydet",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchDefaultPlaceholder": "/yol",
|
||||
"pathMatchPrefixHelp": "Örnek: /api, /api/users vb.'ni eşleştirir.",
|
||||
"pathMatchExactHelp": "Örnek: /api yalnızca /api'yi eşleştirir",
|
||||
"pathMatchRegexHelp": "Örnek: ^/api/.* her şeyi eşleştirir /api/anything",
|
||||
@@ -3767,6 +3791,7 @@
|
||||
"noData": "Veri Yok",
|
||||
"machineClients": "Makine İstemcileri",
|
||||
"install": "Yükle",
|
||||
"downloadInstaller": "Yükleyiciyi İndir",
|
||||
"run": "Çalıştır",
|
||||
"envFile": "Ortam Dosyası",
|
||||
"serviceFile": "Servis Dosyası",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "Bağlantı Kesildi",
|
||||
"approvalsEmptyStateTitle": "Cihaz Onayları Etkin Değil",
|
||||
"approvalsEmptyStateDescription": "Kullanıcıların yeni cihazlara bağlanabilmeleri için yönetici onayı gerektiren rol cihaz onaylarını etkinleştirin.",
|
||||
"approvalsEmptyStateHowToTitle": "Nasıl Etkinleştirilir",
|
||||
"approvalsEmptyStateStep1Title": "Rollere Git",
|
||||
"approvalsEmptyStateStep1Description": "Cihaz onaylarını yapılandırmak için kuruluşunuzun rol ayarlarına gidin.",
|
||||
"approvalsEmptyStateStep2Title": "Cihaz Onaylarını Etkinleştir",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "Yönetici Olarak Görüntüle",
|
||||
"resourceLauncherResourceDetailsDescription": "Bu kaynağın bağlantı bilgileri ve durumu.",
|
||||
"resourceLauncherResourceDetails": "Kaynak Detayları",
|
||||
"resourceLauncherSitesDescription": "Kaynak, aşağıdaki siteler üzerinden erişilebilir.",
|
||||
"resourceLauncherViewSiteAsAdmin": "Siteyi Yönetici Olarak Görüntüle",
|
||||
"resourceLauncherFilterBySite": "Siteye Göre Filtrele",
|
||||
"resourceLauncherSshCommand": "SSH Komutu",
|
||||
"resourceLauncherSshCommandDescription": "Pangolin CLI kullanarak bu kaynağa bir SSH oturumu açın.",
|
||||
"resourceLauncherAuthMethodsDescription": "Bu kaynak için etkin kimlik doğrulama yöntemleri.",
|
||||
"resourceLauncherPrivateClientRequired": "Bu kaynağa özel olarak erişmek için cihazınızda bir istemci ile bağlanın.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "İstemci Bağlantısı Gerekli",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode klavye modu",
|
||||
"sessionToolbarShow": "Araç çubuğunu göster",
|
||||
"sessionToolbarHide": "Araç çubuğunu gizle",
|
||||
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle"
|
||||
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle",
|
||||
"check": "Kontrol Et"
|
||||
}
|
||||
|
||||
+77
-45
@@ -153,6 +153,7 @@
|
||||
"siteResourcesTargetsOnSite": "此站点上的目标",
|
||||
"siteSetting": "{siteName} 设置",
|
||||
"siteNewtTunnel": "新节点 (推荐)",
|
||||
"pangolinSite": "Pangolin 站点",
|
||||
"siteNewtTunnelDescription": "最简单的方式来创建任何网络的入口。没有额外的设置。",
|
||||
"siteWg": "基本 WireGuard",
|
||||
"siteWgDescription": "使用任何 WireGuard 客户端来建立隧道。需要手动配置 NAT。",
|
||||
@@ -465,6 +466,8 @@
|
||||
"apiKeysDelete": "删除 API 密钥",
|
||||
"apiKeysManage": "管理 API 密钥",
|
||||
"apiKeysDescription": "API 密钥用于认证集成 API",
|
||||
"orgsManage": "管理组织",
|
||||
"orgsDescription": "查看和管理此实例中的所有组织",
|
||||
"provisioningKeysTitle": "预配密钥",
|
||||
"provisioningKeysManage": "管理预配密钥",
|
||||
"provisioningKeysDescription": "置备密钥用于验证您组织的自动站点配置。",
|
||||
@@ -716,6 +719,7 @@
|
||||
"nameOptional": "名称(可选)",
|
||||
"accessControls": "访问控制",
|
||||
"userDescription2": "管理此用户的设置",
|
||||
"userGeneralSettingsDescription": "管理此用户在组织中的角色和设置",
|
||||
"accessRoleErrorAdd": "添加用户到角色失败",
|
||||
"accessRoleErrorAddDescription": "添加用户到角色时出错。",
|
||||
"userSaved": "用户已保存",
|
||||
@@ -736,7 +740,7 @@
|
||||
"proxyErrorTls": "无效的 TLS 服务器名称。使用域名格式,或保存空以删除 TLS 服务器名称。",
|
||||
"proxyEnableSSL": "启用 TLS",
|
||||
"proxyEnableSSLDescription": "启用 SSL/TLS 加密以确保目标的 HTTPS 连接。",
|
||||
"target": "Target",
|
||||
"target": "目标",
|
||||
"configureTarget": "配置目标",
|
||||
"targetErrorFetch": "获取目标失败",
|
||||
"targetErrorFetchDescription": "获取目标时出错",
|
||||
@@ -912,8 +916,8 @@
|
||||
"policyAccessRulesFallthroughOff": "禁用规则后,所有流量将通过身份验证。",
|
||||
"policyAccessRulesFallthroughOn": "没有规则匹配时,流量将通过身份验证。",
|
||||
"rulesPlaceholderCidr": "10.0.0.0/8",
|
||||
"rulesPlaceholderPath": "/admin/*",
|
||||
"rulesPlaceholderGeo": "RU, KP",
|
||||
"rulesPlaceholderPath": "/管理员/*",
|
||||
"rulesPlaceholderGeo": "俄罗斯, 朝鲜",
|
||||
"rulesSave": "保存规则",
|
||||
"resourceErrorCreate": "创建资源时出错",
|
||||
"resourceErrorCreateDescription": "创建资源时出错",
|
||||
@@ -948,8 +952,8 @@
|
||||
"unknownCommand": "未知命令",
|
||||
"newtErrorFetchReleases": "无法获取版本信息: {err}",
|
||||
"newtErrorFetchLatest": "无法获取最新版信息: {err}",
|
||||
"newtEndpoint": "Endpoint",
|
||||
"newtId": "ID",
|
||||
"newtEndpoint": "终端",
|
||||
"newtId": "身份ID",
|
||||
"newtSecretKey": "密钥",
|
||||
"newtVersion": "版本",
|
||||
"architecture": "架构",
|
||||
@@ -1154,7 +1158,7 @@
|
||||
"idpAutoProvisionUsers": "自动提供用户",
|
||||
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
||||
"idpAutoProvisionConfigureAfterCreate": "您可以在创建身份提供者后配置自动配置设置。",
|
||||
"licenseBadge": "EE",
|
||||
"licenseBadge": "企业版",
|
||||
"idpType": "提供者类型",
|
||||
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
||||
"idpOidcConfigure": "OAuth2/OIDC 配置",
|
||||
@@ -1311,7 +1315,7 @@
|
||||
"generatePasswordResetCode": "生成密码重置代码",
|
||||
"passwordResetCodeGenerated": "密码重置代码已生成",
|
||||
"passwordResetCodeGeneratedDescription": "与用户分享此代码。他们可以用它来重置他们的密码。",
|
||||
"passwordResetUrl": "Reset URL",
|
||||
"passwordResetUrl": "重置网址",
|
||||
"passwordNew": "新密码",
|
||||
"passwordNewConfirm": "确认新密码",
|
||||
"changePassword": "更改密码",
|
||||
@@ -1424,6 +1428,24 @@
|
||||
"logoutError": "注销错误",
|
||||
"signingAs": "登录为",
|
||||
"serverAdmin": "服务器管理",
|
||||
"promoteServerAdmin": "晋升为服务器管理员",
|
||||
"promoteServerAdminTitle": "晋升为服务器管理员",
|
||||
"promoteServerAdminQuestion": "您确定要将 {selectedUser} 晋升为服务器管理员吗?",
|
||||
"promoteServerAdminMessage": "服务器管理员拥有最高权限,可以管理服务器。",
|
||||
"promoteServerAdminWarning": "可以随时通过降级用户来撤销此操作。",
|
||||
"promoteServerAdminConfirm": "晋升为服务器管理员",
|
||||
"promoteServerAdminSuccess": "用户已晋升",
|
||||
"promoteServerAdminSuccessDescription": "{selectedUser} 现在是服务器管理员。",
|
||||
"promoteServerAdminError": "无法晋升用户",
|
||||
"demoteServerAdmin": "降级为服务器管理员",
|
||||
"demoteServerAdminTitle": "降级为服务器管理员",
|
||||
"demoteServerAdminQuestion": "您确定要将 {selectedUser} 从服务器管理员降级吗?",
|
||||
"demoteServerAdminMessage": "{selectedUser} 将失去所有服务器管理员权限。",
|
||||
"demoteServerAdminWarning": "可以随时通过晋升用户来撤销此操作。",
|
||||
"demoteServerAdminConfirm": "降级为服务器管理员",
|
||||
"demoteServerAdminSuccess": "用户已降级",
|
||||
"demoteServerAdminSuccessDescription": "{selectedUser} 不再是服务器管理员。",
|
||||
"demoteServerAdminError": "无法降级用户",
|
||||
"managedSelfhosted": "托管自托管",
|
||||
"otpEnable": "启用双因子认证",
|
||||
"otpDisable": "禁用双因子认证",
|
||||
@@ -1797,7 +1819,7 @@
|
||||
"aiClientConfigTabManual": "手动配置",
|
||||
"aiClientConfigPreset": "配置",
|
||||
"aiClientConfigStep": "步骤 {number}",
|
||||
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
|
||||
"aiClientConfigEndpointPlaceholder": "https://示例.资源.url.com",
|
||||
"aiClientConfigPlaceholderWarning": "此片段包含示例值,如模型名称或资源 URL。在使用前代替它们为您自己的值。",
|
||||
"aiClientConfigRevealError": "无法加载您的 API 密钥。",
|
||||
"aiClientConfigRevealRetry": "再试一次",
|
||||
@@ -1847,21 +1869,21 @@
|
||||
"aiProviderTypeNotFound": "未找到提供者类型",
|
||||
"aiProviderTypeOpenai": "OpenAI",
|
||||
"aiProviderTypeAnthropic": "Anthropic",
|
||||
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||
"aiProviderTypeGoogleGemini": "谷歌 Gemini",
|
||||
"aiProviderTypeVertexAi": "Vertex AI",
|
||||
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||
"aiProviderTypeMicrosoftFoundry": "微软车间",
|
||||
"aiProviderTypeOpenRouter": "开放路由器",
|
||||
"aiProviderTypeVercelAiGateway": "Vercel AI 网关",
|
||||
"aiProviderTypeCustom": "自定义",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||
"aiProviderTypeOpenaiDescription": "OpenAI 接口",
|
||||
"aiProviderTypeAnthropicDescription": "Anthropic 接口",
|
||||
"aiProviderTypeGoogleGeminiDescription": "Google Gemini 接口",
|
||||
"aiProviderTypeVertexAiDescription": "Google Vertex AI 接口",
|
||||
"aiProviderTypeBedrockDescription": "Amazon Bedrock 运行时 API",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry 接口",
|
||||
"aiProviderTypeOpenRouterDescription": "OpenRouter 接口",
|
||||
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI 网关接口",
|
||||
"aiProviderTypeCustomDescription": "使用您自己的端点或通过站点目标进行路由",
|
||||
"aiProviderUpstreamUrl": "上游URL",
|
||||
"aiProviderUpstreamUrlDescription": "提供商API的基础URL",
|
||||
@@ -1882,7 +1904,7 @@
|
||||
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key头部。由Google Gemini使用",
|
||||
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||
"aiProviderAuthTypeHecDescription": "授权:Splunk密钥。由Splunk HTTP事件收集器使用",
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI 网关",
|
||||
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization:Bearer key。由Cloudflare AI Gateway使用",
|
||||
"aiProviderAuthTypeNone": "无身份验证",
|
||||
"aiProviderAuthTypePassthrough": "透传",
|
||||
@@ -2095,6 +2117,7 @@
|
||||
"resourceBudgetSettings": "预算",
|
||||
"resourceBudgetSettingsDescription": "配置此 AI 网关如何根据支出或令牌限制来限制使用",
|
||||
"sidebarApiKeys": "API密钥",
|
||||
"sidebarOrgs": "组织",
|
||||
"sidebarProvisioning": "预配",
|
||||
"sidebarSettings": "设置",
|
||||
"sidebarAllUsers": "所有用户",
|
||||
@@ -2682,7 +2705,7 @@
|
||||
"clientInstallOlmDescription": "在您的系统上运行 Olm",
|
||||
"clientOlmCredentials": "全权证书",
|
||||
"clientOlmCredentialsDescription": "这是客户端如何通过服务器进行身份验证",
|
||||
"olmEndpoint": "Endpoint",
|
||||
"olmEndpoint": "终端",
|
||||
"olmId": "ID",
|
||||
"olmSecretKey": "密钥",
|
||||
"clientCredentialsSave": "保存证书",
|
||||
@@ -2843,12 +2866,12 @@
|
||||
"resourceEditDomain": "编辑域名",
|
||||
"siteName": "站点名称",
|
||||
"proxyPort": "端口",
|
||||
"resourcesTableProxyResources": "",
|
||||
"resourcesTableProxyResources": "公开资源",
|
||||
"resourcesTableClientResources": "私有资源",
|
||||
"resourcesTableNoProxyResourcesFound": "未找到代理资源。",
|
||||
"resourcesTableNoInternalResourcesFound": "未找到私有资源。",
|
||||
"resourcesTableDestination": "目标",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAlias": "别名",
|
||||
"resourcesTableAliasAddress": "别名地址",
|
||||
"resourcesTableAliasAddressInfo": "此地址是组织实用子网的一部分。它用来使用内部DNS解析来解析别名记录。",
|
||||
"resourcesTableClients": "客户端",
|
||||
@@ -2895,7 +2918,7 @@
|
||||
"editInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。",
|
||||
"editInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP或主机名地址。",
|
||||
"editInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
|
||||
"editInternalResourceDialogAlias": "Alias",
|
||||
"editInternalResourceDialogAlias": "别名",
|
||||
"editInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
|
||||
"createInternalResourceDialogNoSitesAvailable": "暂无可用站点",
|
||||
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一个子网的Newt站点来创建私有资源。",
|
||||
@@ -2954,7 +2977,7 @@
|
||||
"createInternalResourceDialogDestination": "目标",
|
||||
"createInternalResourceDialogDestinationHostDescription": "站点网络上资源的 IP 地址或主机名。",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "站点网络上资源的 CIDR 范围。",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAlias": "别名",
|
||||
"createInternalResourceDialogAliasDescription": "此资源可选的内部DNS别名。",
|
||||
"internalResourceAliasLocalWarning": "以 .local 结尾的别名可能会因某些网络上的 mDNS 而导致解析问题。",
|
||||
"internalResourceDownstreamSchemeRequired": "HTTP 资源需要方案",
|
||||
@@ -3073,7 +3096,7 @@
|
||||
"regionMiddleAfrica": "中东",
|
||||
"regionSouthernAfrica": "D. 南 非",
|
||||
"regionWesternAfrica": "D. 西部非洲",
|
||||
"regionAmericas": "Americas",
|
||||
"regionAmericas": "美洲",
|
||||
"regionCaribbean": "加勒比",
|
||||
"regionCentralAmerica": "中美洲:",
|
||||
"regionSouthAmerica": "南 非",
|
||||
@@ -3089,11 +3112,11 @@
|
||||
"regionNorthernEurope": "北欧洲",
|
||||
"regionSouthernEurope": "南欧洲",
|
||||
"regionWesternEurope": "西欧洲",
|
||||
"regionOceania": "Oceania",
|
||||
"regionOceania": "大洋洲",
|
||||
"regionAustraliaAndNewZealand": "澳大利亚和新西兰",
|
||||
"regionMelanesia": "Melanesia",
|
||||
"regionMicronesia": "Micronesia",
|
||||
"regionPolynesia": "Polynesia",
|
||||
"regionMelanesia": "美拉尼西亚",
|
||||
"regionMicronesia": "密克罗尼西亚",
|
||||
"regionPolynesia": "玻里尼西亚",
|
||||
"managedSelfHosted": {
|
||||
"title": "托管自托管",
|
||||
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
|
||||
@@ -3163,17 +3186,17 @@
|
||||
"roleMappingRemoveRule": "删除",
|
||||
"idpGoogleConfiguration": "Google 配置",
|
||||
"idpGoogleConfigurationDescription": "配置 Google OAuth2 凭据",
|
||||
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
|
||||
"idpGoogleClientIdDescription": "您的 Google OAuth2 客户端 ID",
|
||||
"idpGoogleClientSecretDescription": "Google OAuth2 客户端密钥",
|
||||
"idpAzureConfiguration": "Azure Entra ID 配置",
|
||||
"idpAzureConfigurationDescription": "配置 Azure Entra ID OAuth2 凭据",
|
||||
"idpTenantId": "租户 ID",
|
||||
"idpTenantIdPlaceholder": "tenant-id",
|
||||
"idpTenantIdPlaceholder": "租户 ID",
|
||||
"idpAzureTenantIdDescription": "Azure 租户ID (在 Azure Active Directory 概览中找到)",
|
||||
"idpAzureClientIdDescription": "Azure 应用注册客户端 ID",
|
||||
"idpAzureClientSecretDescription": "Azure 应用程序注册客户端密钥",
|
||||
"idpGoogleTitle": "谷歌",
|
||||
"idpGoogleAlt": "Google",
|
||||
"idpGoogleAlt": "谷歌",
|
||||
"idpAzureTitle": "Azure Entra ID",
|
||||
"idpAzureAlt": "Azure",
|
||||
"idpGoogleConfigurationTitle": "Google 配置",
|
||||
@@ -3182,7 +3205,7 @@
|
||||
"idpAzureClientIdDescription2": "Azure 应用注册客户端 ID",
|
||||
"idpAzureClientSecretDescription2": "Azure 应用程序注册客户端密钥",
|
||||
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供商",
|
||||
"subnet": "子网",
|
||||
"utilitySubnet": "实用程序子网",
|
||||
"subnetDescription": "此组织网络配置的子网。",
|
||||
@@ -3348,7 +3371,7 @@
|
||||
"manageMachineClientsDescription": "创建和管理服务器和系统用于私密连接到资源的客户端",
|
||||
"machineClientsBannerTitle": "服务器与自动化系统",
|
||||
"machineClientsBannerDescription": "机器客户端适用于不与特定用户关联的服务器与自动化系统。它们使用ID和密钥进行身份验证,并可以与Pangolin CLI、Olm CLI或作为容器运行。",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerPangolinCLI": "邦戈林 CLI",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Olm 容器",
|
||||
"clientsTableUserClients": "用户",
|
||||
@@ -3478,6 +3501,7 @@
|
||||
"priority": "优先权",
|
||||
"priorityDescription": "先评估更高优先级线路。优先级 = 100意味着自动排序(系统决定). 使用另一个数字强制执行手动优先级。",
|
||||
"instanceName": "实例名称",
|
||||
"clearInstanceName": "重置服务器关联",
|
||||
"pathMatchModalTitle": "配置路径匹配",
|
||||
"pathMatchModalDescription": "根据传入请求的路径设置匹配方式。",
|
||||
"pathMatchType": "匹配类型",
|
||||
@@ -3533,11 +3557,11 @@
|
||||
"allowedByRule": "根据规则允许",
|
||||
"allowedNoAuth": "无认证",
|
||||
"validAccessToken": "有效访问令牌",
|
||||
"validHeaderAuth": "Valid header auth",
|
||||
"validPincode": "Valid Pincode",
|
||||
"validHeaderAuth": "有效的头部认证",
|
||||
"validPincode": "有效的 PIN 码",
|
||||
"validPassword": "有效密码",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"validEmail": "有效的邮箱",
|
||||
"validSSO": "有效的 SSO",
|
||||
"validVirtualAPIKey": "有效虚拟 API 密钥",
|
||||
"view": "查看",
|
||||
"configManaged": "配置已管理",
|
||||
@@ -3546,7 +3570,7 @@
|
||||
"droppedByRule": "被规则删除",
|
||||
"noSessions": "无会话",
|
||||
"temporaryRequestToken": "临时请求令牌",
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"noMoreAuthMethods": "没有有效的认证",
|
||||
"ip": "IP",
|
||||
"reason": "原因",
|
||||
"requestLogs": "请求日志",
|
||||
@@ -3750,8 +3774,8 @@
|
||||
"regenerateCredentialsWarning": "重新生成凭据将使以前的凭据失效并导致断开连接。请确保更新使用这些凭据的任何配置。",
|
||||
"confirm": "确认",
|
||||
"regenerateCredentialsConfirmation": "您确定要重新生成凭据吗?",
|
||||
"endpoint": "Endpoint",
|
||||
"Id": "Id",
|
||||
"endpoint": "终端",
|
||||
"Id": "ID",
|
||||
"SecretKey": "秘密密钥",
|
||||
"niceId": "好的 ID",
|
||||
"niceIdUpdated": "好的 ID 已更新",
|
||||
@@ -3760,13 +3784,14 @@
|
||||
"niceIdUpdateErrorDescription": "更新Nice ID时出错。",
|
||||
"niceIdCannotBeEmpty": "好的 ID 不能为空",
|
||||
"enterIdentifier": "输入标识符",
|
||||
"identifier": "Identifier",
|
||||
"identifier": "标识符",
|
||||
"deviceLoginUseDifferentAccount": "不是你?使用一个不同的帐户。",
|
||||
"deviceLoginDeviceRequestingAccessToAccount": "设备正在请求访问此帐户。",
|
||||
"loginSelectAuthenticationMethod": "选择要继续的身份验证方法。",
|
||||
"noData": "无数据",
|
||||
"machineClients": "机器客户端",
|
||||
"install": "安装",
|
||||
"downloadInstaller": "下载安装程序",
|
||||
"run": "运行",
|
||||
"envFile": "环境文件",
|
||||
"serviceFile": "服务文件",
|
||||
@@ -3944,7 +3969,7 @@
|
||||
"kernelVersion": "内核版本",
|
||||
"deviceModel": "设备模型",
|
||||
"serialNumber": "序列号",
|
||||
"hostname": "Hostname",
|
||||
"hostname": "主机名",
|
||||
"firstSeen": "第一次查看",
|
||||
"lastSeen": "上次查看时间",
|
||||
"biometricsEnabled": "生物计已启用",
|
||||
@@ -3974,6 +3999,7 @@
|
||||
"disconnected": "断开连接",
|
||||
"approvalsEmptyStateTitle": "设备批准未启用",
|
||||
"approvalsEmptyStateDescription": "在用户连接新设备之前,允许设备批准角色,需要管理员批准。",
|
||||
"approvalsEmptyStateHowToTitle": "如何启用",
|
||||
"approvalsEmptyStateStep1Title": "转到角色",
|
||||
"approvalsEmptyStateStep1Description": "导航到您组织的角色设置来配置设备批准。",
|
||||
"approvalsEmptyStateStep2Title": "启用设备批准",
|
||||
@@ -4253,6 +4279,11 @@
|
||||
"resourceLauncherViewAsAdmin": "以管理员身份查看",
|
||||
"resourceLauncherResourceDetailsDescription": "此资源的连接信息和状态。",
|
||||
"resourceLauncherResourceDetails": "资源详情",
|
||||
"resourceLauncherSitesDescription": "该资源可通过以下网站访问。",
|
||||
"resourceLauncherViewSiteAsAdmin": "以管理员身份查看网站",
|
||||
"resourceLauncherFilterBySite": "按网站过滤",
|
||||
"resourceLauncherSshCommand": "SSH命令",
|
||||
"resourceLauncherSshCommandDescription": "使用Pangolin CLI来打开该资源的SSH会话。",
|
||||
"resourceLauncherAuthMethodsDescription": "此资源启用的身份验证方法。",
|
||||
"resourceLauncherPrivateClientRequired": "请在您的设备上连接客户端以私密访问此资源。",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "需要客户端连接",
|
||||
@@ -4363,5 +4394,6 @@
|
||||
"rdpUnicodeKeyboardMode": "Unicode 键盘模式",
|
||||
"sessionToolbarShow": "显示工具栏",
|
||||
"sessionToolbarHide": "隐藏工具栏",
|
||||
"actionUpdateSiteApprovals": "更新站点审批"
|
||||
"actionUpdateSiteApprovals": "更新站点审批",
|
||||
"check": "检查"
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ const nextConfig: NextConfig = {
|
||||
source: "/:orgId/settings/resources/client/:path*",
|
||||
destination: "/:orgId/settings/resources/private/:path*",
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: "/:orgId/settings/access/users/:userId/access-controls",
|
||||
destination: "/:orgId/settings/access/users/:userId/general",
|
||||
permanent: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Generated
+121
-172
@@ -50,6 +50,7 @@
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"acme-client": "^5.4.0",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.20.0",
|
||||
"better-sqlite3": "11.9.1",
|
||||
@@ -61,6 +62,7 @@
|
||||
"cors": "2.8.6",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"dns-packet": "^5.6.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.7.0",
|
||||
@@ -73,6 +75,7 @@
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "5.4.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lru-cache": "11.5.2",
|
||||
"lucide-react": "1.38.0",
|
||||
"maxmind": "5.0.7",
|
||||
"moment": "2.30.1",
|
||||
@@ -80,7 +83,6 @@
|
||||
"next-intl": "4.14.1",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "9.1.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.23.0",
|
||||
@@ -124,6 +126,7 @@
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/d3": "7.4.3",
|
||||
"@types/dns-packet": "^5.6.5",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/express-session": "1.19.0",
|
||||
"@types/jmespath": "0.15.2",
|
||||
@@ -907,14 +910,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz",
|
||||
"integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
@@ -929,9 +930,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2445,6 +2446,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@levischuck/tiny-cbor": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
|
||||
@@ -2487,6 +2494,29 @@
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime/node_modules/@emnapi/core": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
||||
"integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.3.tgz",
|
||||
@@ -2840,6 +2870,35 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-wasm32-wasi": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz",
|
||||
"integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^0.45.0",
|
||||
"@emnapi/runtime": "^0.45.0",
|
||||
"@tybys/wasm-util": "^0.8.1",
|
||||
"memfs-browser": "^3.4.13000"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
|
||||
"integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/argon2-win32-arm64-msvc": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.2.0.tgz",
|
||||
@@ -3102,16 +3161,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz",
|
||||
"integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
|
||||
@@ -3122,16 +3171,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz",
|
||||
"integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-rs/bcrypt-win32-arm64-msvc": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz",
|
||||
@@ -6181,72 +6220,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
|
||||
@@ -6411,10 +6384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz",
|
||||
"integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6764,6 +6736,16 @@
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dns-packet": {
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.6.5.tgz",
|
||||
"integrity": "sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
@@ -7435,6 +7417,22 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acme-client": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/acme-client/-/acme-client-5.4.0.tgz",
|
||||
"integrity": "sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.7.2",
|
||||
"debug": "^4.3.5",
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -8272,15 +8270,6 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -9261,6 +9250,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dns-packet": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
|
||||
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -12590,9 +12591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -13099,18 +13100,6 @@
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-cache": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
|
||||
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone": "2.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-exports-info": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
|
||||
@@ -13140,6 +13129,15 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
|
||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.54",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||
@@ -13391,26 +13389,6 @@
|
||||
"@node-rs/bcrypt": "1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@emnapi/core": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz",
|
||||
"integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@emnapi/runtime": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
|
||||
"integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@node-rs/argon2": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz",
|
||||
@@ -13602,25 +13580,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz",
|
||||
"integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^0.45.0",
|
||||
"@emnapi/runtime": "^0.45.0",
|
||||
"@tybys/wasm-util": "^0.8.1",
|
||||
"memfs-browser": "^3.4.13000"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz",
|
||||
@@ -13669,16 +13628,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/oslo/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz",
|
||||
"integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
|
||||
+4
-1
@@ -73,6 +73,7 @@
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"acme-client": "^5.4.0",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.20.0",
|
||||
"better-sqlite3": "11.9.1",
|
||||
@@ -84,6 +85,7 @@
|
||||
"cors": "2.8.6",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"dns-packet": "^5.6.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.7.0",
|
||||
@@ -96,6 +98,7 @@
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "5.4.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lru-cache": "11.5.2",
|
||||
"lucide-react": "1.38.0",
|
||||
"maxmind": "5.0.7",
|
||||
"moment": "2.30.1",
|
||||
@@ -103,7 +106,6 @@
|
||||
"next-intl": "4.14.1",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"nodemailer": "9.1.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.23.0",
|
||||
@@ -147,6 +149,7 @@
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/d3": "7.4.3",
|
||||
"@types/dns-packet": "^5.6.5",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/express-session": "1.19.0",
|
||||
"@types/jmespath": "0.15.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 800 KiB After Width: | Height: | Size: 802 KiB |
@@ -0,0 +1,8 @@
|
||||
export async function startCertificateManager() {
|
||||
// No-op: ACME certificate generation/management is only available in
|
||||
// builds that include the private/enterprise feature set.
|
||||
}
|
||||
|
||||
export async function stopCertificateManager() {
|
||||
// No-op counterpart to startCertificateManager.
|
||||
}
|
||||
@@ -682,6 +682,8 @@ export const newts = pgTable(
|
||||
secretHash: varchar("secretHash").notNull(),
|
||||
dateCreated: varchar("dateCreated").notNull(),
|
||||
version: varchar("version"),
|
||||
agent: varchar("agent"), // either newt or cli
|
||||
agentVersion: varchar("agentVersion"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
|
||||
@@ -703,6 +703,8 @@ export const newts = sqliteTable(
|
||||
secretHash: text("secretHash").notNull(),
|
||||
dateCreated: text("dateCreated").notNull(),
|
||||
version: text("version"),
|
||||
agent: text("agent"), // either newt or cli
|
||||
agentVersion: text("agentVersion"),
|
||||
siteId: integer("siteId").references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export async function startDnsServer() {
|
||||
// No-op: the authoritative DNS server is only available in builds
|
||||
// that include the private/enterprise feature set.
|
||||
}
|
||||
|
||||
export async function stopDnsServer() {
|
||||
// No-op counterpart to startDnsServer.
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import { setHostMeta } from "@server/lib/hostMeta";
|
||||
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import { startSchedulers } from "#dynamic/startSchedulers";
|
||||
import { startDnsServer } from "#dynamic/dns";
|
||||
import { startCertificateManager } from "#dynamic/certificates";
|
||||
import license from "#dynamic/license/license";
|
||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
|
||||
@@ -45,6 +47,10 @@ async function startServers() {
|
||||
|
||||
startSchedulers();
|
||||
|
||||
await startDnsServer();
|
||||
|
||||
await startCertificateManager();
|
||||
|
||||
// Start all servers
|
||||
const apiServer = createApiServer();
|
||||
const internalServer = createInternalServer();
|
||||
|
||||
+2
-8
@@ -1,13 +1,7 @@
|
||||
import NodeCache from "node-cache";
|
||||
import logger from "@server/logger";
|
||||
import { createLocalCache } from "@server/lib/createLocalCache";
|
||||
|
||||
// Create local cache with maxKeys limit to prevent memory leaks
|
||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||
export const localCache = new NodeCache({
|
||||
stdTTL: 3600,
|
||||
checkperiod: 120,
|
||||
maxKeys: 10000
|
||||
});
|
||||
export const localCache = createLocalCache();
|
||||
|
||||
// Log cache statistics periodically for monitoring
|
||||
// setInterval(() => {
|
||||
|
||||
@@ -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.22.0";
|
||||
export const APP_VERSION = "1.23.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
const DEFAULT_MAX_KEYS = 10000;
|
||||
const DEFAULT_TTL_SECONDS = 3600;
|
||||
|
||||
export type LocalCache = {
|
||||
get<T>(key: string): T | undefined;
|
||||
set(key: string, value: unknown, ttlSeconds?: number): boolean;
|
||||
del(key: string | string[]): number;
|
||||
has(key: string): boolean;
|
||||
keys(): string[];
|
||||
flushAll(): void;
|
||||
getStats(): { keys: number };
|
||||
getTtl(key: string): number | undefined;
|
||||
};
|
||||
|
||||
export function createLocalCache(
|
||||
max = DEFAULT_MAX_KEYS,
|
||||
ttlSeconds = DEFAULT_TTL_SECONDS
|
||||
): LocalCache {
|
||||
const lru = new LRUCache<string, {}>({
|
||||
max,
|
||||
ttl: ttlSeconds * 1000,
|
||||
updateAgeOnGet: false
|
||||
});
|
||||
|
||||
return {
|
||||
get<T>(key: string): T | undefined {
|
||||
return lru.get(key) as T | undefined;
|
||||
},
|
||||
|
||||
set(key: string, value: unknown, ttlSeconds?: number): boolean {
|
||||
const stored = value as {};
|
||||
if (ttlSeconds === undefined) {
|
||||
lru.set(key, stored);
|
||||
} else if (ttlSeconds === 0) {
|
||||
lru.set(key, stored, { ttl: 0 });
|
||||
} else {
|
||||
lru.set(key, stored, { ttl: ttlSeconds * 1000 });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
del(key: string | string[]): number {
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
let deleted = 0;
|
||||
for (const k of keys) {
|
||||
if (lru.delete(k)) {
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
},
|
||||
|
||||
has(key: string): boolean {
|
||||
return lru.has(key);
|
||||
},
|
||||
|
||||
keys(): string[] {
|
||||
return [...lru.keys()];
|
||||
},
|
||||
|
||||
flushAll(): void {
|
||||
lru.clear();
|
||||
},
|
||||
|
||||
getStats(): { keys: number } {
|
||||
return { keys: lru.size };
|
||||
},
|
||||
|
||||
getTtl(key: string): number | undefined {
|
||||
if (!lru.has(key)) {
|
||||
return undefined;
|
||||
}
|
||||
const remaining = lru.getRemainingTTL(key);
|
||||
if (!Number.isFinite(remaining)) {
|
||||
return 0;
|
||||
}
|
||||
return Date.now() + remaining;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export async function withRetry<T>(
|
||||
const jitter = Math.random() * baseDelay;
|
||||
const delay = baseDelay + jitter;
|
||||
logger.warn(
|
||||
`Transient DB error in ${context}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`,
|
||||
`Transient DB issue in ${context}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`,
|
||||
{ code: error?.code ?? error?.cause?.code }
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export function createCname(
|
||||
domainId: string,
|
||||
baseDomain: string
|
||||
): { baseDomain: string; value: string }[] {
|
||||
throw new Error("Creating CNAME records is not supported in this build");
|
||||
}
|
||||
|
||||
export function createNs(): string[] {
|
||||
throw new Error("Creating NS records is not supported in this build");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Tracks, per process lifetime, whether a given exit node has ever checked in
|
||||
// (called /gerbil/get-config) since this Pangolin instance started. This lets
|
||||
// callers distinguish "gerbil hasn't come up yet" (expected briefly after a
|
||||
// restart, since gerbil depends on pangolin's container starting first) from
|
||||
// "gerbil was reachable and now isn't" (a real problem worth an error log).
|
||||
const checkedInExitNodeIds = new Set<number>();
|
||||
|
||||
export function markExitNodeCheckedIn(exitNodeId: number): void {
|
||||
checkedInExitNodeIds.add(exitNodeId);
|
||||
}
|
||||
|
||||
export function hasExitNodeCheckedIn(exitNodeId: number): boolean {
|
||||
return checkedInExitNodeIds.has(exitNodeId);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from "axios";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNode } from "@server/db";
|
||||
import { hasExitNodeCheckedIn } from "./exitNodeCheckIn";
|
||||
|
||||
interface ExitNodeRequest {
|
||||
remoteType?: string;
|
||||
@@ -72,13 +73,19 @@ export async function sendToExitNode(
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
`Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||
);
|
||||
const message = axios.isAxiosError(error)
|
||||
? `Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||
: `Error making ${method} request for exit node at ${exitNode.reachableAt}: ${error}`;
|
||||
|
||||
// The exit node (gerbil) may still be starting up and not yet
|
||||
// reachable. Until it has checked in at least once, log this at a
|
||||
// lower level since it's expected; once it has checked in, a
|
||||
// connection failure is a real problem.
|
||||
if (hasExitNodeCheckedIn(exitNode.exitNodeId)) {
|
||||
logger.error(message);
|
||||
} else {
|
||||
logger.error(
|
||||
`Error making ${method} request for exit node at ${exitNode.reachableAt}: ${error}`
|
||||
logger.warn(
|
||||
`${message} (exit node has not checked in yet since startup, this is expected briefly)`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./exitNodes";
|
||||
export * from "./exitNodeComms";
|
||||
export * from "./exitNodeCheckIn";
|
||||
export * from "./subnet";
|
||||
export * from "./getCurrentExitNodeId";
|
||||
export * from "./calculateExitNodeWeight";
|
||||
|
||||
@@ -348,8 +348,8 @@ export const configSchema = z
|
||||
.optional()
|
||||
.pipe(z.string())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
subnet_group: z.string().optional().default("100.89.137.0/20"),
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
subnet_group: z.string().optional().default("100.89.137.0/18"),
|
||||
block_size: z.number().positive().gt(0).optional().default(22),
|
||||
site_block_size: z
|
||||
.number()
|
||||
.positive()
|
||||
@@ -493,23 +493,6 @@ export const configSchema = z
|
||||
.prefault({})
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
dns: z
|
||||
.object({
|
||||
nameservers: z
|
||||
.array(z.string().optional().optional())
|
||||
.optional()
|
||||
.default([
|
||||
"ns1.pangolin.net",
|
||||
"ns2.pangolin.net",
|
||||
"ns3.pangolin.net"
|
||||
]),
|
||||
cname_extension: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("cname.pangolin.net")
|
||||
})
|
||||
.optional()
|
||||
.prefault({})
|
||||
})
|
||||
.refine(
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
retries?: number;
|
||||
baseDelayMs?: number;
|
||||
label?: string;
|
||||
// Called with each caught error to decide whether it's worth
|
||||
// retrying. Defaults to retrying everything (existing behavior) -
|
||||
// pass this to exclude errors that are known to be permanent (e.g.
|
||||
// an upstream rate limit or validation rejection) rather than
|
||||
// transient, so they fail fast instead of wasting retry attempts.
|
||||
shouldRetry?: (error: unknown) => boolean;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
retries = 3,
|
||||
baseDelayMs = 250,
|
||||
label = "operation",
|
||||
shouldRetry = () => true
|
||||
} = options;
|
||||
|
||||
let attempt = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
attempt++;
|
||||
if (attempt > retries || !shouldRetry(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Exponential backoff with jitter so retries don't all land at once.
|
||||
const delay =
|
||||
baseDelayMs * 2 ** (attempt - 1) * (0.5 + Math.random());
|
||||
|
||||
logger.warn(
|
||||
`${label} failed (attempt ${attempt}/${retries + 1}), retrying in ${delay.toFixed(0)}ms`,
|
||||
error
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds an operation that has no timeout of its own (e.g. acme-client's
|
||||
// axios instance never sets one, so a stalled TCP connection to the ACME
|
||||
// server hangs forever instead of erroring). Without this, a single hung
|
||||
// call can leave its caller's promise permanently unsettled - fatal for
|
||||
// code that gates future work on that promise resolving, like the
|
||||
// scheduler's runExclusive() waiting on a batch's Promise.all.
|
||||
export async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
ms: number,
|
||||
label = "operation"
|
||||
): Promise<T> {
|
||||
let timer: NodeJS.Timeout;
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
||||
ms
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([promise, timeout]);
|
||||
} finally {
|
||||
clearTimeout(timer!);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import * as yaml from "js-yaml";
|
||||
import axios from "axios";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||
import {
|
||||
getCurrentExitNodeId,
|
||||
hasExitNodeCheckedIn
|
||||
} from "@server/lib/exitNodes";
|
||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
@@ -341,10 +344,6 @@ export class TraefikConfigManager {
|
||||
|
||||
const { domains, traefikConfig } = getTraefikConfig;
|
||||
|
||||
// Add static domains from config
|
||||
// const staticDomains = [config.getRawConfig().app.dashboard_url];
|
||||
// staticDomains.forEach((domain) => domains.add(domain));
|
||||
|
||||
// Log if domains changed
|
||||
if (
|
||||
this.lastActiveDomains.size !== domains.size ||
|
||||
@@ -358,7 +357,7 @@ export class TraefikConfigManager {
|
||||
this.lastActiveDomains = new Set(domains);
|
||||
}
|
||||
|
||||
if (process.env.USE_PANGOLIN_DNS === "true" && build != "oss") {
|
||||
if (process.env.CERT_MODE === "pangolin" && build != "oss") {
|
||||
// Scan current local certificate state
|
||||
this.lastLocalCertificateState =
|
||||
await this.scanLocalCertificateState();
|
||||
@@ -439,13 +438,13 @@ export class TraefikConfigManager {
|
||||
// Always ensure all existing certificates (including wildcards) are in the config
|
||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||
} else {
|
||||
const timeSinceLastFetch = this.lastCertificateFetch
|
||||
? Math.round(
|
||||
(Date.now() -
|
||||
this.lastCertificateFetch.getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: 0;
|
||||
// const timeSinceLastFetch = this.lastCertificateFetch
|
||||
// ? Math.round(
|
||||
// (Date.now() -
|
||||
// this.lastCertificateFetch.getTime()) /
|
||||
// (1000 * 60)
|
||||
// )
|
||||
// : 0;
|
||||
|
||||
// logger.debug(
|
||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||
@@ -466,32 +465,51 @@ export class TraefikConfigManager {
|
||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||
|
||||
// Send domains to SNI proxy
|
||||
let exitNodeForSni: typeof exitNodes.$inferSelect | undefined;
|
||||
try {
|
||||
let exitNode;
|
||||
if (config.getRawConfig().gerbil.exit_node_name) {
|
||||
const exitNodeName =
|
||||
config.getRawConfig().gerbil.exit_node_name!;
|
||||
[exitNode] = await db
|
||||
[exitNodeForSni] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.name, exitNodeName))
|
||||
.limit(1);
|
||||
} else {
|
||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||
[exitNodeForSni] = await db
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.limit(1);
|
||||
}
|
||||
if (exitNode) {
|
||||
await sendToExitNode(exitNode, {
|
||||
if (exitNodeForSni) {
|
||||
await sendToExitNode(exitNodeForSni, {
|
||||
localPath: "/update-local-snis",
|
||||
method: "POST",
|
||||
data: { fullDomains: Array.from(domains) }
|
||||
data: {
|
||||
fullDomains: [
|
||||
...Array.from(domains),
|
||||
...config.getRawConfig().traefik.static_domains
|
||||
]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
"No exit node found. Has gerbil registered yet?"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to post domains to SNI proxy:", err);
|
||||
// sendToExitNode already logs the underlying connection
|
||||
// error at the appropriate level (warn before the exit node
|
||||
// has checked in since startup, error after), so avoid
|
||||
// double-logging it as an error here.
|
||||
if (
|
||||
exitNodeForSni &&
|
||||
!hasExitNodeCheckedIn(exitNodeForSni.exitNodeId)
|
||||
) {
|
||||
logger.warn("Failed to post domains to SNI proxy:", err);
|
||||
} else {
|
||||
logger.error("Failed to post domains to SNI proxy:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Update active domains tracking
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/**
|
||||
* Build the Host()/HostRegexp() Traefik rule for a resource's domain.
|
||||
* Wildcard resources match any single subdomain via HostRegexp.
|
||||
*/
|
||||
// Build the Host()/HostRegexp() Traefik rule for a resource's domain.
|
||||
// Wildcard resources match any single subdomain via HostRegexp.
|
||||
export function buildHostRule(
|
||||
fullDomain: string,
|
||||
wildcard?: boolean | null
|
||||
@@ -14,10 +12,8 @@ export function buildHostRule(
|
||||
return `Host(\`${fullDomain}\`)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a path-matching clause to a Traefik rule based on the resource's
|
||||
* configured path and pathMatchType.
|
||||
*/
|
||||
// Append a path-matching clause to a Traefik rule based on the resource's
|
||||
// configured path and pathMatchType.
|
||||
export function appendPathMatch(
|
||||
rule: string,
|
||||
path: string | null | undefined,
|
||||
@@ -40,10 +36,8 @@ export function appendPathMatch(
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the router priority for a resource, favoring an explicit override
|
||||
* and otherwise deriving it from the path match specificity.
|
||||
*/
|
||||
// Compute the router priority for a resource, favoring an explicit override
|
||||
// and otherwise deriving it from the path match specificity.
|
||||
export function computeRoutePriority(
|
||||
priority: number | null | undefined,
|
||||
path: string | null | undefined,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
startCertificateManager,
|
||||
stopCertificateManager
|
||||
} from "./lib/certificates";
|
||||
@@ -20,6 +20,8 @@ import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth"
|
||||
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
|
||||
import { shutdownUsageRecorder } from "@server/lib/aiBudgetEnforcement";
|
||||
import { shutdownAiSessionLogger } from "@server/routers/aiGateway/logAiSession";
|
||||
import { stopDnsServer } from "./dns";
|
||||
import { stopCertificateManager } from "./certificates";
|
||||
|
||||
async function cleanup() {
|
||||
await stopPingAccumulator();
|
||||
@@ -31,6 +33,8 @@ async function cleanup() {
|
||||
await rateLimitService.cleanup();
|
||||
await wsCleanup();
|
||||
await logStreamingManager.shutdown();
|
||||
await stopDnsServer();
|
||||
await stopCertificateManager();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { AuthoritativeDNSServer } from "#private/lib/dns";
|
||||
import { privateConfig } from "#private/lib/config";
|
||||
|
||||
let dnsServer: AuthoritativeDNSServer | undefined;
|
||||
|
||||
export async function startDnsServer() {
|
||||
const dnsConfig = privateConfig.getRawPrivateConfig().dns;
|
||||
if (!dnsConfig || !dnsConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
dnsServer = new AuthoritativeDNSServer(dnsConfig.listen_port);
|
||||
|
||||
await dnsServer.start();
|
||||
}
|
||||
|
||||
export async function stopDnsServer() {
|
||||
if (!dnsServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
await dnsServer.stop();
|
||||
dnsServer = undefined;
|
||||
}
|
||||
+15
-25
@@ -11,17 +11,11 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import NodeCache from "node-cache";
|
||||
import logger from "@server/logger";
|
||||
import { redisManager, regionalRedisManager } from "@server/private/lib/redis";
|
||||
import { createLocalCache } from "@server/lib/createLocalCache";
|
||||
import { redisManager, regionalRedisManager } from "#private/lib/redis";
|
||||
|
||||
// Create local cache with maxKeys limit to prevent memory leaks
|
||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||
export const localCache = new NodeCache({
|
||||
stdTTL: 3600,
|
||||
checkperiod: 120,
|
||||
maxKeys: 10000
|
||||
});
|
||||
export const localCache = createLocalCache();
|
||||
|
||||
// Log cache statistics periodically for monitoring
|
||||
// setInterval(() => {
|
||||
@@ -97,11 +91,11 @@ class AdaptiveCache {
|
||||
const value = await redisManager.get(key);
|
||||
|
||||
if (value !== null) {
|
||||
logger.debug(`Cache hit in Redis: ${key}`);
|
||||
// logger.debug(`Cache hit in Redis: ${key}`);
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
|
||||
logger.debug(`Cache miss in Redis: ${key}`);
|
||||
// logger.debug(`Cache miss in Redis: ${key}`);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
logger.error(`Redis get error for key ${key}:`, error);
|
||||
@@ -134,7 +128,7 @@ class AdaptiveCache {
|
||||
const success = await redisManager.del(k);
|
||||
if (success) {
|
||||
deletedCount++;
|
||||
logger.debug(`Deleted key from Redis: ${k}`);
|
||||
// logger.debug(`Deleted key from Redis: ${k}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +155,7 @@ class AdaptiveCache {
|
||||
const success = localCache.del(k);
|
||||
if (success > 0) {
|
||||
deletedCount++;
|
||||
logger.debug(`Deleted key from local cache: ${k}`);
|
||||
// logger.debug(`Deleted key from local cache: ${k}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +223,7 @@ class AdaptiveCache {
|
||||
}
|
||||
|
||||
localCache.flushAll();
|
||||
logger.debug("Flushed local cache");
|
||||
// logger.debug("Flushed local cache");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,15 +295,11 @@ export default cache;
|
||||
|
||||
/**
|
||||
* Regional adaptive cache backed by the in-cluster Redis instance.
|
||||
* Falls back to a local NodeCache when the regional Redis is unavailable.
|
||||
* Falls back to a local LRU cache when the regional Redis is unavailable.
|
||||
* Use this for data that is regional in nature (e.g. status history) so
|
||||
* reads are served from the same cluster the user is hitting.
|
||||
*/
|
||||
const regionalLocalCache = new NodeCache({
|
||||
stdTTL: 3600,
|
||||
checkperiod: 120,
|
||||
maxKeys: 10000
|
||||
});
|
||||
const regionalLocalCache = createLocalCache();
|
||||
|
||||
class RegionalAdaptiveCache {
|
||||
private useRedis(): boolean {
|
||||
@@ -332,7 +322,7 @@ class RegionalAdaptiveCache {
|
||||
redisTtl
|
||||
);
|
||||
if (success) {
|
||||
logger.debug(`[regional] Set key in Redis: ${key}`);
|
||||
// logger.debug(`[regional] Set key in Redis: ${key}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -353,10 +343,10 @@ class RegionalAdaptiveCache {
|
||||
try {
|
||||
const value = await regionalRedisManager.get(key);
|
||||
if (value !== null) {
|
||||
logger.debug(`[regional] Cache hit in Redis: ${key}`);
|
||||
// logger.debug(`[regional] Cache hit in Redis: ${key}`);
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
logger.debug(`[regional] Cache miss in Redis: ${key}`);
|
||||
// logger.debug(`[regional] Cache miss in Redis: ${key}`);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -385,7 +375,7 @@ class RegionalAdaptiveCache {
|
||||
const success = await regionalRedisManager.del(k);
|
||||
if (success) {
|
||||
deletedCount++;
|
||||
logger.debug(`[regional] Deleted key from Redis: ${k}`);
|
||||
// logger.debug(`[regional] Deleted key from Redis: ${k}`);
|
||||
}
|
||||
}
|
||||
if (deletedCount === keys.length) return deletedCount;
|
||||
@@ -400,7 +390,7 @@ class RegionalAdaptiveCache {
|
||||
const count = regionalLocalCache.del(k);
|
||||
if (count > 0) {
|
||||
deletedCount++;
|
||||
logger.debug(`[regional] Deleted key from local cache: ${k}`);
|
||||
// logger.debug(`[regional] Deleted key from local cache: ${k}`);
|
||||
}
|
||||
}
|
||||
return deletedCount;
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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 * as acme from "acme-client";
|
||||
import * as fs from "fs";
|
||||
import { eq } from "drizzle-orm/sql";
|
||||
import { privateConfig as config } from "#private/lib/config";
|
||||
import { DnsChallenge, db, dnsChallenge } from "@server/db";
|
||||
import { withRetry } from "@server/lib/retry";
|
||||
import logger from "@server/logger";
|
||||
import { acmeRateLimiter } from "./acmeRateLimiter";
|
||||
|
||||
// acme-client's own retry/backoff logging (429 retries, 5xx retries, each
|
||||
// status-poll tick in waitForValidStatus) is a no-op by default - it only
|
||||
// activates via DEBUG=acme-client or this call, neither of which was wired
|
||||
// up. Without it, a cert silently retrying a Let's Encrypt rate limit for
|
||||
// several minutes is indistinguishable in our logs from one that's actually
|
||||
// hung, since our own logging only wraps the call, not what happens inside
|
||||
// it. Must run before any AcmeClient method is called.
|
||||
acme.setLogger((msg: string) => logger.info(`[acme-client] ${msg}`));
|
||||
|
||||
// acme-client's axios retry wrapper treats any response-less request error
|
||||
// (timeout, connection reset, DNS blip reaching the ACME server) as
|
||||
// retryable, but once its internal retries are exhausted it falls through to
|
||||
// `validateStatus(response)` with `response` still undefined, throwing this
|
||||
// uninformative TypeError instead of the real network error.
|
||||
// https://github.com/publishlab/node-acme-client/blob/master/src/axios.js
|
||||
function isUnresponsiveAcmeError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof TypeError &&
|
||||
error.message ===
|
||||
"Cannot read properties of undefined (reading 'config')"
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAcmeError(error: unknown): Error {
|
||||
if (isUnresponsiveAcmeError(error)) {
|
||||
return new Error(
|
||||
"ACME server did not respond after repeated attempts (network error reaching the ACME endpoint)",
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
|
||||
export class AcmeClientManager {
|
||||
private client: acme.Client | null = null;
|
||||
private accountKey: string | null = null;
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
this.accountKey = await this.loadAccountKey();
|
||||
|
||||
this.client = new acme.Client({
|
||||
directoryUrl: config.getRawConfig().acme!.acme_directory_url,
|
||||
accountKey: this.accountKey
|
||||
});
|
||||
|
||||
// Try to create account or get existing one
|
||||
await this.client.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${config.getRawConfig().acme!.contact_email}`]
|
||||
});
|
||||
|
||||
logger.info("ACME client initialized successfully");
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialize ACME client:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAccountKey(): Promise<string> {
|
||||
const keyPath = config.getRawConfig().acme!.acme_account_key_path;
|
||||
|
||||
if (fs.existsSync(keyPath)) {
|
||||
logger.info("Loading existing account key");
|
||||
return fs.readFileSync(keyPath, "utf8");
|
||||
} else {
|
||||
logger.info("Generating new account key");
|
||||
const privateKey = await acme.crypto.createPrivateKey();
|
||||
const privateKeyString = privateKey.toString();
|
||||
fs.writeFileSync(keyPath, privateKeyString);
|
||||
return privateKeyString;
|
||||
}
|
||||
}
|
||||
|
||||
getClient(): acme.Client {
|
||||
if (!this.client) {
|
||||
throw new Error("ACME client not initialized");
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async createOrder(domain: string, wildcard: boolean = false): Promise<any> {
|
||||
const client = this.getClient();
|
||||
|
||||
const identifiers = wildcard
|
||||
? [
|
||||
{ type: "dns", value: domain },
|
||||
{ type: "dns", value: `*.${domain}` }
|
||||
]
|
||||
: [{ type: "dns", value: domain }];
|
||||
|
||||
await acmeRateLimiter.acquire();
|
||||
const order = await client.createOrder({
|
||||
identifiers
|
||||
});
|
||||
|
||||
if (wildcard) {
|
||||
logger.info(`Created wildcard order for domain: ${domain}`);
|
||||
} else {
|
||||
logger.info(`Created order for domain: ${domain}`);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
async getAuthorizations(order: any): Promise<any[]> {
|
||||
const client = this.getClient();
|
||||
await acmeRateLimiter.acquire();
|
||||
return client.getAuthorizations(order);
|
||||
}
|
||||
|
||||
async handleDnsChallenge(
|
||||
dnsChallenges: {
|
||||
authz: any;
|
||||
challenge: any;
|
||||
}[]
|
||||
): Promise<void> {
|
||||
const client = this.getClient();
|
||||
|
||||
let challengeDomains: DnsChallenge[] = [];
|
||||
|
||||
for (const { authz, challenge } of dnsChallenges) {
|
||||
const keyAuthorization =
|
||||
await client.getChallengeKeyAuthorization(challenge);
|
||||
|
||||
// Extract the domain from authorization
|
||||
const domain = authz.identifier.value;
|
||||
|
||||
// Store challenge in database for DNS server to pick up
|
||||
challengeDomains = await withRetry(
|
||||
() =>
|
||||
db
|
||||
.insert(dnsChallenge)
|
||||
.values({
|
||||
domain: domain,
|
||||
token: challenge.token,
|
||||
keyAuthorization,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: Math.floor(
|
||||
(Date.now() +
|
||||
config.getRawConfig().acme!
|
||||
.challenge_ttl_ms) /
|
||||
1000
|
||||
)
|
||||
})
|
||||
.returning(),
|
||||
{ label: `insert dnsChallenge for domain ${domain}` }
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`DNS challenge stored for domain: ${domain} as token ${challenge.token} and keyAuthorization`
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
const failedDomains: string[] = [];
|
||||
|
||||
for (const { authz, challenge } of dnsChallenges) {
|
||||
const domain = authz.identifier.value;
|
||||
const challengeDomain = `_acme-challenge.${domain}`;
|
||||
|
||||
try {
|
||||
// The ACME server occasionally has a transient network blip
|
||||
// mid-sequence; retry the whole verify/complete/wait sequence
|
||||
// rather than just the DNS challenge propagation wait, since
|
||||
// these calls are safe to repeat against the ACME server.
|
||||
await withRetry(
|
||||
async () => {
|
||||
// Verify challenge
|
||||
await acmeRateLimiter.acquire();
|
||||
await client.verifyChallenge(authz, challenge);
|
||||
|
||||
// Complete challenge
|
||||
logger.info(
|
||||
`Completing challenge for domain: ${challengeDomain}`
|
||||
);
|
||||
await acmeRateLimiter.acquire();
|
||||
await client.completeChallenge(challenge);
|
||||
|
||||
// Wait for validation
|
||||
logger.info(
|
||||
`Waiting for challenge to be validated for domain: ${challengeDomain}...`
|
||||
);
|
||||
await acmeRateLimiter.acquire();
|
||||
await client.waitForValidStatus(challenge);
|
||||
},
|
||||
{
|
||||
retries: 2,
|
||||
baseDelayMs: 5000,
|
||||
label: `ACME challenge completion for domain ${domain}`,
|
||||
// Only retry the known network-blip crash - a
|
||||
// genuine validation failure (e.g. challenge marked
|
||||
// "invalid" because the DNS record wasn't found) is
|
||||
// permanent and should fail immediately instead of
|
||||
// burning Let's Encrypt's per-hostname failed-
|
||||
// validation rate limit on retries that can't help.
|
||||
shouldRetry: isUnresponsiveAcmeError
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(`Challenge completed for domain: ${domain}`);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to complete challenge for domain ${domain}:`,
|
||||
normalizeAcmeError(error)
|
||||
);
|
||||
failedDomains.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
for (const challengeDomain of challengeDomains) {
|
||||
await this.removeDnsChallenge(challengeDomain.dnsChallengeId);
|
||||
logger.info(
|
||||
`Removed DNS challenge for domain: ${challengeDomain.domain}`
|
||||
);
|
||||
}
|
||||
|
||||
// A failed dns-01 challenge leaves the order stuck in "pending" -
|
||||
// finalizing it would just fail with a confusing ACME error, so
|
||||
// stop here and let the caller mark the certificate as failed.
|
||||
if (failedDomains.length > 0) {
|
||||
throw new Error(
|
||||
`DNS-01 challenge validation failed for domain(s): ${failedDomains.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeDnsChallenge(dnsChallengeId: number): Promise<void> {
|
||||
try {
|
||||
await withRetry(
|
||||
() =>
|
||||
db
|
||||
.delete(dnsChallenge)
|
||||
.where(eq(dnsChallenge.dnsChallengeId, dnsChallengeId)),
|
||||
{ label: `delete dnsChallenge ${dnsChallengeId}` }
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to clean up DNS challenge for id ${dnsChallengeId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async finalizeCertificate(
|
||||
order: any,
|
||||
domain: string,
|
||||
wildcard: boolean = false
|
||||
): Promise<{ certificate: string; privateKey: string }> {
|
||||
const client = this.getClient();
|
||||
|
||||
const altNames = wildcard ? [`*.${domain}`, domain] : [domain];
|
||||
|
||||
// Create CSR
|
||||
const [privateKey, csr] = await acme.crypto.createCsr({
|
||||
altNames
|
||||
});
|
||||
|
||||
// Finalize order
|
||||
await acmeRateLimiter.acquire();
|
||||
const finalizedOrder = await client.finalizeOrder(order, csr);
|
||||
|
||||
// Get certificate
|
||||
await acmeRateLimiter.acquire();
|
||||
const certificate = await client.getCertificate(finalizedOrder);
|
||||
|
||||
logger.info(`Certificate obtained for domain: ${domain}`);
|
||||
|
||||
return {
|
||||
certificate: certificate.toString(),
|
||||
privateKey: privateKey.toString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const acmeClientManager = new AcmeClientManager();
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { privateConfig as config } from "#private/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { redis } from "../redis";
|
||||
// Caps outgoing ACME API calls to a fixed budget per wall-clock second,
|
||||
// shared across all pops workers via Redis (mirrors the lockManager pattern
|
||||
// in @lib/lock) - a per-process limiter wouldn't be enough since multiple
|
||||
// workers issue certificates against the same Let's Encrypt account.
|
||||
const ACQUIRE_SCRIPT = `
|
||||
local key = KEYS[1]
|
||||
local limit = tonumber(ARGV[1])
|
||||
local current = redis.call('INCR', key)
|
||||
if current == 1 then
|
||||
redis.call('PEXPIRE', key, 2000)
|
||||
end
|
||||
if current > limit then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
end
|
||||
`;
|
||||
|
||||
class AcmeRateLimiter {
|
||||
async acquire(): Promise<void> {
|
||||
const limit =
|
||||
config.getRawConfig().acme?.acme_requests_per_second ?? 15;
|
||||
|
||||
for (;;) {
|
||||
const bucket = Math.floor(Date.now() / 1000);
|
||||
const key = `acme_rate_limit:${bucket}`;
|
||||
|
||||
let allowed: number;
|
||||
try {
|
||||
allowed = (await redis.eval(
|
||||
ACQUIRE_SCRIPT,
|
||||
1,
|
||||
key,
|
||||
limit.toString()
|
||||
)) as number;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"ACME rate limiter check failed, proceeding without throttling:",
|
||||
error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowed === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Budget for this second is spent - wait for the next window.
|
||||
const waitMs = 1000 - (Date.now() % 1000) + 10;
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const acmeRateLimiter = new AcmeRateLimiter();
|
||||
@@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 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 { acmeClientManager } from "./acme-client";
|
||||
import { dnsValidator } from "./dns-validator";
|
||||
import { getTableColumns } from "drizzle-orm";
|
||||
import { eq, and, or, isNull, lt, asc } from "drizzle-orm/sql";
|
||||
import { config } from "@server/lib/config";
|
||||
import { db, certificates, domains, Certificate } from "@server/db";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { withTimeout, withRetry } from "@server/lib/retry";
|
||||
import logger from "@server/logger";
|
||||
import { lockManager } from "../lock";
|
||||
import { pushCertUpdateToAffectedNewts } from "@server/lib/acmeCertSync";
|
||||
import crypto from "crypto";
|
||||
|
||||
// Number of on-demand DNS validation attempts made right before a
|
||||
// certificate is (re)issued, to avoid burning Let's Encrypt rate limits on
|
||||
// domains whose DNS has drifted since they were last verified.
|
||||
const PRE_CERT_DNS_VALIDATION_ATTEMPTS = 3;
|
||||
|
||||
// Hard ceiling on a single certificate's issuance/renewal flow. acme-client's
|
||||
// axios instance never sets a request timeout, so a stalled connection to
|
||||
// the ACME server hangs forever instead of erroring - and since
|
||||
// processPendingCertificates/processRenewalCandidates gate the *next* batch
|
||||
// on Promise.all(...) over the current one, one hung certificate would
|
||||
// otherwise stall every other domain permanently. Sized generously above the
|
||||
// legitimate worst case (acme-client's own bounded backoff is ~3.6min per
|
||||
// status-polling loop, and a wildcard cert's two identifiers plus order
|
||||
// finalization can chain a few of those) so this only fires on a genuine hang.
|
||||
const CERTIFICATE_ISSUANCE_TIMEOUT_MS = 20 * 60 * 1000;
|
||||
|
||||
// "requested" is set the instant a cert starts processing and is never
|
||||
// queried anywhere else - processPendingCertificates only selects "pending"
|
||||
// and processRenewalCandidates only selects "valid". So if the *process*
|
||||
// dies mid-flight (OOM, node eviction, a rolling deploy) rather than just
|
||||
// hanging, the row is orphaned in "requested" permanently with nothing to
|
||||
// ever pick it back up, no matter how good the in-process timeouts are.
|
||||
// Threshold is set comfortably above CERTIFICATE_ISSUANCE_TIMEOUT_MS plus the
|
||||
// scheduler's own outer backstop so this never reclaims a cert that's still
|
||||
// genuinely being worked on.
|
||||
const STUCK_CERTIFICATE_THRESHOLD_MS = 40 * 60 * 1000;
|
||||
|
||||
export class CertificateService {
|
||||
// Runs at the top of every processPendingCertificates tick so an
|
||||
// interrupted worker's leftovers always get put back in the queue
|
||||
// instead of sitting invisible to every query forever.
|
||||
private async reclaimStuckCertificates(): Promise<void> {
|
||||
const staleBefore =
|
||||
Math.floor(Date.now() / 1000) -
|
||||
Math.floor(STUCK_CERTIFICATE_THRESHOLD_MS / 1000);
|
||||
|
||||
const reclaimed = await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
status: "pending",
|
||||
errorMessage:
|
||||
'Reclaimed after being stuck in "requested" state - the worker processing it likely restarted or crashed',
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "requested"),
|
||||
lt(certificates.updatedAt, staleBefore)
|
||||
)
|
||||
)
|
||||
.returning({ domain: certificates.domain });
|
||||
|
||||
if (reclaimed.length > 0) {
|
||||
logger.warn(
|
||||
`Reclaimed ${reclaimed.length} certificate(s) stuck in "requested" state: ${reclaimed
|
||||
.map((c) => c.domain)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async processPendingCertificates(): Promise<void> {
|
||||
logger.debug("Checking for pending certificates...");
|
||||
|
||||
await this.reclaimStuckCertificates();
|
||||
|
||||
const pendingCerts = await db
|
||||
.select(getTableColumns(certificates))
|
||||
.from(certificates)
|
||||
.leftJoin(domains, eq(certificates.domainId, domains.domainId))
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "pending"),
|
||||
or(
|
||||
// Certs with no linked domain row (e.g. legacy certs
|
||||
// imported from acme.json) aren't gated on domain
|
||||
// verification since there's nothing to check.
|
||||
isNull(certificates.domainId),
|
||||
and(
|
||||
eq(domains.verified, true),
|
||||
eq(domains.failed, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(10);
|
||||
|
||||
if (pendingCerts.length === 0) {
|
||||
logger.debug("No pending certificates found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Found ${pendingCerts.length} pending certificates`);
|
||||
|
||||
// Process the batch concurrently so one domain stuck retrying a slow
|
||||
// DNS-01 challenge (the ACME client's waitForValidStatus can spend
|
||||
// minutes on a bad domain) doesn't stall the rest of the batch.
|
||||
// processSingleCertificate catches its own errors and each cert uses
|
||||
// an independent per-domain lock, so this is safe to parallelize.
|
||||
await Promise.all(
|
||||
pendingCerts.map((cert) => this.processSingleCertificate(cert))
|
||||
);
|
||||
}
|
||||
|
||||
async processRenewalCandidates(): Promise<void> {
|
||||
logger.debug("Checking for certificates needing renewal...");
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const renewalCandidates = await db
|
||||
.select(getTableColumns(certificates))
|
||||
.from(certificates)
|
||||
.leftJoin(domains, eq(certificates.domainId, domains.domainId))
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "valid"),
|
||||
lt(certificates.expiresAt, now + 15 * 24 * 60 * 60), // 15 days from now
|
||||
or(
|
||||
// Certs with no linked domain row (e.g. legacy certs
|
||||
// imported from acme.json) aren't gated on domain
|
||||
// verification since there's nothing to check.
|
||||
isNull(certificates.domainId),
|
||||
and(
|
||||
eq(domains.verified, true),
|
||||
eq(domains.failed, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
// Most urgent first, so already-expired certs aren't starved
|
||||
// behind the limit by certs that still have weeks of runway.
|
||||
.orderBy(asc(certificates.expiresAt))
|
||||
.limit(50);
|
||||
|
||||
if (renewalCandidates.length === 0) {
|
||||
logger.debug("No certificates need renewal");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Found ${renewalCandidates.length} certificates needing renewal`
|
||||
);
|
||||
|
||||
for (const cert of renewalCandidates) {
|
||||
if (cert.expiresAt !== null && cert.expiresAt < now) {
|
||||
logger.warn(
|
||||
`Certificate for ${cert.domain} is marked "valid" but already expired at ${new Date(cert.expiresAt * 1000).toISOString()} (bad state) - renewing immediately`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process the batch concurrently - see processPendingCertificates for why.
|
||||
await Promise.all(
|
||||
renewalCandidates.map((cert) => this.renewCertificate(cert))
|
||||
);
|
||||
}
|
||||
|
||||
private async processSingleCertificate(cert: Certificate): Promise<void> {
|
||||
const lockKey = `cert:${cert.domain}`;
|
||||
|
||||
const lockToken = await lockManager.acquireLock(lockKey);
|
||||
if (!lockToken) {
|
||||
logger.debug(
|
||||
`Could not acquire lock for certificate: ${cert.domain}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Processing certificate for domain: ${cert.domain}`);
|
||||
|
||||
// Update status to processing
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
status: "requested",
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId));
|
||||
//
|
||||
|
||||
await withTimeout(
|
||||
this.obtainCertificate(cert),
|
||||
CERTIFICATE_ISSUANCE_TIMEOUT_MS,
|
||||
`certificate issuance for ${cert.domain}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to process certificate for ${cert.domain}:`,
|
||||
error
|
||||
);
|
||||
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
status: "failed",
|
||||
errorMessage:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error",
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId));
|
||||
} finally {
|
||||
await lockManager.releaseLock(lockKey, lockToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async renewCertificate(cert: Certificate): Promise<void> {
|
||||
const lockKey = `cert:${cert.domain}`;
|
||||
|
||||
const lockToken = await lockManager.acquireLock(lockKey);
|
||||
if (!lockToken) {
|
||||
logger.debug(
|
||||
`Could not acquire lock for certificate renewal: ${cert.domain}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Renewing certificate for domain: ${cert.domain}`);
|
||||
|
||||
// Update last renewal attempt
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
lastRenewalAttempt: Math.floor(Date.now() / 1000),
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId));
|
||||
|
||||
await withTimeout(
|
||||
this.obtainCertificate(cert),
|
||||
CERTIFICATE_ISSUANCE_TIMEOUT_MS,
|
||||
`certificate renewal for ${cert.domain}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to renew certificate for ${cert.domain}:`,
|
||||
error
|
||||
);
|
||||
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
status: "failed",
|
||||
errorMessage:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error",
|
||||
lastRenewalAttempt: Math.floor(Date.now() / 1000),
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId));
|
||||
} finally {
|
||||
await lockManager.releaseLock(lockKey, lockToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-checks the domain's DNS records right before we spend a Let's
|
||||
// Encrypt order on it, so drift that happened after the domain was
|
||||
// originally verified doesn't burn ACME rate limits. Certs with no
|
||||
// linked domain row (e.g. legacy/manually-managed certs) skip this and
|
||||
// proceed as before, since there are no tracked DNS records to check.
|
||||
private async verifyDomainBeforeIssuance(cert: Certificate): Promise<void> {
|
||||
if (!cert.domainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [domain] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.domainId, cert.domainId))
|
||||
.limit(1);
|
||||
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (
|
||||
let attempt = 1;
|
||||
attempt <= PRE_CERT_DNS_VALIDATION_ATTEMPTS;
|
||||
attempt++
|
||||
) {
|
||||
// Offset `tries` so each attempt round-robins to a different
|
||||
// privateConfigured DNS resolver instead of re-querying the same one.
|
||||
const probe = { ...domain, tries: domain.tries + attempt - 1 };
|
||||
if (
|
||||
await dnsValidator.validateDomain(probe, {
|
||||
forceRecheck: true
|
||||
})
|
||||
) {
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ verified: true, failed: false, errorMessage: null })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Pre-certificate DNS check ${attempt}/${PRE_CERT_DNS_VALIDATION_ATTEMPTS} failed for domain ${domain.baseDomain} (cert: ${cert.domain})`
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = `Domain failed DNS validation ${PRE_CERT_DNS_VALIDATION_ATTEMPTS} times before certificate issuance`;
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ verified: false, failed: true, errorMessage })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
private async obtainCertificate(cert: Certificate): Promise<void> {
|
||||
await this.verifyDomainBeforeIssuance(cert);
|
||||
|
||||
// Create order
|
||||
const order = await acmeClientManager.createOrder(
|
||||
cert.domain,
|
||||
cert.wildcard || false
|
||||
);
|
||||
|
||||
// Update with order ID
|
||||
await withRetry(
|
||||
() =>
|
||||
db
|
||||
.update(certificates)
|
||||
.set({
|
||||
orderId: order.url,
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId)),
|
||||
{ label: `update orderId for certificate ${cert.domain}` }
|
||||
);
|
||||
|
||||
// Get authorizations
|
||||
const authorizations = await acmeClientManager.getAuthorizations(order);
|
||||
|
||||
// Aggregate all DNS-01 challenges
|
||||
const dnsChallenges = authorizations.map((authz: any) => {
|
||||
const dnsChallenge = authz.challenges.find(
|
||||
(c: any) => c.type === "dns-01"
|
||||
);
|
||||
if (!dnsChallenge) {
|
||||
throw new Error(
|
||||
`No DNS-01 challenge found for ${authz.identifier.value}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
authz,
|
||||
challenge: dnsChallenge
|
||||
};
|
||||
});
|
||||
|
||||
// Send all DNS-01 challenges in one request to handleDnsChallenge
|
||||
await acmeClientManager.handleDnsChallenge(dnsChallenges);
|
||||
|
||||
// Finalize certificate
|
||||
const { certificate, privateKey } =
|
||||
await acmeClientManager.finalizeCertificate(
|
||||
order,
|
||||
cert.domain,
|
||||
cert.wildcard || false
|
||||
);
|
||||
|
||||
const encryptionKey = config.getRawConfig().server.secret;
|
||||
if (!encryptionKey) {
|
||||
throw new Error("Encryption key not provided");
|
||||
}
|
||||
|
||||
// Encrypt certificate and private key
|
||||
const encryptedCert = encrypt(certificate, encryptionKey);
|
||||
const encryptedKey = encrypt(privateKey, encryptionKey);
|
||||
|
||||
// Parse certificate to get expiration date
|
||||
const expiresAt = this.extractExpirationDate(certificate);
|
||||
|
||||
// Update database record. This persists the certificate we just
|
||||
// obtained from the ACME server, so it's retried aggressively -
|
||||
// losing this write means re-issuing the cert from scratch.
|
||||
await withRetry(
|
||||
() =>
|
||||
db
|
||||
.update(certificates)
|
||||
.set({
|
||||
status: "valid",
|
||||
expiresAt: Math.floor(expiresAt.getTime() / 1000),
|
||||
renewalCount: (cert.renewalCount || 0) + 1,
|
||||
errorMessage: null,
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
certFile: encryptedCert,
|
||||
keyFile: encryptedKey
|
||||
})
|
||||
.where(eq(certificates.certId, cert.certId)),
|
||||
{
|
||||
retries: 5,
|
||||
label: `persist issued certificate for ${cert.domain}`
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Certificate successfully obtained/renewed for domain: ${cert.domain}`
|
||||
);
|
||||
|
||||
await pushCertUpdateToAffectedNewts(
|
||||
cert.domain,
|
||||
cert.domainId ?? null,
|
||||
certificate,
|
||||
privateKey
|
||||
);
|
||||
}
|
||||
|
||||
private extractExpirationDate(certificate: string): Date {
|
||||
try {
|
||||
// Extract the certificate block
|
||||
const pem = certificate
|
||||
.replace(/-----BEGIN CERTIFICATE-----/g, "")
|
||||
.replace(/-----END CERTIFICATE-----/g, "")
|
||||
.replace(/\s+/g, "");
|
||||
const der = Buffer.from(pem, "base64");
|
||||
|
||||
// Use Node.js crypto to parse the certificate
|
||||
const x509 = new crypto.X509Certificate(der);
|
||||
return new Date(x509.validTo);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"Failed to parse certificate expiration date, using default",
|
||||
error
|
||||
);
|
||||
// Default to 90 days from now (Let's Encrypt default)
|
||||
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async addCertificateRequest(domain: string): Promise<void> {
|
||||
try {
|
||||
await db.insert(certificates).values({
|
||||
domain,
|
||||
status: "pending",
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
logger.info(`Certificate request added for domain: ${domain}`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("unique")) {
|
||||
logger.warn(
|
||||
`Certificate request already exists for domain: ${domain}`
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getCertificateStatus(domain: string) {
|
||||
const cert = await db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(eq(certificates.domain, domain))
|
||||
.limit(1);
|
||||
|
||||
return cert[0] || null;
|
||||
}
|
||||
|
||||
async cleanupExpiredChallenges(): Promise<void> {
|
||||
try {
|
||||
const result = await db
|
||||
.delete(certificates)
|
||||
.where(
|
||||
lt(certificates.expiresAt, Math.floor(Date.now() / 1000))
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (result.length > 0) {
|
||||
logger.info(
|
||||
`Cleaned up ${result.length} expired DNS challenges`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to cleanup expired challenges:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const certificateService = new CertificateService();
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* 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 { eq, and, lt } from "drizzle-orm";
|
||||
import * as dns from "dns/promises";
|
||||
import { privateConfig as config } from "#private/lib/config";
|
||||
import { db, domains, DnsRecord, dnsRecords, Domain } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { lockManager } from "../lock";
|
||||
|
||||
export const DNS_VALIDATOR_MAX_TRIES = 300;
|
||||
|
||||
export class DNSValidator {
|
||||
private static readonly MAX_TRIES = DNS_VALIDATOR_MAX_TRIES;
|
||||
|
||||
constructor() {}
|
||||
|
||||
async validateAll(): Promise<void> {
|
||||
// Get all domains that are not yet verified and haven't exceeded max tries
|
||||
const unverifiedDomains: Domain[] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(
|
||||
and(
|
||||
eq(domains.verified, false),
|
||||
lt(domains.tries, DNSValidator.MAX_TRIES)
|
||||
)
|
||||
);
|
||||
|
||||
if (unverifiedDomains.length === 0) {
|
||||
logger.debug("No unverified domains found for DNS validation");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Validating ${unverifiedDomains.length} DNS records`);
|
||||
|
||||
for (const domain of unverifiedDomains) {
|
||||
const lockKey = `dns:${domain.baseDomain}`;
|
||||
const lockToken = await lockManager.acquireLock(lockKey);
|
||||
if (!lockToken) {
|
||||
logger.debug(
|
||||
`Could not acquire lock for DNS validation: ${domain.baseDomain}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const isValid = await this.validateDomain(domain);
|
||||
if (isValid) {
|
||||
await db
|
||||
.update(domains)
|
||||
.set({
|
||||
verified: true,
|
||||
failed: false,
|
||||
tries: 0,
|
||||
errorMessage: null
|
||||
})
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
logger.info(
|
||||
`Domain ${domain.baseDomain} validated successfully`
|
||||
);
|
||||
} else {
|
||||
const newTries = domain.tries + 1;
|
||||
const shouldMarkAsFailed =
|
||||
newTries >= DNSValidator.MAX_TRIES;
|
||||
|
||||
await db
|
||||
.update(domains)
|
||||
.set({
|
||||
tries: newTries,
|
||||
failed: shouldMarkAsFailed
|
||||
})
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
|
||||
if (shouldMarkAsFailed) {
|
||||
logger.warn(
|
||||
`Domain ${domain.baseDomain} exceeded maximum tries (${DNSValidator.MAX_TRIES}), marking as failed`
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Domain ${domain.baseDomain} did not validate (attempt ${newTries}/${DNSValidator.MAX_TRIES})`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Error validating domain ${domain.baseDomain}:`,
|
||||
err
|
||||
);
|
||||
// Increment tries even on error
|
||||
const newTries = domain.tries + 1;
|
||||
const shouldMarkAsFailed = newTries >= DNSValidator.MAX_TRIES;
|
||||
|
||||
await db
|
||||
.update(domains)
|
||||
.set({
|
||||
tries: newTries,
|
||||
failed: shouldMarkAsFailed
|
||||
})
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
} finally {
|
||||
await lockManager.releaseLock(lockKey, lockToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async validateDomain(
|
||||
domain: Domain,
|
||||
opts: { forceRecheck?: boolean } = {}
|
||||
): Promise<boolean> {
|
||||
const { forceRecheck = false } = opts;
|
||||
const resolver = new dns.Resolver();
|
||||
const servers = config.getRawConfig().acme?.dns_resolvers;
|
||||
if (!servers || servers.length === 0) {
|
||||
throw new Error("No DNS resolvers configured");
|
||||
}
|
||||
const dnsServer = servers[domain.tries % servers.length]!;
|
||||
resolver.setServers([dnsServer]);
|
||||
logger.debug(
|
||||
`Using DNS server ${dnsServer} for domain ${domain.baseDomain} (try ${domain.tries})`
|
||||
);
|
||||
|
||||
// Get all DNS records for this domain
|
||||
const records: DnsRecord[] = await db
|
||||
.select()
|
||||
.from(dnsRecords)
|
||||
.where(eq(dnsRecords.domainId, domain.domainId));
|
||||
|
||||
if (records.length === 0) {
|
||||
logger.warn(`No DNS records found for domain ${domain.baseDomain}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!forceRecheck && records.every((r) => r.verified)) {
|
||||
logger.info(
|
||||
`All DNS records already verified for domain ${domain.baseDomain}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Validating ${records.length} DNS records for domain ${domain.baseDomain}`
|
||||
);
|
||||
|
||||
// Collect the full set of expected NS values for this domain so we can
|
||||
// detect extra records that are present in DNS but not in our DB.
|
||||
const expectedNsValues = new Set<string>(
|
||||
records.filter((r) => r.recordType === "NS").map((r) => r.value)
|
||||
);
|
||||
|
||||
// Cache resolved NS records across iterations — there will be 3 NS
|
||||
// records in the DB and we don't need to hit the upstream server 3 times.
|
||||
let previousNs: string[] | null = null;
|
||||
|
||||
for (const record of records) {
|
||||
// Skip already verified records, unless a live recheck was requested
|
||||
if (record.verified && !forceRecheck) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let isValid = false;
|
||||
|
||||
try {
|
||||
if (record.recordType === "NS") {
|
||||
let nsRecords: string[] | null = previousNs;
|
||||
if (!nsRecords) {
|
||||
nsRecords = await resolver.resolveNs(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
}
|
||||
logger.info(
|
||||
`NS records for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
}:`,
|
||||
nsRecords
|
||||
);
|
||||
|
||||
// Check if this expected NS value is present in the live records.
|
||||
// A stale/legacy expected value (e.g. left over from a
|
||||
// nameserver rebrand) is also accepted as long as the live
|
||||
// records resolve to some other known-valid nameserver —
|
||||
// the specific literal hostname stored per-domain isn't
|
||||
// meaningful once it's a recognized alias.
|
||||
isValid = nsRecords.some((ns) => ns === record.value);
|
||||
|
||||
previousNs = nsRecords;
|
||||
} else if (record.recordType === "CNAME") {
|
||||
const cnameRecords = await resolver.resolveCname(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
logger.info(
|
||||
`CNAME records for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
}:`,
|
||||
cnameRecords
|
||||
);
|
||||
|
||||
// Check if the CNAME record matches the expected value
|
||||
isValid =
|
||||
cnameRecords.length === 1 &&
|
||||
cnameRecords[0] === record.value;
|
||||
} else if (record.recordType === "TXT") {
|
||||
const txtRecords = await resolver.resolveTxt(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
logger.info(
|
||||
`TXT records for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
}:`,
|
||||
txtRecords
|
||||
);
|
||||
|
||||
// TXT records come as an array of arrays, flatten and check
|
||||
const flatTxtRecords = txtRecords.flat();
|
||||
isValid = flatTxtRecords.includes(record.value);
|
||||
} else if (record.recordType === "A") {
|
||||
const aRecords = await resolver.resolve4(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
logger.info(
|
||||
`A records for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
}:`,
|
||||
aRecords
|
||||
);
|
||||
|
||||
// Check if the A record matches the expected value
|
||||
isValid = aRecords.includes(record.value);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Unsupported record type: ${record.recordType}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
logger.debug(
|
||||
`Did not resolve ${record.recordType} record for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Update the individual record verification status. Runs for
|
||||
// both a mismatched value and a failed/thrown DNS lookup, so a
|
||||
// previously-verified record that stops resolving (e.g. NXDOMAIN
|
||||
// after NS delegation is dropped) gets downgraded instead of
|
||||
// leaving stale `verified: true` state behind.
|
||||
if (isValid) {
|
||||
await db
|
||||
.update(dnsRecords)
|
||||
.set({ verified: true })
|
||||
.where(eq(dnsRecords.id, record.id));
|
||||
logger.info(
|
||||
`DNS record ${record.id} (${record.recordType}) for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
} verified successfully`
|
||||
);
|
||||
} else {
|
||||
if (record.verified) {
|
||||
await db
|
||||
.update(dnsRecords)
|
||||
.set({ verified: false })
|
||||
.where(eq(dnsRecords.id, record.id));
|
||||
}
|
||||
logger.debug(
|
||||
`DNS record ${record.id} (${record.recordType}) for ${
|
||||
record.baseDomain || domain.baseDomain
|
||||
} does not match expected value: ${record.value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Extra NS record check ---
|
||||
// If we resolved NS records during this pass, verify that the live DNS
|
||||
// has no nameservers beyond the ones we expect. Individual records may
|
||||
// already be marked verified above, but we must block full domain
|
||||
// verification until the extra records are removed.
|
||||
if (previousNs !== null && expectedNsValues.size > 0) {
|
||||
const extraNsRecords = previousNs.filter(
|
||||
(ns) => !expectedNsValues.has(ns)
|
||||
);
|
||||
|
||||
if (extraNsRecords.length > 0) {
|
||||
const errorMessage = `Extra NS records found that are not expected: ${extraNsRecords.join(", ")}. Remove these nameservers to complete domain verification.`;
|
||||
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ errorMessage })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
|
||||
logger.warn(
|
||||
`Domain ${domain.baseDomain} has extra NS records that prevent verification: ${extraNsRecords.join(", ")}`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// No extras — clear any stale error that was previously written
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ errorMessage: null })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
}
|
||||
|
||||
// Check if all records are now verified
|
||||
const updatedRecords: DnsRecord[] = await db
|
||||
.select()
|
||||
.from(dnsRecords)
|
||||
.where(eq(dnsRecords.domainId, domain.domainId));
|
||||
|
||||
const allRecordsVerified = updatedRecords.every((r) => r.verified);
|
||||
|
||||
logger.info(
|
||||
`Domain ${domain.baseDomain}: ${
|
||||
updatedRecords.filter((r) => r.verified).length
|
||||
}/${updatedRecords.length} records verified`
|
||||
);
|
||||
|
||||
return allRecordsVerified;
|
||||
}
|
||||
}
|
||||
|
||||
export const dnsValidator = new DNSValidator();
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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 { eq, and, or, isNull, lt } from "drizzle-orm";
|
||||
import * as dns from "dns/promises";
|
||||
import { DNS_VALIDATOR_MAX_TRIES } from "./dns-validator";
|
||||
import { db, domains, DnsRecord, dnsRecords, Domain } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { lockManager } from "../lock";
|
||||
import { privateConfig as config } from "#private/lib/config";
|
||||
|
||||
// Module-level counter so successive domains in a batch round-robin across servers.
|
||||
let serverIndex = 0;
|
||||
|
||||
export class DomainReverifier {
|
||||
async reverifyAll(): Promise<void> {
|
||||
const certConfig = config.getRawConfig().acme;
|
||||
if (!certConfig) {
|
||||
logger.debug(
|
||||
"No certificate config — skipping domain reverification"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const windowMs = certConfig.domain_reverification_window_ms;
|
||||
const batchSize = certConfig.domain_reverification_batch_size;
|
||||
const windowSecs = Math.floor(windowMs / 1000);
|
||||
const cutoff = Math.floor(Date.now() / 1000) - windowSecs;
|
||||
|
||||
const domainsToCheck: Domain[] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(
|
||||
and(
|
||||
eq(domains.verified, true),
|
||||
or(
|
||||
isNull(domains.lastCheckedAt),
|
||||
lt(domains.lastCheckedAt, cutoff)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(batchSize);
|
||||
|
||||
if (domainsToCheck.length === 0) {
|
||||
logger.debug("No verified domains due for reverification");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Reverifying ${domainsToCheck.length} domains`);
|
||||
|
||||
for (const domain of domainsToCheck) {
|
||||
const lockKey = `dns-reverify:${domain.baseDomain}`;
|
||||
const lockToken = await lockManager.acquireLock(lockKey);
|
||||
if (!lockToken) {
|
||||
logger.debug(
|
||||
`Could not acquire lock for domain reverification: ${domain.baseDomain}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.reverifyDomain(domain, certConfig.dns_resolvers);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Unexpected error reverifying domain ${domain.baseDomain}:`,
|
||||
err
|
||||
);
|
||||
// Still stamp lastCheckedAt so we don't hammer a broken domain every run.
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ lastCheckedAt: Math.floor(Date.now() / 1000) })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
} finally {
|
||||
await lockManager.releaseLock(lockKey, lockToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async reverifyDomain(
|
||||
domain: Domain,
|
||||
servers: string[]
|
||||
): Promise<void> {
|
||||
if (!servers || servers.length === 0) {
|
||||
throw new Error("No DNS resolvers configured");
|
||||
}
|
||||
|
||||
// Round-robin across servers; advance the global counter so the next
|
||||
// domain in the same batch gets a different server.
|
||||
const dnsServer = servers[serverIndex % servers.length]!;
|
||||
serverIndex++;
|
||||
|
||||
const resolver = new dns.Resolver();
|
||||
resolver.setServers([dnsServer]);
|
||||
|
||||
logger.debug(
|
||||
`Reverifying domain ${domain.baseDomain} using DNS server ${dnsServer}`
|
||||
);
|
||||
|
||||
const records: DnsRecord[] = await db
|
||||
.select()
|
||||
.from(dnsRecords)
|
||||
.where(eq(dnsRecords.domainId, domain.domainId));
|
||||
|
||||
if (records.length === 0) {
|
||||
logger.warn(
|
||||
`No DNS records found for domain ${domain.baseDomain} during reverification — marking failed`
|
||||
);
|
||||
await this.markFailed(
|
||||
domain.domainId,
|
||||
"No DNS records found during periodic reverification"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedNsValues = new Set<string>(
|
||||
records.filter((r) => r.recordType === "NS").map((r) => r.value)
|
||||
);
|
||||
|
||||
let allValid = true;
|
||||
let errorMessage: string | null = null;
|
||||
let resolvedNs: string[] | null = null;
|
||||
|
||||
for (const record of records) {
|
||||
let isValid = false;
|
||||
|
||||
try {
|
||||
if (record.recordType === "NS") {
|
||||
if (!resolvedNs) {
|
||||
resolvedNs = await resolver.resolveNs(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
}
|
||||
isValid = resolvedNs.some((ns) => ns === record.value);
|
||||
} else if (record.recordType === "CNAME") {
|
||||
const cnameRecords = await resolver.resolveCname(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
isValid =
|
||||
cnameRecords.length === 1 &&
|
||||
cnameRecords[0] === record.value;
|
||||
} else if (record.recordType === "TXT") {
|
||||
const txtRecords = await resolver.resolveTxt(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
isValid = txtRecords.flat().includes(record.value);
|
||||
} else if (record.recordType === "A") {
|
||||
const aRecords = await resolver.resolve4(
|
||||
record.baseDomain || domain.baseDomain
|
||||
);
|
||||
isValid = aRecords.includes(record.value);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Unsupported record type ${record.recordType} during reverification of ${domain.baseDomain}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
`DNS lookup failed for ${record.recordType} record on ${record.baseDomain || domain.baseDomain}:`,
|
||||
err
|
||||
);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
allValid = false;
|
||||
errorMessage = `${record.recordType} record for ${record.baseDomain || domain.baseDomain} no longer resolves to expected value "${record.value}"`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for extra NS records beyond what we expect.
|
||||
if (allValid && resolvedNs !== null && expectedNsValues.size > 0) {
|
||||
const extraNs = resolvedNs.filter(
|
||||
(ns) => !expectedNsValues.has(ns)
|
||||
);
|
||||
if (extraNs.length > 0) {
|
||||
allValid = false;
|
||||
errorMessage = `Extra NS records found: ${extraNs.join(", ")}. Remove these nameservers.`;
|
||||
}
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (allValid) {
|
||||
await db
|
||||
.update(domains)
|
||||
.set({ lastCheckedAt: now, errorMessage: null })
|
||||
.where(eq(domains.domainId, domain.domainId));
|
||||
logger.debug(
|
||||
`Domain ${domain.baseDomain} passed periodic reverification`
|
||||
);
|
||||
} else {
|
||||
await this.markFailed(domain.domainId, errorMessage);
|
||||
logger.warn(
|
||||
`Domain ${domain.baseDomain} failed periodic reverification: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async markFailed(
|
||||
domainId: string,
|
||||
errorMessage: string | null
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(domains)
|
||||
.set({
|
||||
verified: false,
|
||||
failed: true,
|
||||
// Three below MAX_TRIES: keeps the domain out of the DNS
|
||||
// validator's immediate retry loop, while still leaving it
|
||||
// eligible (tries < MAX_TRIES) for a few more validation
|
||||
// passes instead of being excluded forever once tries hits
|
||||
// MAX_TRIES.
|
||||
tries: DNS_VALIDATOR_MAX_TRIES - 3,
|
||||
lastCheckedAt: Math.floor(Date.now() / 1000),
|
||||
errorMessage
|
||||
})
|
||||
.where(eq(domains.domainId, domainId));
|
||||
}
|
||||
}
|
||||
|
||||
export const domainReverifier = new DomainReverifier();
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 logger from "@server/logger";
|
||||
import { privateConfig } from "#private/lib/config";
|
||||
import { acmeClientManager } from "./acme-client";
|
||||
import { jobScheduler } from "./scheduler";
|
||||
|
||||
export async function startCertificateManager() {
|
||||
const acmeConfig = privateConfig.getRawPrivateConfig().acme;
|
||||
if (
|
||||
acmeConfig &&
|
||||
acmeConfig.cert_mode === "pangolin" &&
|
||||
acmeConfig.enable_acme_client
|
||||
) {
|
||||
logger.info("Starting certificate management server...");
|
||||
|
||||
// Initialize ACME client
|
||||
await acmeClientManager.initialize();
|
||||
|
||||
// Start certificate issuance/renewal jobs
|
||||
await jobScheduler.start();
|
||||
}
|
||||
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
// DNS record validation/reverification doesn't require certs, so it
|
||||
// runs whenever Pangolin is acting as the authoritative DNS server,
|
||||
// independent of the cert manager above.
|
||||
await jobScheduler.startDnsJobs();
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopCertificateManager() {
|
||||
await jobScheduler.stop();
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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 { withTimeout } from "@server/lib/retry";
|
||||
import logger from "@server/logger";
|
||||
import { certificateService } from "./certificate-service";
|
||||
import { privateConfig as config } from "#private/lib/config";
|
||||
import { dnsValidator } from "./dns-validator";
|
||||
import { domainReverifier } from "./domain-reverifier";
|
||||
import license from "#private/license/license";
|
||||
|
||||
// Backstop for runExclusive: no single job's own internal timeouts (e.g.
|
||||
// certificate-service's per-cert issuance timeout) are relied on here. This
|
||||
// is the last line of defense - if *anything* inside a job hangs with no
|
||||
// error (a stalled Redis/DB call, a future code path that forgets to bound
|
||||
// itself, etc.), state.active must still reset so the next tick can run.
|
||||
// Without it, one hung run permanently skips every future tick for that job,
|
||||
// since runExclusive only clears state.active after the job promise settles.
|
||||
const RUN_EXCLUSIVE_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
export class JobScheduler {
|
||||
private certIntervals: NodeJS.Timeout[] = [];
|
||||
private dnsIntervals: NodeJS.Timeout[] = [];
|
||||
private certRunning = false;
|
||||
private dnsRunning = false;
|
||||
|
||||
// Guards against a slow batch (e.g. 10 certs whose DNS challenges take a
|
||||
// while) still being processed when the next interval tick fires -
|
||||
// without this, overlapping ticks would each pull their own batch of up
|
||||
// to 10 pending/renewal certs and process them concurrently instead of
|
||||
// waiting for the prior batch to finish.
|
||||
private runExclusive(
|
||||
job: () => Promise<void>,
|
||||
state: { active: boolean },
|
||||
label: string
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
if (!(await license.isUnlocked())) {
|
||||
logger.debug(
|
||||
`Skipping ${label} tick - license is not subscribed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.active) {
|
||||
logger.debug(
|
||||
`Skipping ${label} tick - previous run still in progress`
|
||||
);
|
||||
return;
|
||||
}
|
||||
state.active = true;
|
||||
try {
|
||||
await withTimeout(job(), RUN_EXCLUSIVE_TIMEOUT_MS, label);
|
||||
} catch (error) {
|
||||
logger.error(`Error in ${label}:`, error);
|
||||
} finally {
|
||||
state.active = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Certificate issuance/renewal - requires an ACME client, so this is
|
||||
// only started when Pangolin is actually managing certs.
|
||||
async start(): Promise<void> {
|
||||
if (this.certRunning) {
|
||||
logger.warn("Certificate job scheduler is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
this.certRunning = true;
|
||||
logger.info("Starting certificate job scheduler");
|
||||
|
||||
const newCertState = { active: false };
|
||||
const renewalState = { active: false };
|
||||
|
||||
const runNewCertCheck = this.runExclusive(
|
||||
() => certificateService.processPendingCertificates(),
|
||||
newCertState,
|
||||
"processing pending certificates"
|
||||
);
|
||||
const runRenewalCheck = this.runExclusive(
|
||||
() => certificateService.processRenewalCandidates(),
|
||||
renewalState,
|
||||
"processing renewal candidates"
|
||||
);
|
||||
|
||||
// Schedule new certificate processing
|
||||
const newCertInterval = setInterval(
|
||||
runNewCertCheck,
|
||||
config.getRawConfig().acme!.new_cert_check_interval_ms
|
||||
);
|
||||
|
||||
// Schedule renewal processing (every 24 hours)
|
||||
const renewalInterval = setInterval(
|
||||
runRenewalCheck,
|
||||
config.getRawConfig().acme!.renewal_check_interval_ms
|
||||
);
|
||||
|
||||
this.certIntervals.push(newCertInterval, renewalInterval);
|
||||
|
||||
// Run initial checks
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await runNewCertCheck();
|
||||
// await runRenewalCheck();
|
||||
} catch (error) {
|
||||
logger.error("Error in initial certificate processing:", error);
|
||||
}
|
||||
}, 1000); // Wait 1 second after startup
|
||||
|
||||
logger.info("Certificate job scheduler started successfully");
|
||||
}
|
||||
|
||||
// DNS record validation/reverification - doesn't touch certs at all, so
|
||||
// this runs independently whenever Pangolin is acting as the
|
||||
// authoritative DNS server, regardless of cert_mode.
|
||||
async startDnsJobs(): Promise<void> {
|
||||
if (this.dnsRunning) {
|
||||
logger.warn("DNS validation job scheduler is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
this.dnsRunning = true;
|
||||
logger.info("Starting DNS validation job scheduler");
|
||||
|
||||
const dnsValidationState = { active: false };
|
||||
const reverifyState = { active: false };
|
||||
|
||||
const runDnsValidation = this.runExclusive(
|
||||
() => dnsValidator.validateAll(),
|
||||
dnsValidationState,
|
||||
"validating DNS records"
|
||||
);
|
||||
const runReverify = this.runExclusive(
|
||||
() => domainReverifier.reverifyAll(),
|
||||
reverifyState,
|
||||
"reverifying domains"
|
||||
);
|
||||
|
||||
// Schedule DNS validation
|
||||
const dnsValidationInterval = setInterval(
|
||||
runDnsValidation,
|
||||
config.getRawConfig().acme?.dns_check_interval_ms ?? 60000
|
||||
);
|
||||
|
||||
// Schedule periodic reverification of already-verified domains
|
||||
const reverifyInterval = setInterval(
|
||||
runReverify,
|
||||
config.getRawConfig().acme?.domain_reverification_interval_ms ??
|
||||
3600000
|
||||
);
|
||||
|
||||
this.dnsIntervals.push(dnsValidationInterval, reverifyInterval);
|
||||
|
||||
// Run an initial validation pass shortly after startup
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await runDnsValidation();
|
||||
} catch (error) {
|
||||
logger.error("Error in initial DNS validation:", error);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
logger.info("DNS validation job scheduler started successfully");
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.certRunning) {
|
||||
logger.info("Stopping certificate job scheduler");
|
||||
this.certRunning = false;
|
||||
this.certIntervals.forEach((interval) => clearInterval(interval));
|
||||
this.certIntervals = [];
|
||||
}
|
||||
|
||||
if (this.dnsRunning) {
|
||||
logger.info("Stopping DNS validation job scheduler");
|
||||
this.dnsRunning = false;
|
||||
this.dnsIntervals.forEach((interval) => clearInterval(interval));
|
||||
this.dnsIntervals = [];
|
||||
}
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.certRunning || this.dnsRunning;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobScheduler = new JobScheduler();
|
||||
@@ -146,12 +146,20 @@ export class PrivateConfig {
|
||||
process.env.USE_PANGOLIN_DNS =
|
||||
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
|
||||
}
|
||||
|
||||
if (this.rawPrivateConfig.acme?.cert_mode) {
|
||||
process.env.CERT_MODE = this.rawPrivateConfig.acme.cert_mode;
|
||||
}
|
||||
}
|
||||
|
||||
public getRawPrivateConfig() {
|
||||
return this.rawPrivateConfig;
|
||||
}
|
||||
|
||||
public getRawConfig() {
|
||||
return this.getRawPrivateConfig();
|
||||
}
|
||||
|
||||
// `flags.enable_acme_cert_sync`, `flags.disable_private_http_placeholder`,
|
||||
// and `acme` used to live in the private config file. They now live in
|
||||
// the public config file. If an operator still has them set in the
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { build } from "@server/build";
|
||||
import privateConfig from "#private/lib/config";
|
||||
|
||||
export function createCname(domainId: string, baseDomain: string) {
|
||||
if (!privateConfig.getRawPrivateConfig().dns?.cname_extension) {
|
||||
throw new Error("CNAME extension not configured");
|
||||
}
|
||||
|
||||
let cnameRecords = [
|
||||
{
|
||||
value: `${domainId}.${privateConfig.getRawPrivateConfig().dns?.cname_extension}`,
|
||||
baseDomain: baseDomain
|
||||
},
|
||||
{
|
||||
value: `_acme-challenge.${domainId}.${privateConfig.getRawPrivateConfig().dns?.cname_extension}`,
|
||||
baseDomain: `_acme-challenge.${baseDomain}`
|
||||
}
|
||||
];
|
||||
|
||||
return cnameRecords;
|
||||
}
|
||||
|
||||
export function createNs() {
|
||||
if (!privateConfig.getRawPrivateConfig().dns?.nameserver_name) {
|
||||
throw new Error("Nameservers not configured");
|
||||
}
|
||||
|
||||
const nsRecords = [
|
||||
privateConfig.getRawPrivateConfig().dns?.nameserver_name,
|
||||
...(privateConfig.getRawPrivateConfig().dns?.alternate_nameservers ||
|
||||
[])
|
||||
] as string[];
|
||||
return nsRecords;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from "./server";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import { eq } from "drizzle-orm";
|
||||
import { sendToClient } from "#private/routers/ws";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "@server/lib/config";
|
||||
import { hasExitNodeCheckedIn } from "@server/lib/exitNodes";
|
||||
|
||||
interface ExitNodeRequest {
|
||||
remoteType?: string;
|
||||
@@ -138,13 +139,19 @@ export async function sendToExitNode(
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(
|
||||
`Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${hostname} (status: ${error.response?.status}): ${error.message}`
|
||||
);
|
||||
const message = axios.isAxiosError(error)
|
||||
? `Error making ${method} request (can Pangolin see Gerbil HTTP API?) for exit node at ${hostname} (status: ${error.response?.status}): ${error.message}`
|
||||
: `Error making ${method} request for exit node at ${hostname}: ${error}`;
|
||||
|
||||
// The exit node (gerbil) may still be starting up and not yet
|
||||
// reachable. Until it has checked in at least once, log this at a
|
||||
// lower level since it's expected; once it has checked in, a
|
||||
// connection failure is a real problem.
|
||||
if (hasExitNodeCheckedIn(exitNode.exitNodeId)) {
|
||||
logger.error(message);
|
||||
} else {
|
||||
logger.error(
|
||||
`Error making ${method} request for exit node at ${hostname}: ${error}`
|
||||
logger.warn(
|
||||
`${message} (exit node has not checked in yet since startup, this is expected briefly)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,70 @@ export const privateConfigSchema = z
|
||||
.optional()
|
||||
})
|
||||
.optional(),
|
||||
dns: z
|
||||
.object({
|
||||
enabled: z.boolean().optional().default(false),
|
||||
listen_port: z.number().int().positive().optional().default(53),
|
||||
nameserver_name: z.string(),
|
||||
cname_extension: z.string(),
|
||||
site_extension: z.string().optional(),
|
||||
cname_alternate_extensions: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([]),
|
||||
alternate_nameservers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([]),
|
||||
rate_limit: z
|
||||
.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
window_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1000)
|
||||
.max(600000)
|
||||
.optional()
|
||||
.default(60000),
|
||||
max_requests: z
|
||||
.number()
|
||||
.int()
|
||||
.min(50)
|
||||
.max(100000)
|
||||
.optional()
|
||||
.default(1200),
|
||||
max_requests_per_query_type: z
|
||||
.number()
|
||||
.int()
|
||||
.min(10)
|
||||
.max(50000)
|
||||
.optional()
|
||||
.default(600)
|
||||
})
|
||||
.default({
|
||||
enabled: true,
|
||||
window_ms: 60000,
|
||||
max_requests: 1200,
|
||||
max_requests_per_query_type: 600
|
||||
}),
|
||||
static_records: z
|
||||
.array(
|
||||
z.object({
|
||||
domain: z.string(),
|
||||
type: z.enum(["TXT", "CNAME", "A", "NS"]),
|
||||
value: z.string(),
|
||||
ttl: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(300)
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.default([])
|
||||
})
|
||||
.optional(),
|
||||
gerbil: z
|
||||
.object({
|
||||
local_exit_node_reachable_at: z
|
||||
@@ -125,15 +189,86 @@ export const privateConfigSchema = z
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
// @deprecated Moved to the public config file as `acme`
|
||||
// (server/lib/readConfigFile.ts). Kept here only so existing private
|
||||
// config files keep parsing; any value set here is migrated into the
|
||||
// public config at startup by PrivateConfig (server/private/lib/config.ts).
|
||||
acme: z
|
||||
.object({
|
||||
cert_mode: z
|
||||
.enum(["traefik", "pangolin"])
|
||||
.optional()
|
||||
.default("traefik"),
|
||||
enable_acme_client: z.boolean().optional().default(false),
|
||||
// @deprecated Moved to the public config file
|
||||
// (server/lib/readConfigFile.ts). Kept here only so existing private
|
||||
// config files keep parsing; any value set here is migrated into the
|
||||
// public config at startup by PrivateConfig (server/private/lib/config.ts).
|
||||
acme_json_path: z.string().optional(),
|
||||
// @deprecated Moved to the public config file
|
||||
// (server/lib/readConfigFile.ts). Kept here only so existing private
|
||||
// config files keep parsing; any value set here is migrated into the
|
||||
// public config at startup by PrivateConfig (server/private/lib/config.ts).
|
||||
acme_http_endpoint: z.string().optional(),
|
||||
sync_interval_ms: z.number().optional()
|
||||
// @deprecated Moved to the public config file
|
||||
// (server/lib/readConfigFile.ts). Kept here only so existing private
|
||||
// config files keep parsing; any value set here is migrated into the
|
||||
// public config at startup by PrivateConfig (server/private/lib/config.ts).
|
||||
sync_interval_ms: z.number().optional(),
|
||||
acme_directory_url: z
|
||||
.string()
|
||||
.url()
|
||||
.default("https://acme-v02.api.letsencrypt.org/directory"),
|
||||
contact_email: z.string().email().optional(),
|
||||
acme_account_key_path: z
|
||||
.string()
|
||||
.default("./config/account.key"),
|
||||
challenge_ttl_ms: z.number().int().positive().default(300000),
|
||||
renewal_check_interval_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(3600000),
|
||||
new_cert_check_interval_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(5000),
|
||||
// Kept safely under Let's Encrypt's ~20 req/s limit since this
|
||||
// budget is shared across all pops workers and only covers the
|
||||
// request-issuing calls we make directly (not every request
|
||||
// acme-client makes internally, e.g. while polling for
|
||||
// challenge/order status).
|
||||
acme_requests_per_second: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(15),
|
||||
dns_check_interval_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(60000),
|
||||
domain_reverification_interval_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(3600000), // 1 hour — how often to run the reverification pass
|
||||
domain_reverification_window_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(259200000), // 72 hours — how old checkedAt must be before rechecking
|
||||
domain_reverification_batch_size: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.default(20), // max domains to recheck per pass
|
||||
dns_resolvers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"9.9.9.9",
|
||||
"208.67.222.222"
|
||||
])
|
||||
})
|
||||
.optional(),
|
||||
branding: z
|
||||
|
||||
@@ -396,7 +396,7 @@ export async function getTraefikConfig(
|
||||
);
|
||||
|
||||
let validCerts: CertificateResult[] = [];
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
if (privateConfig.getRawPrivateConfig().acme?.cert_mode == "pangolin") {
|
||||
// create a list of all domains to get certs for
|
||||
const domains = new Set<string>();
|
||||
for (const resource of resourcesMap.values()) {
|
||||
@@ -522,7 +522,10 @@ export async function getTraefikConfig(
|
||||
);
|
||||
|
||||
let tls = {};
|
||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
if (
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
tls = buildWildcardTls({
|
||||
fullDomain,
|
||||
hasSubdomain: !!resource.subdomain,
|
||||
@@ -789,7 +792,8 @@ export async function getTraefikConfig(
|
||||
preferWildcardCert
|
||||
}) => {
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
return buildWildcardTls({
|
||||
fullDomain,
|
||||
@@ -832,7 +836,8 @@ export async function getTraefikConfig(
|
||||
redirectHttpsMiddlewareName,
|
||||
resolveTls: (fullDomain) => {
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
// siteResource aliases don't have a per-domain cert
|
||||
// resolver stored, so always fall back to the global
|
||||
@@ -924,7 +929,10 @@ export async function getTraefikConfig(
|
||||
const rule = buildHostRule(fullDomain, ir.wildcard);
|
||||
|
||||
let tls: any = {};
|
||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
if (
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
tls = buildWildcardTls({
|
||||
fullDomain,
|
||||
hasSubdomain: !!ir.subdomain,
|
||||
@@ -1005,7 +1013,8 @@ export async function getTraefikConfig(
|
||||
|
||||
let tls: any = {};
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
// siteResource aliases don't have a per-domain cert
|
||||
// resolver stored, so always fall back to the global
|
||||
@@ -1080,7 +1089,7 @@ export async function getTraefikConfig(
|
||||
.where(eq(exitNodes.exitNodeId, exitNodeId));
|
||||
|
||||
let validCertsLoginPages: CertificateResult[] = [];
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
if (privateConfig.getRawPrivateConfig().acme?.cert_mode == "pangolin") {
|
||||
// create a list of all domains to get certs for
|
||||
const domains = new Set<string>();
|
||||
for (const lp of exitNodeLoginPages) {
|
||||
@@ -1126,7 +1135,8 @@ export async function getTraefikConfig(
|
||||
|
||||
const tls = {};
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
||||
privateConfig.getRawPrivateConfig().acme?.cert_mode !=
|
||||
"pangolin"
|
||||
) {
|
||||
// TODO: we need to add the wildcard logic here too
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { db, HostMeta, sites, users } from "@server/db";
|
||||
import { hostMeta, licenseKey } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import NodeCache from "node-cache";
|
||||
import { createLocalCache } from "@server/lib/createLocalCache";
|
||||
import { validateJWT } from "./licenseJwt";
|
||||
import { count, eq } from "drizzle-orm";
|
||||
import moment from "moment";
|
||||
@@ -65,8 +65,8 @@ export class License {
|
||||
private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`;
|
||||
private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`;
|
||||
|
||||
private statusCache = new NodeCache();
|
||||
private licenseKeyCache = new NodeCache();
|
||||
private statusCache = createLocalCache();
|
||||
private licenseKeyCache = createLocalCache();
|
||||
|
||||
private statusKey = "status";
|
||||
private serverSecret!: string;
|
||||
@@ -179,7 +179,7 @@ LQIDAQAB
|
||||
status.isHostLicensed = false;
|
||||
// Invalidate all and set new cache (empty)
|
||||
this.licenseKeyCache.flushAll();
|
||||
this.statusCache.set(this.statusKey, status);
|
||||
this.statusCache.set(this.statusKey, status, 0);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ LQIDAQAB
|
||||
// Invalidate old cache and set new cache
|
||||
this.licenseKeyCache.flushAll();
|
||||
for (const [key, value] of newCache.entries()) {
|
||||
this.licenseKeyCache.set<LicenseKeyCache>(key, value);
|
||||
this.licenseKeyCache.set(key, value, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error checking license status:");
|
||||
@@ -398,7 +398,7 @@ LQIDAQAB
|
||||
this.checkInProgress = false;
|
||||
}
|
||||
|
||||
this.statusCache.set(this.statusKey, status);
|
||||
this.statusCache.set(this.statusKey, status, 0);
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { getRandomItemInArray } from "@app/lib/getRandomItemInArray";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { processTestAlerts } from "@server/private/lib/alerts/processTestAlerts";
|
||||
import { processTestAlerts } from "#private/lib/alerts/processTestAlerts";
|
||||
import { type AlertAction } from "@server/routers/alertRule/types";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
@@ -33,8 +33,11 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import { HC_EVENT_TYPES, SITE_EVENT_TYPES, RESOURCE_EVENT_TYPES } from "./createAlertRule";
|
||||
import { invalidateAllRemoteExitNodeSessions } from "@server/private/auth/sessions/remoteExitNode";
|
||||
import {
|
||||
HC_EVENT_TYPES,
|
||||
SITE_EVENT_TYPES,
|
||||
RESOURCE_EVENT_TYPES
|
||||
} from "./createAlertRule";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -85,35 +88,57 @@ const bodySchema = z
|
||||
const isHcEvent = (HC_EVENT_TYPES as readonly string[]).includes(
|
||||
val.eventType
|
||||
);
|
||||
const isResourceEvent = (RESOURCE_EVENT_TYPES as readonly string[]).includes(
|
||||
val.eventType
|
||||
);
|
||||
const isResourceEvent = (
|
||||
RESOURCE_EVENT_TYPES as readonly string[]
|
||||
).includes(val.eventType);
|
||||
|
||||
if (isSiteEvent && val.siteIds !== undefined && val.siteIds.length === 0 && !val.allSites) {
|
||||
if (
|
||||
isSiteEvent &&
|
||||
val.siteIds !== undefined &&
|
||||
val.siteIds.length === 0 &&
|
||||
!val.allSites
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one siteId is required for site event types when allSites is false",
|
||||
message:
|
||||
"At least one siteId is required for site event types when allSites is false",
|
||||
path: ["siteIds"]
|
||||
});
|
||||
}
|
||||
|
||||
if (isHcEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length === 0 && !val.allHealthChecks) {
|
||||
if (
|
||||
isHcEvent &&
|
||||
val.healthCheckIds !== undefined &&
|
||||
val.healthCheckIds.length === 0 &&
|
||||
!val.allHealthChecks
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one healthCheckId is required for health check event types when allHealthChecks is false",
|
||||
message:
|
||||
"At least one healthCheckId is required for health check event types when allHealthChecks is false",
|
||||
path: ["healthCheckIds"]
|
||||
});
|
||||
}
|
||||
|
||||
if (isResourceEvent && val.resourceIds !== undefined && val.resourceIds.length === 0 && !val.allResources) {
|
||||
if (
|
||||
isResourceEvent &&
|
||||
val.resourceIds !== undefined &&
|
||||
val.resourceIds.length === 0 &&
|
||||
!val.allResources
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "At least one resourceId is required for resource event types when allResources is false",
|
||||
message:
|
||||
"At least one resourceId is required for resource event types when allResources is false",
|
||||
path: ["resourceIds"]
|
||||
});
|
||||
}
|
||||
|
||||
if (isSiteEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length > 0) {
|
||||
if (
|
||||
isSiteEvent &&
|
||||
val.healthCheckIds !== undefined &&
|
||||
val.healthCheckIds.length > 0
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "healthCheckIds must not be set for site event types",
|
||||
@@ -129,7 +154,11 @@ const bodySchema = z
|
||||
});
|
||||
}
|
||||
|
||||
if (isResourceEvent && val.siteIds !== undefined && val.siteIds.length > 0) {
|
||||
if (
|
||||
isResourceEvent &&
|
||||
val.siteIds !== undefined &&
|
||||
val.siteIds.length > 0
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "siteIds must not be set for resource event types",
|
||||
@@ -137,10 +166,15 @@ const bodySchema = z
|
||||
});
|
||||
}
|
||||
|
||||
if (isResourceEvent && val.healthCheckIds !== undefined && val.healthCheckIds.length > 0) {
|
||||
if (
|
||||
isResourceEvent &&
|
||||
val.healthCheckIds !== undefined &&
|
||||
val.healthCheckIds.length > 0
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "healthCheckIds must not be set for resource event types",
|
||||
message:
|
||||
"healthCheckIds must not be set for resource event types",
|
||||
path: ["healthCheckIds"]
|
||||
});
|
||||
}
|
||||
@@ -153,7 +187,6 @@ const UpdateAlertRuleResponseDataSchema = z.object({
|
||||
alertRuleId: z.number()
|
||||
});
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/org/{orgId}/alert-rule/{alertRuleId}",
|
||||
@@ -174,7 +207,9 @@ registry.registerPath({
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(UpdateAlertRuleResponseDataSchema)
|
||||
schema: createApiResponseSchema(
|
||||
UpdateAlertRuleResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,9 +285,11 @@ export async function updateAlertRule(
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (eventType !== undefined) updateData.eventType = eventType;
|
||||
if (enabled !== undefined) updateData.enabled = enabled;
|
||||
if (cooldownSeconds !== undefined) updateData.cooldownSeconds = cooldownSeconds;
|
||||
if (cooldownSeconds !== undefined)
|
||||
updateData.cooldownSeconds = cooldownSeconds;
|
||||
if (allSites !== undefined) updateData.allSites = allSites;
|
||||
if (allHealthChecks !== undefined) updateData.allHealthChecks = allHealthChecks;
|
||||
if (allHealthChecks !== undefined)
|
||||
updateData.allHealthChecks = allHealthChecks;
|
||||
if (allResources !== undefined) updateData.allResources = allResources;
|
||||
|
||||
await db
|
||||
@@ -273,7 +310,11 @@ export async function updateAlertRule(
|
||||
|
||||
// Only insert junction rows when allSites is not true
|
||||
const effectiveAllSites = allSites ?? false;
|
||||
if (!effectiveAllSites && siteIds !== undefined && siteIds.length > 0) {
|
||||
if (
|
||||
!effectiveAllSites &&
|
||||
siteIds !== undefined &&
|
||||
siteIds.length > 0
|
||||
) {
|
||||
await db.insert(alertSites).values(
|
||||
siteIds.map((siteId) => ({
|
||||
alertRuleId,
|
||||
@@ -290,7 +331,11 @@ export async function updateAlertRule(
|
||||
.where(eq(alertHealthChecks.alertRuleId, alertRuleId));
|
||||
|
||||
const effectiveAllHealthChecks = allHealthChecks ?? false;
|
||||
if (!effectiveAllHealthChecks && healthCheckIds !== undefined && healthCheckIds.length > 0) {
|
||||
if (
|
||||
!effectiveAllHealthChecks &&
|
||||
healthCheckIds !== undefined &&
|
||||
healthCheckIds.length > 0
|
||||
) {
|
||||
await db.insert(alertHealthChecks).values(
|
||||
healthCheckIds.map((healthCheckId) => ({
|
||||
alertRuleId,
|
||||
@@ -307,7 +352,11 @@ export async function updateAlertRule(
|
||||
.where(eq(alertResources.alertRuleId, alertRuleId));
|
||||
|
||||
const effectiveAllResources = allResources ?? false;
|
||||
if (!effectiveAllResources && resourceIds !== undefined && resourceIds.length > 0) {
|
||||
if (
|
||||
!effectiveAllResources &&
|
||||
resourceIds !== undefined &&
|
||||
resourceIds.length > 0
|
||||
) {
|
||||
await db.insert(alertResources).values(
|
||||
resourceIds.map((resourceId) => ({
|
||||
alertRuleId,
|
||||
@@ -392,7 +441,10 @@ export async function updateAlertRule(
|
||||
webhookActions.map((wa) => ({
|
||||
alertRuleId,
|
||||
webhookUrl: wa.webhookUrl,
|
||||
config: wa.config != null ? encrypt(wa.config, serverSecret) : null,
|
||||
config:
|
||||
wa.config != null
|
||||
? encrypt(wa.config, serverSecret)
|
||||
: null,
|
||||
enabled: wa.enabled
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -31,7 +31,9 @@ export async function clearInstanceName(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = clearInstanceNameParamsSchema.safeParse(req.params);
|
||||
const parsedParams = clearInstanceNameParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -63,7 +65,8 @@ export async function clearInstanceName(
|
||||
return next(
|
||||
createHttpError(
|
||||
data.status || HttpCode.BAD_REQUEST,
|
||||
data.message || "Failed to clear instance name from Fossorial API"
|
||||
data.message ||
|
||||
"Failed to clear server ID from Fossorial API"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -72,7 +75,7 @@ export async function clearInstanceName(
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Instance name cleared successfully",
|
||||
message: "Server ID cleared successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -80,8 +83,8 @@ export async function clearInstanceName(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred while clearing the instance name."
|
||||
"An error occurred while clearing the server ID."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
|
||||
import { db, ExitNode, exitNodes } from "@server/db";
|
||||
import { getUniqueExitNodeEndpointName } from "@server/db/names";
|
||||
import config from "@server/lib/config";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
|
||||
import logger from "@server/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -45,6 +45,8 @@ export async function createExitNode(
|
||||
.values({
|
||||
publicKey,
|
||||
endpoint: config.getRawConfig().gerbil.base_endpoint,
|
||||
region:
|
||||
privateConfig.getRawPrivateConfig().app.region || null,
|
||||
address,
|
||||
listenPort,
|
||||
online: true,
|
||||
|
||||
@@ -2478,7 +2478,12 @@ hybridRouter.post(
|
||||
destinations: destinations
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (!(
|
||||
error instanceof Error &&
|
||||
error.message === "Exit node not allowed"
|
||||
)) {
|
||||
logger.error(error);
|
||||
}
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { EventEmitter } from "events";
|
||||
|
||||
export interface ExitNodeOnlineEvent {
|
||||
exitNodeId: number;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export const EXIT_NODE_ONLINE_EVENT = "exit-node-online";
|
||||
|
||||
export const exitNodeEvents = new EventEmitter();
|
||||
@@ -12,11 +12,27 @@
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { db, exitNodes, newts, sites } from "@server/db";
|
||||
import { db, newts, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import redisManager from "#private/lib/redis";
|
||||
// import { sendToClient } from "#private/routers/ws";
|
||||
import { sendToClient } from "../ws";
|
||||
import {
|
||||
exitNodeEvents,
|
||||
EXIT_NODE_ONLINE_EVENT,
|
||||
ExitNodeOnlineEvent
|
||||
} from "./exitNodeEvents";
|
||||
|
||||
exitNodeEvents.on(
|
||||
EXIT_NODE_ONLINE_EVENT,
|
||||
({ exitNodeId, endpoint }: ExitNodeOnlineEvent) => {
|
||||
scheduleExitNodeReconnect(exitNodeId, endpoint).catch((error) => {
|
||||
logger.error("Failed to schedule exit node reconnect", {
|
||||
error
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const INITIAL_DELAY_MS = 15 * 1000; // 15 seconds before first check
|
||||
const CHECK_INTERVAL_MS = 10 * 1000; // Check every 10 seconds
|
||||
@@ -26,7 +42,7 @@ const REDIS_HASH_PREFIX = "exit-node-reconnect:";
|
||||
|
||||
interface PendingReconnect {
|
||||
startTime: number;
|
||||
reachableAt: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
// In-memory tracking for this node
|
||||
@@ -40,15 +56,15 @@ let schedulerInterval: NodeJS.Timeout | null = null;
|
||||
*/
|
||||
export async function scheduleExitNodeReconnect(
|
||||
exitNodeId: number,
|
||||
reachableAt: string
|
||||
endpoint: string
|
||||
): Promise<void> {
|
||||
logger.info(
|
||||
`Scheduling newt reconnect for exit node ${exitNodeId} (reachableAt: ${reachableAt})`
|
||||
`Scheduling newt reconnect for exit node ${exitNodeId} (endpoint: ${endpoint})`
|
||||
);
|
||||
|
||||
const entry: PendingReconnect = {
|
||||
startTime: Date.now(),
|
||||
reachableAt
|
||||
endpoint
|
||||
};
|
||||
|
||||
pendingReconnects.set(exitNodeId, entry);
|
||||
@@ -63,8 +79,8 @@ export async function scheduleExitNodeReconnect(
|
||||
);
|
||||
await redisManager.hset(
|
||||
`${REDIS_HASH_PREFIX}${exitNodeId}`,
|
||||
"reachableAt",
|
||||
reachableAt
|
||||
"endpoint",
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,14 +117,14 @@ async function processPendingReconnects(): Promise<void> {
|
||||
`${REDIS_HASH_PREFIX}${id}`,
|
||||
"startTime"
|
||||
);
|
||||
const reachableAt = await redisManager.hget(
|
||||
const endpoint = await redisManager.hget(
|
||||
`${REDIS_HASH_PREFIX}${id}`,
|
||||
"reachableAt"
|
||||
"endpoint"
|
||||
);
|
||||
if (startTimeStr && reachableAt) {
|
||||
if (startTimeStr && endpoint) {
|
||||
toProcess.set(id, {
|
||||
startTime: parseInt(startTimeStr, 10),
|
||||
reachableAt
|
||||
endpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -135,7 +151,7 @@ async function processPendingReconnects(): Promise<void> {
|
||||
}
|
||||
|
||||
// Check if the exit node HTTP endpoint is reachable
|
||||
const pingUrl = `${entry.reachableAt}/ping`;
|
||||
const pingUrl = `http://${entry.endpoint}/ping`;
|
||||
try {
|
||||
await axios.get(pingUrl, { timeout: 5000 });
|
||||
} catch {
|
||||
@@ -150,47 +166,47 @@ async function processPendingReconnects(): Promise<void> {
|
||||
`Exit node ${exitNodeId} is reachable. Sending newt/wg/reconnect to connected newts.`
|
||||
);
|
||||
|
||||
// await sendReconnectToNewts(exitNodeId);
|
||||
await sendReconnectToNewts(exitNodeId);
|
||||
await removePending(exitNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// async function sendReconnectToNewts(exitNodeId: number): Promise<void> {
|
||||
// try {
|
||||
// const connectedNewts = await db
|
||||
// .select({ newtId: newts.newtId })
|
||||
// .from(newts)
|
||||
// .innerJoin(sites, eq(newts.siteId, sites.siteId))
|
||||
// .where(eq(sites.exitNodeId, exitNodeId));
|
||||
async function sendReconnectToNewts(exitNodeId: number): Promise<void> {
|
||||
try {
|
||||
const connectedNewts = await db
|
||||
.select({ newtId: newts.newtId })
|
||||
.from(newts)
|
||||
.innerJoin(sites, eq(newts.siteId, sites.siteId))
|
||||
.where(eq(sites.exitNodeId, exitNodeId));
|
||||
|
||||
// if (connectedNewts.length === 0) {
|
||||
// logger.debug(
|
||||
// `No newts found for exit node ${exitNodeId}, nothing to reconnect`
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
if (connectedNewts.length === 0) {
|
||||
logger.debug(
|
||||
`No newts found for exit node ${exitNodeId}, nothing to reconnect`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// logger.info(
|
||||
// `Sending newt/wg/reconnect to ${connectedNewts.length} newt(s) for exit node ${exitNodeId}`
|
||||
// );
|
||||
logger.info(
|
||||
`Sending newt/wg/reconnect to ${connectedNewts.length} newt(s) for exit node ${exitNodeId}`
|
||||
);
|
||||
|
||||
// const reconnectMessage = {
|
||||
// type: "newt/wg/reconnect",
|
||||
// data: {}
|
||||
// };
|
||||
const reconnectMessage = {
|
||||
type: "newt/wg/reconnect",
|
||||
data: {}
|
||||
};
|
||||
|
||||
// await Promise.allSettled(
|
||||
// connectedNewts.map(({ newtId }) =>
|
||||
// sendToClient(newtId, reconnectMessage)
|
||||
// )
|
||||
// );
|
||||
// } catch (error) {
|
||||
// logger.error(
|
||||
// `Failed to send reconnect messages for exit node ${exitNodeId}`,
|
||||
// { error }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
await Promise.allSettled(
|
||||
connectedNewts.map(({ newtId }) =>
|
||||
sendToClient(newtId, reconnectMessage)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to send reconnect messages for exit node ${exitNodeId}`,
|
||||
{ error }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function removePending(exitNodeId: number): Promise<void> {
|
||||
pendingReconnects.delete(exitNodeId);
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
validateRemoteExitNodeSessionToken,
|
||||
EXPIRES
|
||||
} from "#private/auth/sessions/remoteExitNode";
|
||||
import { getOrCreateCachedToken } from "@server/private/lib/tokenCache";
|
||||
import { getOrCreateCachedToken } from "#private/lib/tokenCache";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
@@ -16,7 +16,7 @@ import { MessageHandler } from "@server/routers/ws";
|
||||
import { RemoteExitNode } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { scheduleExitNodeReconnect } from "./exitNodeReconnectScheduler";
|
||||
import { exitNodeEvents, EXIT_NODE_ONLINE_EVENT } from "./exitNodeEvents";
|
||||
|
||||
/**
|
||||
* Handles ping messages from clients and responds with pong
|
||||
@@ -40,7 +40,7 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
|
||||
try {
|
||||
// Fetch the current state before updating so we can detect the offline→online transition
|
||||
const [currentExitNode] = await db
|
||||
.select({ online: exitNodes.online, reachableAt: exitNodes.reachableAt })
|
||||
.select({ online: exitNodes.online, endpoint: exitNodes.endpoint })
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId))
|
||||
.limit(1);
|
||||
@@ -55,12 +55,14 @@ export const handleRemoteExitNodePingMessage: MessageHandler = async (
|
||||
.where(eq(exitNodes.exitNodeId, remoteExitNode.exitNodeId));
|
||||
|
||||
// If the exit node was offline and is now coming online, schedule newt reconnects
|
||||
if (currentExitNode && !currentExitNode.online && currentExitNode.reachableAt) {
|
||||
scheduleExitNodeReconnect(
|
||||
remoteExitNode.exitNodeId,
|
||||
currentExitNode.reachableAt
|
||||
).catch((error) => {
|
||||
logger.error("Failed to schedule exit node reconnect", { error });
|
||||
if (
|
||||
currentExitNode &&
|
||||
!currentExitNode.online &&
|
||||
currentExitNode.endpoint
|
||||
) {
|
||||
exitNodeEvents.emit(EXIT_NODE_ONLINE_EVENT, {
|
||||
exitNodeId: remoteExitNode.exitNodeId,
|
||||
endpoint: currentExitNode.endpoint
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -691,7 +691,9 @@ export async function verifyResourceSession(
|
||||
);
|
||||
|
||||
resourceSession = result?.resourceSession;
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
if (resourceSession) {
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceSession?.isRequestToken) {
|
||||
@@ -1121,7 +1123,9 @@ async function allowAccessToken(
|
||||
resource.resourceId
|
||||
);
|
||||
resourceSession = result?.resourceSession;
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
if (resourceSession) {
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -21,6 +21,7 @@ import { LimitId } from "@server/lib/billing";
|
||||
import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators";
|
||||
import { build } from "@server/build";
|
||||
import config from "@server/lib/config";
|
||||
import { createNs, createCname } from "#dynamic/lib/dns/generateDomains";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -283,8 +284,7 @@ export async function createOrgDomain(
|
||||
|
||||
// TODO: This needs to be cross region and not hardcoded
|
||||
if (type === "ns") {
|
||||
nsRecords = config.getRawConfig().dns.nameservers as string[];
|
||||
|
||||
nsRecords = createNs();
|
||||
// Save NS records to database
|
||||
for (const nsValue of nsRecords) {
|
||||
recordsToInsert.push({
|
||||
@@ -296,16 +296,7 @@ export async function createOrgDomain(
|
||||
});
|
||||
}
|
||||
} else if (type === "cname") {
|
||||
cnameRecords = [
|
||||
{
|
||||
value: `${domainId}.${config.getRawConfig().dns.cname_extension}`,
|
||||
baseDomain: baseDomain
|
||||
},
|
||||
{
|
||||
value: `_acme-challenge.${domainId}.${config.getRawConfig().dns.cname_extension}`,
|
||||
baseDomain: `_acme-challenge.${baseDomain}`
|
||||
}
|
||||
];
|
||||
cnameRecords = createCname(domainId, baseDomain);
|
||||
|
||||
// Save CNAME records to database
|
||||
for (const cnameRecord of cnameRecords) {
|
||||
|
||||
@@ -87,6 +87,12 @@ authenticated.get("/org/checkId", org.checkId);
|
||||
authenticated.put("/org", getUserOrgs, org.createOrg);
|
||||
|
||||
authenticated.get("/orgs", verifyUserIsServerAdmin, org.listOrgs);
|
||||
authenticated.get("/admin/orgs", verifyUserIsServerAdmin, org.adminListOrgs);
|
||||
authenticated.delete(
|
||||
"/admin/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
org.adminDeleteOrg
|
||||
);
|
||||
authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs);
|
||||
|
||||
authenticated.get(
|
||||
@@ -1378,6 +1384,12 @@ if (build !== "saas") {
|
||||
user.adminGeneratePasswordResetCode
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/server-admin",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminSetServerAdmin
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/user/:userId",
|
||||
verifyUserIsServerAdmin,
|
||||
|
||||
@@ -10,6 +10,7 @@ import config from "@server/lib/config";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { getAllowedIps } from "../target/helpers";
|
||||
import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
|
||||
import { markExitNodeCheckedIn } from "@server/lib/exitNodes";
|
||||
|
||||
// Define Zod schema for request validation
|
||||
const getConfigSchema = z.object({
|
||||
@@ -65,6 +66,8 @@ export async function getConfig(
|
||||
);
|
||||
}
|
||||
|
||||
markExitNodeCheckedIn(exitNode.exitNodeId);
|
||||
|
||||
const configResponse = await generateGerbilConfig(exitNode);
|
||||
|
||||
logger.debug("Sending config: ", configResponse);
|
||||
|
||||
@@ -522,6 +522,7 @@ async function fetchLabelsForResources(
|
||||
type SiteGroupRow = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
niceId: string;
|
||||
type: string;
|
||||
online: boolean;
|
||||
itemCount: number;
|
||||
@@ -556,6 +557,7 @@ async function listSiteGroups(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(resources.resourceId)
|
||||
@@ -576,7 +578,13 @@ async function listSiteGroups(
|
||||
|
||||
const publicRows = await publicQuery
|
||||
.where(and(...publicConditions))
|
||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||
.groupBy(
|
||||
sites.siteId,
|
||||
sites.name,
|
||||
sites.niceId,
|
||||
sites.type,
|
||||
sites.online
|
||||
);
|
||||
|
||||
for (const row of publicRows) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -586,6 +594,7 @@ async function listSiteGroups(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -612,6 +621,7 @@ async function listSiteGroups(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(siteResources.siteResourceId)
|
||||
@@ -638,7 +648,13 @@ async function listSiteGroups(
|
||||
|
||||
const siteRows = await siteResourceQuery
|
||||
.where(and(...siteConditions))
|
||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||
.groupBy(
|
||||
sites.siteId,
|
||||
sites.name,
|
||||
sites.niceId,
|
||||
sites.type,
|
||||
sites.online
|
||||
);
|
||||
|
||||
for (const row of siteRows) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -648,6 +664,7 @@ async function listSiteGroups(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -1061,6 +1078,43 @@ export async function listLauncherGroupsForUser(
|
||||
};
|
||||
}
|
||||
|
||||
function toLauncherSiteInfo(row: {
|
||||
siteId: number | null;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
siteType: string | null;
|
||||
siteOnline: boolean | null;
|
||||
}): LauncherSiteInfo | null {
|
||||
if (
|
||||
row.siteId == null ||
|
||||
row.siteName == null ||
|
||||
row.siteNiceId == null ||
|
||||
row.siteType == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
siteId: row.siteId,
|
||||
name: row.siteName,
|
||||
niceId: row.siteNiceId,
|
||||
type: row.siteType,
|
||||
online: row.siteOnline ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function pickPrimarySite(
|
||||
sites: LauncherSiteInfo[],
|
||||
siteIdFilter?: number
|
||||
): LauncherSiteInfo | undefined {
|
||||
if (sites.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (siteIdFilter != null) {
|
||||
return sites.find((site) => site.siteId === siteIdFilter) ?? sites[0];
|
||||
}
|
||||
return sites[0];
|
||||
}
|
||||
|
||||
async function mapPublicResources(
|
||||
orgId: string,
|
||||
resourceIds: number[],
|
||||
@@ -1084,6 +1138,7 @@ async function mapPublicResources(
|
||||
enabled: resources.enabled,
|
||||
siteId: sites.siteId,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteType: sites.type,
|
||||
siteOnline: sites.online,
|
||||
exitNodeEndpoint: exitNodes.endpoint
|
||||
@@ -1097,56 +1152,65 @@ 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
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const result: LauncherResource[] = [];
|
||||
const byKey = new Map<string, LauncherResource>();
|
||||
const siteIdsByKey = new Map<string, Set<number>>();
|
||||
|
||||
for (const row of rows) {
|
||||
const key = `public:${row.resourceId}`;
|
||||
if (seen.has(key)) {
|
||||
let item = byKey.get(key);
|
||||
|
||||
if (!item) {
|
||||
const access = formatPublicResourceAccess({
|
||||
mode: row.mode,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
proxyPort: row.proxyPort,
|
||||
wildcard: row.wildcard,
|
||||
exitNodeEndpoint: row.exitNodeEndpoint
|
||||
});
|
||||
|
||||
item = {
|
||||
launcherResourceKey: key,
|
||||
resourceType: "public",
|
||||
resourceId: row.resourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
||||
sites: []
|
||||
};
|
||||
byKey.set(key, item);
|
||||
siteIdsByKey.set(key, new Set());
|
||||
}
|
||||
|
||||
const site = toLauncherSiteInfo(row);
|
||||
if (!site) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
|
||||
const access = formatPublicResourceAccess({
|
||||
mode: row.mode,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
proxyPort: row.proxyPort,
|
||||
wildcard: row.wildcard,
|
||||
exitNodeEndpoint: row.exitNodeEndpoint
|
||||
});
|
||||
|
||||
result.push({
|
||||
launcherResourceKey: key,
|
||||
resourceType: "public",
|
||||
resourceId: row.resourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
||||
site:
|
||||
row.siteId != null
|
||||
? {
|
||||
siteId: row.siteId,
|
||||
name: row.siteName!,
|
||||
type: row.siteType!,
|
||||
online: row.siteOnline ?? undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||
if (seenSiteIds.has(site.siteId)) {
|
||||
continue;
|
||||
}
|
||||
seenSiteIds.add(site.siteId);
|
||||
item.sites.push(site);
|
||||
}
|
||||
|
||||
return result;
|
||||
for (const item of byKey.values()) {
|
||||
item.sites.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||
);
|
||||
item.site = pickPrimarySite(item.sites, siteIdFilter);
|
||||
}
|
||||
|
||||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
async function mapSiteResources(
|
||||
@@ -1175,6 +1239,7 @@ async function mapSiteResources(
|
||||
enabled: siteResources.enabled,
|
||||
siteId: sites.siteId,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteType: sites.type,
|
||||
siteOnline: sites.online
|
||||
})
|
||||
@@ -1189,59 +1254,69 @@ 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
|
||||
eq(siteResources.status, "approved")
|
||||
)
|
||||
);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const result: LauncherResource[] = [];
|
||||
const byKey = new Map<string, LauncherResource>();
|
||||
const siteIdsByKey = new Map<string, Set<number>>();
|
||||
|
||||
for (const row of rows) {
|
||||
const key = `site:${row.siteResourceId}`;
|
||||
if (seen.has(key)) {
|
||||
let item = byKey.get(key);
|
||||
|
||||
if (!item) {
|
||||
const access = formatSiteResourceAccess({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme,
|
||||
ssl: row.ssl,
|
||||
fullDomain: row.fullDomain,
|
||||
alias: row.alias,
|
||||
aliasAddress: row.aliasAddress
|
||||
});
|
||||
|
||||
item = {
|
||||
launcherResourceKey: key,
|
||||
resourceType: "site",
|
||||
resourceId: row.siteResourceId,
|
||||
siteResourceId: row.siteResourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels:
|
||||
labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
||||
sites: []
|
||||
};
|
||||
byKey.set(key, item);
|
||||
siteIdsByKey.set(key, new Set());
|
||||
}
|
||||
|
||||
const site = toLauncherSiteInfo(row);
|
||||
if (!site) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
|
||||
const access = formatSiteResourceAccess({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme,
|
||||
ssl: row.ssl,
|
||||
fullDomain: row.fullDomain,
|
||||
alias: row.alias,
|
||||
aliasAddress: row.aliasAddress
|
||||
});
|
||||
|
||||
result.push({
|
||||
launcherResourceKey: key,
|
||||
resourceType: "site",
|
||||
resourceId: row.siteResourceId,
|
||||
siteResourceId: row.siteResourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
||||
site:
|
||||
row.siteId != null
|
||||
? {
|
||||
siteId: row.siteId,
|
||||
name: row.siteName!,
|
||||
type: row.siteType!,
|
||||
online: row.siteOnline ?? undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||
if (seenSiteIds.has(site.siteId)) {
|
||||
continue;
|
||||
}
|
||||
seenSiteIds.add(site.siteId);
|
||||
item.sites.push(site);
|
||||
}
|
||||
|
||||
return result;
|
||||
for (const item of byKey.values()) {
|
||||
item.sites.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||
);
|
||||
item.site = pickPrimarySite(item.sites, siteIdFilter);
|
||||
}
|
||||
|
||||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
function filterResourcesBySite(
|
||||
@@ -1252,13 +1327,17 @@ function filterResourcesBySite(
|
||||
return items.filter((item) => item.mode === "inference");
|
||||
}
|
||||
if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
|
||||
return items.filter((item) => !item.site && item.mode !== "inference");
|
||||
return items.filter(
|
||||
(item) => item.sites.length === 0 && item.mode !== "inference"
|
||||
);
|
||||
}
|
||||
const siteId = Number.parseInt(groupKey, 10);
|
||||
if (!Number.isFinite(siteId)) {
|
||||
return items;
|
||||
}
|
||||
return items.filter((item) => item.site?.siteId === siteId);
|
||||
return items.filter((item) =>
|
||||
item.sites.some((site) => site.siteId === siteId)
|
||||
);
|
||||
}
|
||||
|
||||
function filterResourcesByLabel(
|
||||
@@ -1499,6 +1578,7 @@ async function collectAccessibleSites(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(resources.resourceId)
|
||||
@@ -1507,7 +1587,13 @@ async function collectAccessibleSites(
|
||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||
.where(and(...publicConditions))
|
||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||
.groupBy(
|
||||
sites.siteId,
|
||||
sites.name,
|
||||
sites.niceId,
|
||||
sites.type,
|
||||
sites.online
|
||||
);
|
||||
|
||||
for (const row of publicRows) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -1517,6 +1603,7 @@ async function collectAccessibleSites(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -1540,6 +1627,7 @@ async function collectAccessibleSites(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(siteResources.siteResourceId)
|
||||
@@ -1551,7 +1639,13 @@ async function collectAccessibleSites(
|
||||
)
|
||||
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||
.where(and(...siteConditions))
|
||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||
.groupBy(
|
||||
sites.siteId,
|
||||
sites.name,
|
||||
sites.niceId,
|
||||
sites.type,
|
||||
sites.online
|
||||
);
|
||||
|
||||
for (const row of siteRows) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -1561,6 +1655,7 @@ async function collectAccessibleSites(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -1675,6 +1770,7 @@ export async function listAccessibleLauncherSitesForUser(
|
||||
.map((row) => ({
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online
|
||||
}))
|
||||
|
||||
@@ -32,6 +32,7 @@ export type LauncherLabel = {
|
||||
export type LauncherSiteInfo = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
niceId: string;
|
||||
type: string;
|
||||
online?: boolean;
|
||||
};
|
||||
@@ -51,6 +52,7 @@ export type LauncherResource = {
|
||||
mode: string;
|
||||
labels: LauncherLabel[];
|
||||
site?: LauncherSiteInfo;
|
||||
sites: LauncherSiteInfo[];
|
||||
};
|
||||
|
||||
export type LauncherGroup = {
|
||||
@@ -184,8 +186,7 @@ export function parseIdListParam(value: string | undefined): number[] {
|
||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||
|
||||
export type LauncherViewSelection =
|
||||
| { type: "default" }
|
||||
| { type: "saved"; viewId: number };
|
||||
{ type: "default" } | { type: "saved"; viewId: number };
|
||||
|
||||
export type LauncherScaleCapabilities = {
|
||||
allowSiteGrouping: boolean;
|
||||
|
||||
@@ -13,31 +13,40 @@ import logger from "@server/logger";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
// Stale-while-revalidate in-memory fallback for the releases API.
|
||||
type ReleaseInfo = {
|
||||
version: string;
|
||||
// binary filename -> sha256 hex (sourced from asset `digest` field in GitHub API)
|
||||
assetDigests: Record<string, string>;
|
||||
};
|
||||
let staleReleaseInfo: ReleaseInfo | null = null;
|
||||
|
||||
// Cache key holding the last known good release info. It never expires, so
|
||||
// it keeps serving if GitHub is unreachable, even across restarts/nodes.
|
||||
const RELEASE_INFO_KEY = "cache:releaseInfo";
|
||||
// Short-lived marker controlling how often we re-check GitHub. While it's
|
||||
// missing (expired, or a previous attempt failed) every request retries.
|
||||
const RELEASE_INFO_FRESH_KEY = "cache:releaseInfoFresh";
|
||||
const RELEASE_INFO_REFRESH_SECONDS = 3600;
|
||||
|
||||
/**
|
||||
* Fetches the latest stable newt release from GitHub and returns the version
|
||||
* tag together with a map of asset-name → sha256 hex digest.
|
||||
* Results are cached for one hour; stale data is returned on failure.
|
||||
* The last successful result is cached indefinitely and re-checked hourly;
|
||||
* on failure the last known good data keeps being served and every
|
||||
* subsequent request retries GitHub until it succeeds again.
|
||||
*/
|
||||
async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
try {
|
||||
const cached = await cache.get<ReleaseInfo>("cache:newtReleaseInfo");
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
async function getLatestReleaseInfo(repo: string): Promise<ReleaseInfo | null> {
|
||||
const stored = await cache.get<ReleaseInfo>(`${RELEASE_INFO_KEY}:${repo}`);
|
||||
const isFresh = await cache.has(`${RELEASE_INFO_FRESH_KEY}:${repo}`);
|
||||
if (stored && isFresh) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const fetchResponse = await fetch(
|
||||
"https://api.github.com/repos/fosrl/newt/releases",
|
||||
`https://api.github.com/repos/fosrl/${repo}/releases`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
@@ -47,13 +56,13 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
logger.warn(
|
||||
`Failed to fetch Newt releases from GitHub: ${fetchResponse.status} ${fetchResponse.statusText}`
|
||||
);
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
let releases: any[] = await fetchResponse.json();
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
logger.warn("No releases found for Newt repository");
|
||||
return staleReleaseInfo;
|
||||
logger.warn("No releases found for repository");
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
@@ -81,8 +90,8 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
});
|
||||
|
||||
if (releases.length === 0) {
|
||||
logger.warn("No stable releases found for Newt repository");
|
||||
return staleReleaseInfo;
|
||||
logger.warn("No stable releases found for repository");
|
||||
return stored ?? null;
|
||||
}
|
||||
|
||||
const latest = releases[0];
|
||||
@@ -106,8 +115,12 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
}
|
||||
|
||||
const info: ReleaseInfo = { version, assetDigests };
|
||||
staleReleaseInfo = info;
|
||||
await cache.set("cache:newtReleaseInfo", info, 3600);
|
||||
await cache.set(RELEASE_INFO_KEY, info, 0);
|
||||
await cache.set(
|
||||
RELEASE_INFO_FRESH_KEY,
|
||||
true,
|
||||
RELEASE_INFO_REFRESH_SECONDS
|
||||
);
|
||||
return info;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
@@ -118,14 +131,15 @@ async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return staleReleaseInfo;
|
||||
return stored ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const bodySchema = z.object({
|
||||
newtId: z.string(),
|
||||
secret: z.string(),
|
||||
platform: z.string() // e.g. "linux_amd64", "darwin_arm64"
|
||||
platform: z.string(), // e.g. "linux_amd64", "darwin_arm64"
|
||||
agent: z.string().optional().default("newt")
|
||||
});
|
||||
|
||||
export type GetNewtVersionBody = z.infer<typeof bodySchema>;
|
||||
@@ -153,7 +167,7 @@ export async function getNewtVersion(
|
||||
);
|
||||
}
|
||||
|
||||
const { newtId, secret, platform } = parsedBody.data;
|
||||
const { newtId, secret, platform, agent } = parsedBody.data;
|
||||
|
||||
try {
|
||||
// Verify newt credentials
|
||||
@@ -258,9 +272,13 @@ export async function getNewtVersion(
|
||||
}
|
||||
|
||||
// Fetch latest release info (version + asset digests) in one API call.
|
||||
const releaseInfo = await getLatestReleaseInfo();
|
||||
const releaseInfoNewt = await getLatestReleaseInfo("newt");
|
||||
let releaseInfoCli: ReleaseInfo | undefined | null;
|
||||
if (agent == "cli") {
|
||||
releaseInfoCli = await getLatestReleaseInfo("cli");
|
||||
}
|
||||
|
||||
if (!releaseInfo) {
|
||||
if (!releaseInfoNewt || (agent == "cli" && !releaseInfoCli)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
@@ -269,18 +287,25 @@ export async function getNewtVersion(
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersion = releaseInfo.version;
|
||||
const latestVersion = releaseInfoNewt.version;
|
||||
|
||||
// Binary name follows the get-newt.sh convention: newt_<platform>[.exe]
|
||||
const binaryName = platform.includes("windows")
|
||||
const binaryNameNewt = platform.includes("windows")
|
||||
? `newt_${platform}.exe`
|
||||
: `newt_${platform}`;
|
||||
|
||||
const downloadUrl = `https://github.com/fosrl/newt/releases/download/${latestVersion}/${binaryName}`;
|
||||
const binaryNameCli = platform.includes("windows")
|
||||
? `pangolin-cli_${platform}.exe`
|
||||
: `pangolin-cli_${platform}`;
|
||||
|
||||
const downloadUrl = `https://github.com/fosrl/${agent}/releases/download/${agent == "cli" ? releaseInfoCli?.version : releaseInfoNewt.version}/${agent == "cli" ? binaryNameCli : binaryNameNewt}`;
|
||||
|
||||
// Look up the SHA256 digest for this specific binary from the GitHub
|
||||
// release asset metadata (the `digest` field, format "sha256:<hex>").
|
||||
const sha256 = releaseInfo.assetDigests[binaryName] ?? "";
|
||||
const sha256 =
|
||||
releaseInfoNewt.assetDigests[
|
||||
agent == "cli" ? binaryNameCli : binaryNameNewt
|
||||
] ?? "";
|
||||
|
||||
// Determine whether the newt that's asking is already up to date.
|
||||
// We store the current version on the newt row when it registers.
|
||||
@@ -300,8 +325,8 @@ export async function getNewtVersion(
|
||||
|
||||
return response<GetNewtVersionResponse>(res, {
|
||||
data: {
|
||||
latestVersion,
|
||||
currentIsLatest,
|
||||
latestVersion, // this will always be the newt version
|
||||
currentIsLatest, // this will always be based on the newt version
|
||||
downloadUrl,
|
||||
sha256
|
||||
},
|
||||
|
||||
@@ -37,6 +37,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
publicKey,
|
||||
pingResults,
|
||||
newtVersion,
|
||||
agent,
|
||||
agentVersion,
|
||||
backwardsCompatible,
|
||||
chainId
|
||||
} = message.data;
|
||||
@@ -169,22 +171,21 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
logger.error(`Failed to add peer to exit node: ${error}`);
|
||||
}
|
||||
|
||||
if (newtVersion && newtVersion !== newt.version) {
|
||||
if (
|
||||
newtVersion !== newt.version ||
|
||||
agent !== newt.agent ||
|
||||
agentVersion !== newt.agentVersion
|
||||
) {
|
||||
// update the newt version in the database
|
||||
await db
|
||||
.update(newts)
|
||||
.set({
|
||||
version: newtVersion as string
|
||||
})
|
||||
.where(eq(newts.newtId, newt.newtId));
|
||||
}
|
||||
|
||||
if (newtVersion && newtVersion !== newt.version) {
|
||||
// update the newt version in the database
|
||||
await db
|
||||
.update(newts)
|
||||
.set({
|
||||
version: newtVersion as string
|
||||
version: newtVersion as string,
|
||||
agent: agent,
|
||||
agentVersion:
|
||||
!agentVersion && agent == "newt"
|
||||
? newtVersion
|
||||
: agentVersion
|
||||
})
|
||||
.where(eq(newts.newtId, newt.newtId));
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
/*
|
||||
* 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 { db } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { sites, Newt, orgs, clients, clientSitesAssociationsCache, users } from "@server/db";
|
||||
|
||||
@@ -85,7 +85,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
);
|
||||
|
||||
if (!resources || resources.length === 0) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Resource not found`
|
||||
);
|
||||
await sendCancel();
|
||||
@@ -94,7 +94,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
|
||||
if (resources.length > 1) {
|
||||
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Multiple resources found matching the criteria`
|
||||
);
|
||||
return;
|
||||
@@ -119,7 +119,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
);
|
||||
|
||||
if (currentResourceAssociationCaches.length === 0) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Client ${client.clientId} does not have access to resource ${resource.siteResourceId}`
|
||||
);
|
||||
await sendCancel();
|
||||
@@ -127,7 +127,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
}
|
||||
|
||||
if (!resource.networkId) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Resource ${resource.siteResourceId} has no network`
|
||||
);
|
||||
await sendCancel();
|
||||
@@ -141,7 +141,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
.where(eq(siteNetworks.networkId, resource.networkId));
|
||||
|
||||
if (!siteRows || siteRows.length === 0) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: No sites found for resource ${resource.siteResourceId}`
|
||||
);
|
||||
await sendCancel();
|
||||
@@ -164,9 +164,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
}
|
||||
|
||||
if (sitesToProcess.length === 0) {
|
||||
logger.error(
|
||||
`handleOlmServerInitAddPeerHandshake: No sites to process`
|
||||
);
|
||||
logger.warn(`handleOlmServerInitAddPeerHandshake: No sites to process`);
|
||||
await sendCancel();
|
||||
return;
|
||||
}
|
||||
@@ -193,7 +191,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
}
|
||||
|
||||
if (!site.exitNodeId) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Site ${site.siteId} has no exit node, skipping`
|
||||
);
|
||||
continue;
|
||||
@@ -205,7 +203,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId));
|
||||
|
||||
if (!exitNode) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: Exit node not found for site ${site.siteId}, skipping`
|
||||
);
|
||||
continue;
|
||||
@@ -229,7 +227,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
}
|
||||
|
||||
if (!handshakeInitiated) {
|
||||
logger.error(
|
||||
logger.warn(
|
||||
`handleOlmServerInitAddPeerHandshake: No accessible sites with valid exit nodes found, cancelling chain`
|
||||
);
|
||||
await sendCancel();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
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";
|
||||
import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const adminDeleteOrgSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export type AdminDeleteOrgResponse = {};
|
||||
|
||||
registry.registerPath({
|
||||
method: "delete",
|
||||
path: "/admin/org/{orgId}",
|
||||
description: "Delete any organization in the system (server admin).",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
params: adminDeleteOrgSchema
|
||||
},
|
||||
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 adminDeleteOrg(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = adminDeleteOrgSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Organization with ID ${orgId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const result = await deleteOrgById(orgId);
|
||||
sendTerminationMessages(result);
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organization deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred..."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, users } from "@server/db";
|
||||
import { orgs, resources, sites, userOrgs } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { and, asc, desc, eq, like, or, sql, type SQL } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
|
||||
const adminListOrgsSchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 20,
|
||||
description: "Number of items per page"
|
||||
}),
|
||||
page: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(1)
|
||||
.default(1)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
}),
|
||||
query: z.string().optional(),
|
||||
sort_by: z
|
||||
.enum(["name", "createdAt"])
|
||||
.optional()
|
||||
.catch(undefined)
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["name", "createdAt"],
|
||||
description: "Field to sort by"
|
||||
}),
|
||||
order: z
|
||||
.enum(["asc", "desc"])
|
||||
.optional()
|
||||
.default("asc")
|
||||
.catch("asc")
|
||||
.openapi({
|
||||
type: "string",
|
||||
enum: ["asc", "desc"],
|
||||
default: "asc",
|
||||
description: "Sort order"
|
||||
})
|
||||
});
|
||||
|
||||
export type AdminOrgRow = {
|
||||
orgId: string;
|
||||
name: string;
|
||||
subnet: string | null;
|
||||
utilitySubnet: string | null;
|
||||
createdAt: string | null;
|
||||
userCount: number;
|
||||
siteCount: number;
|
||||
resourceCount: number;
|
||||
owner: {
|
||||
userId: string;
|
||||
username: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type AdminListOrgsResponse = PaginatedResponse<{
|
||||
orgs: AdminOrgRow[];
|
||||
}>;
|
||||
|
||||
const AdminListOrgsResponseDataSchema = z.object({
|
||||
orgs: z.array(
|
||||
z.object({
|
||||
orgId: z.string(),
|
||||
name: z.string(),
|
||||
subnet: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
userCount: z.number(),
|
||||
siteCount: z.number(),
|
||||
resourceCount: z.number()
|
||||
})
|
||||
),
|
||||
pagination: z.object({
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
pageSize: z.number()
|
||||
})
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/admin/orgs",
|
||||
description:
|
||||
"List all organizations in the system with usage counts (server admin).",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
query: adminListOrgsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: createApiResponseSchema(
|
||||
AdminListOrgsResponseDataSchema
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function adminListOrgs(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = adminListOrgsSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { pageSize, page, query, sort_by, order } = parsedQuery.data;
|
||||
|
||||
let conditions: (SQL<unknown> | undefined)[] = [];
|
||||
if (query) {
|
||||
const q = "%" + query.toLowerCase() + "%";
|
||||
conditions.push(
|
||||
or(
|
||||
like(sql`LOWER(${orgs.name})`, q),
|
||||
like(sql`LOWER(${orgs.orgId})`, q),
|
||||
like(sql`LOWER(${orgs.subnet})`, q)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sortColumns = {
|
||||
name: orgs.name,
|
||||
createdAt: orgs.createdAt
|
||||
} as const;
|
||||
|
||||
const orderBy = sort_by
|
||||
? order === "asc"
|
||||
? asc(sortColumns[sort_by])
|
||||
: desc(sortColumns[sort_by])
|
||||
: asc(orgs.name);
|
||||
|
||||
// Drizzle renders bare column references in the select list without their
|
||||
// table prefix, which would make a correlated subquery compare a column to
|
||||
// itself, so the outer `orgs` side is qualified explicitly.
|
||||
const orgIdRef = sql`${sql.identifier("orgs")}.${sql.identifier("orgId")}`;
|
||||
|
||||
const [countRows, rows] = await Promise.all([
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(orgs)
|
||||
.where(and(...conditions)),
|
||||
db
|
||||
.selectDistinct({
|
||||
orgId: orgs.orgId,
|
||||
name: orgs.name,
|
||||
subnet: orgs.subnet,
|
||||
utilitySubnet: orgs.utilitySubnet,
|
||||
createdAt: orgs.createdAt,
|
||||
userCount: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM ${userOrgs}
|
||||
WHERE ${userOrgs.orgId} = ${orgIdRef}
|
||||
)`.as("userCount"),
|
||||
siteCount: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM ${sites}
|
||||
WHERE ${sites.orgId} = ${orgIdRef}
|
||||
)`.as("siteCount"),
|
||||
resourceCount: sql<number>`(
|
||||
SELECT COUNT(*)
|
||||
FROM ${resources}
|
||||
WHERE ${resources.orgId} = ${orgIdRef}
|
||||
)`.as("resourceCount"),
|
||||
owner: {
|
||||
userId: users.userId,
|
||||
username: users.username
|
||||
}
|
||||
})
|
||||
.from(orgs)
|
||||
.where(and(...conditions, eq(userOrgs.isOwner, true)))
|
||||
.leftJoin(userOrgs, eq(userOrgs.orgId, orgs.orgId))
|
||||
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
||||
.limit(pageSize)
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(orderBy)
|
||||
]);
|
||||
|
||||
const totalCount = Number(countRows[0]?.count ?? 0);
|
||||
|
||||
return response<AdminListOrgsResponse>(res, {
|
||||
data: {
|
||||
orgs: rows.map((row) => ({
|
||||
...row,
|
||||
userCount: Number(row.userCount ?? 0),
|
||||
siteCount: Number(row.siteCount ?? 0),
|
||||
resourceCount: Number(row.resourceCount ?? 0)
|
||||
})),
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organizations retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred..."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,5 @@ export * from "./listOrgs";
|
||||
export * from "./pickOrgDefaults";
|
||||
export * from "./checkOrgUserAccess";
|
||||
export * from "./resetOrgBandwidth";
|
||||
export * from "./adminListOrgs";
|
||||
export * from "./adminDeleteOrg";
|
||||
|
||||
@@ -48,6 +48,8 @@ type SiteQueryRow = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
export type GetSiteResponse = SiteQueryRow["sites"] & {
|
||||
newtId: string | null;
|
||||
newtVersion: string | null;
|
||||
agent: string | null;
|
||||
agentVersion: string | null;
|
||||
countryCode: string | null;
|
||||
};
|
||||
|
||||
@@ -137,6 +139,8 @@ export async function getSite(
|
||||
...site.sites,
|
||||
newtId: site.newt ? site.newt.newtId : null,
|
||||
newtVersion: site.newt?.version ?? null,
|
||||
agent: site.newt?.agent ?? null,
|
||||
agentVersion: site.newt?.agentVersion ?? null,
|
||||
countryCode: site.sites.endpoint
|
||||
? ((await getCountryCodeForIp(site.sites.endpoint)) ?? null)
|
||||
: null
|
||||
|
||||
@@ -133,6 +133,8 @@ function querySitesBase() {
|
||||
online: sites.online,
|
||||
address: sites.address,
|
||||
newtVersion: newts.version,
|
||||
agent: newts.agent,
|
||||
agentVersion: newts.agentVersion,
|
||||
exitNodeId: sites.exitNodeId,
|
||||
exitNodeName: exitNodes.name,
|
||||
exitNodeEndpoint: exitNodes.endpoint,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user