Compare commits
162 Commits
cross-org-
...
6969671fc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6969671fc4 | ||
|
|
e765f661a7 | ||
|
|
7da3719a00 | ||
|
|
206b3a7d22 | ||
|
|
ed327626bb | ||
|
|
b59262b7af | ||
|
|
8093904d47 | ||
|
|
66c0ed5bf0 | ||
|
|
725603101b | ||
|
|
7f0264dec3 | ||
|
|
5e88862e29 | ||
|
|
b3bc70875b | ||
|
|
34dc4c2d07 | ||
|
|
2ef7a709d3 | ||
|
|
d7a9e1a517 | ||
|
|
f938e9c3c0 | ||
|
|
c8d560d78f | ||
|
|
3641969dd4 | ||
|
|
49b3163bbe | ||
|
|
1a36475afa | ||
|
|
335de04a4e | ||
|
|
f38069623b | ||
|
|
0a70896080 | ||
|
|
5a09062070 | ||
|
|
47be3dbdf9 | ||
|
|
9f5f89c9eb | ||
|
|
2e8d170114 | ||
|
|
bf1787acd5 | ||
|
|
78ff835ac9 | ||
|
|
3c005c9ab1 | ||
|
|
54adcd2c56 | ||
|
|
9a6408d28c | ||
|
|
2dad97cb6b | ||
|
|
0ac97ecd5e | ||
|
|
387049beac | ||
|
|
c9240ecb84 | ||
|
|
b87e71c557 | ||
|
|
866293aa5a | ||
|
|
e142dd32b4 | ||
|
|
949786dab5 | ||
|
|
2dd142b0e9 | ||
|
|
dfd16a6752 | ||
|
|
f4454d4d48 | ||
|
|
e7efc917f0 | ||
|
|
5ffe1ba07d | ||
|
|
b56e2972c4 | ||
|
|
ca1a084397 | ||
|
|
a7a1f81e9d | ||
|
|
9c09f17dc5 | ||
|
|
21e2c022c7 | ||
|
|
222cbc886d | ||
|
|
db2e76bd31 | ||
|
|
bf32cc150d | ||
|
|
967de0b79f | ||
|
|
22231e6c45 | ||
|
|
20ed9966b9 | ||
|
|
b2d5a1ffdf | ||
|
|
a5b8a44e78 | ||
|
|
dddf060e1a | ||
|
|
df8104fe56 | ||
|
|
8214700eaa | ||
|
|
74165aa1cc | ||
|
|
0872fd5818 | ||
|
|
008ad0a1de | ||
|
|
f74791111e | ||
|
|
22964cff0f | ||
|
|
e952c2d34a | ||
|
|
408eaf55f6 | ||
|
|
0a043af482 | ||
|
|
bd89867ecb | ||
|
|
3645cc5759 | ||
|
|
f932cc7aca | ||
|
|
c1782a2650 | ||
|
|
d6c15c8b81 | ||
|
|
b958537f3e | ||
|
|
597cae2b78 | ||
|
|
c4308aaa69 | ||
|
|
a9d68bd0cf | ||
|
|
5fcb80a193 | ||
|
|
79c6fcac95 | ||
|
|
57579e635c | ||
|
|
1a1d1cfb83 | ||
|
|
1397e61643 | ||
|
|
a04e2a5e00 | ||
|
|
b169a872a7 | ||
|
|
1d4b2b1da1 | ||
|
|
ad15b7c3c6 | ||
|
|
b070570cb6 | ||
|
|
55595ec042 | ||
|
|
5e505224d0 | ||
|
|
3c6775992d | ||
|
|
bf64e226d3 | ||
|
|
f379986a59 | ||
|
|
22ead84aa7 | ||
|
|
570ff75164 | ||
|
|
87a554b6ef | ||
|
|
cf741a6f87 | ||
|
|
7d50703c26 | ||
|
|
33182bcf85 | ||
|
|
fb29efeff3 | ||
|
|
7d13ed79b2 | ||
|
|
49ae5eecb6 | ||
|
|
9fe4f78269 | ||
|
|
646e440dec | ||
|
|
bd3d6994c1 | ||
|
|
5fd78817a8 | ||
|
|
1b9a395432 | ||
|
|
3996e14e70 | ||
|
|
7a40084bf4 | ||
|
|
30fd48a14a | ||
|
|
72bc125f84 | ||
|
|
173a81ead8 | ||
|
|
676eacc9cf | ||
|
|
b18ea66def | ||
|
|
41f541a531 | ||
|
|
561a9ab379 | ||
|
|
71497a7887 | ||
|
|
aa41a63430 | ||
|
|
0db55daff6 | ||
|
|
9b271950d2 | ||
|
|
89b6b1fb56 | ||
|
|
789b991c56 | ||
|
|
0cbcc0c29c | ||
|
|
b5e239d1ad | ||
|
|
5f79e8ebbd | ||
|
|
1564c4bee7 | ||
|
|
0cf385b718 | ||
|
|
83ecf53776 | ||
|
|
5803da4893 | ||
|
|
fc4633db91 | ||
|
|
9e50569c31 | ||
|
|
a19f0acfb9 | ||
|
|
8a47d69d0d | ||
|
|
73482c2a05 | ||
|
|
79751c208d | ||
|
|
510931e7d6 | ||
|
|
584a8e7d1d | ||
|
|
a74378e1d3 | ||
|
|
c027c8958b | ||
|
|
a730f4da1d | ||
|
|
d73796b92e | ||
|
|
96b9123306 | ||
|
|
e4cbf088b4 | ||
|
|
333ccb8438 | ||
|
|
eb771ceda4 | ||
|
|
1efd2af44b | ||
|
|
4cce6e0820 | ||
|
|
2841c5ed4e | ||
|
|
02033f611f | ||
|
|
1366901e24 | ||
|
|
c4f48f5748 | ||
|
|
c48bc71443 | ||
|
|
d85496453f | ||
|
|
21b91374a3 | ||
|
|
a1ce7f54a0 | ||
|
|
87524fe8ae | ||
|
|
2093bb5357 | ||
|
|
6f2e37948c | ||
|
|
b7421e47cc | ||
|
|
7cbe3d42a1 | ||
|
|
d8b511b198 | ||
|
|
102a235407 |
@@ -53,9 +53,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
|
||||
|
||||
## Deployment Options
|
||||
|
||||
- **Pangolin Cloud** — Fully managed service - no infrastructure required.
|
||||
- **Self-Host: Community Edition** — Free, open source, and licensed under AGPL-3.
|
||||
- **Self-Host: Enterprise Edition** — Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
|
||||
- **Pangolin Cloud** - Fully managed service - no infrastructure required.
|
||||
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3.
|
||||
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
|
||||
|
||||
## Key Features
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: Create API Key
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/api-key
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"isRoot": true
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Delete API Key
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
delete {
|
||||
url: http://localhost:3000/api/v1/api-key/dm47aacqxxn3ubj
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: List API Key Actions
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: List Org API Keys
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: List Root API Keys
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/root/api-keys
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: Set API Key Actions
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/actions
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"actionIds": ["listSites"]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: Set API Key Orgs
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/api-key/ex0izu2c37fjz9x/orgs
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"orgIds": ["home-lab"]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
meta {
|
||||
name: API Keys
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
meta {
|
||||
name: 2fa-disable
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/disable
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"password": "aaaaa-1A",
|
||||
"code": "377289"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: 2fa-enable
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/enable
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"code": "374138"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: 2fa-request
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/2fa/request
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"password": "aaaaa-1A"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
meta {
|
||||
name: change-password
|
||||
type: http
|
||||
seq: 9
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/change-password
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"oldPassword": "",
|
||||
"newPassword": ""
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
meta {
|
||||
name: login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/login
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "admin@fosrl.io",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: logout
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:4000/api/v1/auth/logout
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: reset-password-request
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/reset-password/request
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "milo@pangolin.net"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: reset-password
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/reset-password
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"token": "3uhsbom72dwdhboctwrtntyd6jrlg4jtf5oaxy4k",
|
||||
"newPassword": "aaaaa-1A",
|
||||
"code": "6irqCGR3"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
meta {
|
||||
name: signup
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/auth/signup
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "numbat@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: verify-email-request
|
||||
type: http
|
||||
seq: 8
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/verify-email/request
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
meta {
|
||||
name: verify-email
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:3000/api/v1/auth/verify-email
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"code": "50317187"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: verify-user
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/badger/verify-user?sessionId=mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
sessionId: mb52273jkb6t3oys2bx6ur5x7rcrkl26c7warg3e
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: createClient
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/site/1/client
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"siteId": 1,
|
||||
"name": "test",
|
||||
"type": "olm",
|
||||
"subnet": "100.90.129.4/30",
|
||||
"olmId": "029yzunhx6nh3y5",
|
||||
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: pickClientDefaults
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Create OIDC Provider
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/org/home-lab/idp/oidc
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"clientId": "JJoSvHCZcxnXT2sn6CObj6a21MuKNRXs3kN5wbys",
|
||||
"clientSecret": "2SlGL2wOGgMEWLI9yUuMAeFxre7qSNJVnXMzyepdNzH1qlxYnC4lKhhQ6a157YQEkYH3vm40KK4RCqbYiF8QIweuPGagPX3oGxEj2exwutoXFfOhtq4hHybQKoFq01Z3",
|
||||
"authUrl": "http://localhost:9000/application/o/authorize/",
|
||||
"tokenUrl": "http://localhost:9000/application/o/token/",
|
||||
"scopes": ["email", "openid", "profile"],
|
||||
"userIdentifier": "email"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Generate OIDC URL
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
meta {
|
||||
name: IDP
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Traefik Config
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/traefik-config
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
meta {
|
||||
name: Internal
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Create Newt
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/newt
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
meta {
|
||||
name: Get Token
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/auth/newt/get-token
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"newtId": "o0d4rdxq3stnz7b",
|
||||
"secret": "sy7l09fnaesd03iwrfp9m3qf0ryn19g0zf3dqieaazb4k7vk"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: createOlm
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:3000/api/v1/olm
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
meta {
|
||||
name: Olm
|
||||
seq: 15
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Check Id
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/checkId
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: listOrgs
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: createRemoteExitNode
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
put {
|
||||
url: http://localhost:4000/api/v1/org/org_i21aifypnlyxur2/remote-exit-node
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: listResourcesByOrg
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: listResourcesBySite
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/site/1/resources?limit=10&offset=0
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
limit: 10
|
||||
offset: 0
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Get Site
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/org/test/sites/mexican-mole-lizard-windy
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: listSites
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: listTargets
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/resource/web.main.localhost/targets?limit=10&offset=0
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
params:query {
|
||||
limit: 10
|
||||
offset: 0
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: Test
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: traefik-config
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3001/api/v1/traefik-config
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: adminListUsers
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:3000/api/v1/users
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: adminRemoveUser
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
delete {
|
||||
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: getUser
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url:
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Pangolin",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"presets": {
|
||||
"requestType": "http",
|
||||
"requestUrl": "http://localhost:3000/api/v1"
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ def process_directory(root_dir):
|
||||
if has_correct_header:
|
||||
print(f"Header up-to-date: {file_path}")
|
||||
else:
|
||||
# Either no header exists or the header is outdated — write
|
||||
# Either no header exists or the header is outdated - write
|
||||
# the correct one.
|
||||
action = "Replaced header in" if has_any_header else "Added header to"
|
||||
new_content = HEADER_NORMALIZED + '\n\n' + body
|
||||
@@ -106,7 +106,7 @@ def process_directory(root_dir):
|
||||
files_modified += 1
|
||||
else:
|
||||
if has_any_header:
|
||||
# Remove the header — it shouldn't be here.
|
||||
# Remove the header - it shouldn't be here.
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(body)
|
||||
print(f"Removed header from: {file_path}")
|
||||
@@ -134,4 +134,4 @@ if __name__ == "__main__":
|
||||
print(f"Error: Directory '{target_directory}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
process_directory(os.path.abspath(target_directory))
|
||||
process_directory(os.path.abspath(target_directory))
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Име за показване за този доставчик на идентичност",
|
||||
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
|
||||
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Можете да конфигурирате настройките за автоматично предоставяне, след като дистрибуторът на самоличност бъде създаден.",
|
||||
"licenseBadge": "ЕЕ",
|
||||
"idpType": "Тип доставчик",
|
||||
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Карта на роля по подразбиране",
|
||||
"defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.",
|
||||
"defaultMappingsOrg": "Карта на организация по подразбиране",
|
||||
"defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.",
|
||||
"defaultMappingsOrgDescription": "При задаване, този израз трябва да върне идентификационния номер на организацията или true, за да се даде достъп на потребителя до тази организация. Ако не е зададено, дефинирането на роля е достатъчно: потребителят има право на достъп, стига валидно картографиране на роля да бъде разрешено за него в рамките на организацията.",
|
||||
"defaultMappingsSubmit": "Запазване на файловете по подразбиране",
|
||||
"orgPoliciesEdit": "Редактиране на Организационна Политика",
|
||||
"org": "Организация",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
|
||||
"introTitle": "Управлявано Самостоятелно-хостван Панголиин",
|
||||
"introDescription": "е опция за внедряване, предназначена за хора, които искат простота и допълнителна надеждност, като същевременно запазят данните си частни и самостоятелно-хоствани.",
|
||||
"introDetail": "С тази опция все още управлявате свой собствен Панголиин възел — вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:",
|
||||
"introDetail": "С тази опция все още управлявате свой собствен Панголиин възел - вашите тунели, SSL терминатора и трафик остават на вашия сървър. Разликата е, че управлението и мониторингът се обработват чрез нашия облачен панел за контрол, който отключва редица предимства:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "По-прости операции",
|
||||
"description": "Няма нужда да управлявате свой собствен имейл сървър или да настройвате сложни аларми. Ще получите проверки и предупреждения при прекъсване от самото начало."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Открит международен домейн",
|
||||
"willbestoredas": "Ще бъде съхранено като:",
|
||||
"roleMappingDescription": "Определете как се разпределят ролите на потребителите при вписване, когато е активирано автоматично предоставяне.",
|
||||
"roleMappingDescription": "Определете как ролите се присвояват на потребителите, когато се вписват с този доставчик на самоличност.",
|
||||
"selectRole": "Избор на роля",
|
||||
"roleMappingExpression": "Израз",
|
||||
"selectRolePlaceholder": "Избор на роля",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Разкриване на употреба",
|
||||
"description": "Изберете лицензионен клас, който точно отразява вашата целена употреба. Персоналният лиценз позволява безплатно ползване на софтуера за индивидуална, некомерсиална или маломащабна комерсиална дейност с годишен брутен приход под 100,000 USD. Всяко ползване извън тези граници — включително ползване във фирма, организация или друга доходоносна среда — изисква валиден корпоративен лиценз и плащане на съответната лицензионна такса. Всички потребители, независимо дали са лични или корпоративни, трябва да спазват Условията на Fossorial Commercial License."
|
||||
"description": "Изберете лицензионен клас, който точно отразява вашата целена употреба. Персоналният лиценз позволява безплатно ползване на софтуера за индивидуална, некомерсиална или маломащабна комерсиална дейност с годишен брутен приход под 100,000 USD. Всяко ползване извън тези граници - включително ползване във фирма, организация или друга доходоносна среда - изисква валиден корпоративен лиценз и плащане на съответната лицензионна такса. Всички потребители, независимо дали са лични или корпоративни, трябва да спазват Условията на Fossorial Commercial License."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Информация за пробен период",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON масив",
|
||||
"httpDestFormatJsonArrayDescription": "Една заявка на партида, тялото е JSON масив. Съвместим с повечето общи уеб куки и Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Една заявка на партида, тялото е ново линии отделени JSON — един обект на ред, няма външен масив. Изисквано от Splunk HEC, Elastic / OpenSearch и Grafana.",
|
||||
"httpDestFormatNdjsonDescription": "Една заявка на партида, тялото е ново линии отделени JSON - един обект на ред, няма външен масив. Изисквано от Splunk HEC, Elastic / OpenSearch и Grafana.",
|
||||
"httpDestFormatSingleTitle": "Едно събитие на заявка",
|
||||
"httpDestFormatSingleDescription": "Изпращат се отделни HTTP POST за всяко индивидуално събитие. Използвайте само за крайни точки, които не могат да обработват партиди.",
|
||||
"httpDestLogTypesTitle": "Видове логове",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно",
|
||||
"httpDestCreatedSuccess": "Дестинацията беше създадена успешно",
|
||||
"httpDestUpdateFailed": "Неуспешно актуализиране на дестинацията",
|
||||
"httpDestCreateFailed": "Неуспешно създаване на дестинацията"
|
||||
"httpDestCreateFailed": "Неуспешно създаване на дестинацията",
|
||||
"idpAddActionCreateNew": "Създайте нов доставчик на самоличност",
|
||||
"idpAddActionImportFromOrg": "Импортиране от друга организация",
|
||||
"idpImportDialogTitle": "Импортиране на доставчик на самоличност",
|
||||
"idpImportDialogDescription": "Изберете доставчик на самоличност от организация, в която сте администратор. Той ще бъде свързан с тази организация.",
|
||||
"idpImportSearchPlaceholder": "Търсене по име на организация или доставчик...",
|
||||
"idpImportEmpty": "Няма намерени доставчици на самоличност.",
|
||||
"idpImportedDescription": "Доставчикът на самоличност беше импортиран успешно.",
|
||||
"idpDeleteGlobalQuestion": "Сигурни ли сте, че искате да изтриете този доставчик на самоличност завинаги?",
|
||||
"idpDeleteGlobalDescription": "Това ще изтрие доставичка на самоличност завинаги от всички организации, с които е свързан.",
|
||||
"idpUnassociateTitle": "Отвързване на доставчик на самоличност",
|
||||
"idpUnassociateQuestion": "Сигурни ли сте, че искате да отвържете този доставчик на самоличност от тази организация?",
|
||||
"idpUnassociateDescription": "Всички потребители, свързани с този доставчик на самоличност, ще бъдат премахнати от тази организация, но доставчика на самоличност ще продължи да съществува за други свързани организации.",
|
||||
"idpUnassociateConfirm": "Потвърдете отвързване на доставчика на самоличност",
|
||||
"idpUnassociateWarning": "Това не може да бъде отменено за тази организация.",
|
||||
"idpUnassociatedDescription": "Доставчика на самоличност е успешно отвързан от тази организация",
|
||||
"idpUnassociateMenu": "Отвързване",
|
||||
"idpDeleteAllOrgsMenu": "Изтриване"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Zobrazované jméno tohoto poskytovatele identity",
|
||||
"idpAutoProvisionUsers": "Automatická úprava uživatelů",
|
||||
"idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Nastavení automatického poskytování lze nakonfigurovat, jakmile je vytvořen poskytovatel identity.",
|
||||
"licenseBadge": "PE",
|
||||
"idpType": "Typ poskytovatele",
|
||||
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Výchozí mapování rolí",
|
||||
"defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.",
|
||||
"defaultMappingsOrg": "Výchozí mapování organizace",
|
||||
"defaultMappingsOrgDescription": "Tento výraz musí vrátit org ID nebo pravdu, aby měl uživatel přístup k organizaci.",
|
||||
"defaultMappingsOrgDescription": "Pokud je nastaven, musí tento výraz vracet ID organizace nebo pravda, aby k této organizaci měl uživatel přístup. Pokud není nastaveno, je dostačující definice mapování rolí: uživateli je umožněn přístup, pokud pro něj lze v rámci organizace vyřešit platné mapování rolí.",
|
||||
"defaultMappingsSubmit": "Uložit výchozí mapování",
|
||||
"orgPoliciesEdit": "Upravit zásady organizace",
|
||||
"org": "Organizace",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami",
|
||||
"introTitle": "Spravovaný Pangolin",
|
||||
"introDescription": "je možnost nasazení určená pro lidi, kteří chtějí jednoduchost a spolehlivost při zachování soukromých a samoobslužných dat.",
|
||||
"introDetail": "Pomocí této volby stále provozujete vlastní uzel Pangolin — tunely, SSL terminály a provoz všech pobytů na vašem serveru. Rozdíl spočívá v tom, že řízení a monitorování se řeší prostřednictvím našeho cloudového panelu, který odemkne řadu výhod:",
|
||||
"introDetail": "Pomocí této volby stále provozujete vlastní uzel Pangolin - tunely, SSL terminály a provoz všech pobytů na vašem serveru. Rozdíl spočívá v tom, že řízení a monitorování se řeší prostřednictvím našeho cloudového panelu, který odemkne řadu výhod:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Jednoduchý provoz",
|
||||
"description": "Není třeba spouštět svůj vlastní poštovní server nebo nastavit komplexní upozornění. Ze schránky dostanete upozornění na zdravotní kontrolu a výpadek."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Zjištěna mezinárodní doména",
|
||||
"willbestoredas": "Bude uloženo jako:",
|
||||
"roleMappingDescription": "Určete, jak jsou role přiřazeny uživatelům, když se přihlásí, když je povoleno automatické poskytnutí služby.",
|
||||
"roleMappingDescription": "Určete, jak jsou role přiřazeny uživatelům, když se přihlásí s tímto poskytovatelem identity.",
|
||||
"selectRole": "Vyberte roli",
|
||||
"roleMappingExpression": "Výraz",
|
||||
"selectRolePlaceholder": "Vyberte roli",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Cíl byl úspěšně aktualizován",
|
||||
"httpDestCreatedSuccess": "Cíl byl úspěšně vytvořen",
|
||||
"httpDestUpdateFailed": "Nepodařilo se aktualizovat cíl",
|
||||
"httpDestCreateFailed": "Nepodařilo se vytvořit cíl"
|
||||
"httpDestCreateFailed": "Nepodařilo se vytvořit cíl",
|
||||
"idpAddActionCreateNew": "Vytvořit nového poskytovatele identity",
|
||||
"idpAddActionImportFromOrg": "Importovat z jiné organizace",
|
||||
"idpImportDialogTitle": "Importovat poskytovatele identity",
|
||||
"idpImportDialogDescription": "Vyberte poskytovatele identity z organizace, v níž jste administrátor. Tento poskytovatel bude propojen s touto organizací.",
|
||||
"idpImportSearchPlaceholder": "Hledat podle názvu organizace nebo poskytovatele...",
|
||||
"idpImportEmpty": "Nebyli nalezeni žádní poskytovatelé identity.",
|
||||
"idpImportedDescription": "Poskytovatel identity byl úspěšně importován.",
|
||||
"idpDeleteGlobalQuestion": "Opravdu chcete trvale smazat tohoto poskytovatele identity?",
|
||||
"idpDeleteGlobalDescription": "Tímto bude poskytovatel identity trvale odstraněn ze všech organizací, se kterými je spojen.",
|
||||
"idpUnassociateTitle": "Odpojit poskytovatele identity",
|
||||
"idpUnassociateQuestion": "Opravdu chcete odpojit tohoto poskytovatele identity od této organizace?",
|
||||
"idpUnassociateDescription": "Všichni uživatelé spojení s tímto poskytovatelem identity budou odstraněni z této organizace, ale poskytovatel identity zůstane nadále existovat pro ostatní přidružené organizace.",
|
||||
"idpUnassociateConfirm": "Potvrdit odpojení poskytovatele identity",
|
||||
"idpUnassociateWarning": "Toto nelze pro tuto organizaci vrátit.",
|
||||
"idpUnassociatedDescription": "Poskytovatel identity byl úspěšně odpojen od této organizace",
|
||||
"idpUnassociateMenu": "Odpojit",
|
||||
"idpDeleteAllOrgsMenu": "Odstranit"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Ein Anzeigename für diesen Identitätsanbieter",
|
||||
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
|
||||
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Sie können die automatische Bereitstellung einstellen, sobald der Identitätsanbieter erstellt ist.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Anbietertyp",
|
||||
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Standard-Rollenzuordnung",
|
||||
"defaultMappingsRoleDescription": "JMESPath zur Extraktion von Rolleninformationen aus dem ID-Token. Das Ergebnis dieses Ausdrucks muss den Rollennamen als String zurückgeben, wie er in der Organisation definiert ist.",
|
||||
"defaultMappingsOrg": "Standard-Organisationszuordnung",
|
||||
"defaultMappingsOrgDescription": "JMESPath zur Extraktion von Organisationsinformationen aus dem ID-Token. Dieser Ausdruck muss die Organisations-ID oder true zurückgeben, damit der Benutzer Zugriff auf die Organisation erhält.",
|
||||
"defaultMappingsOrgDescription": "Wenn diese Einstellung festgelegt ist, muss dieser Ausdruck die Organisations-ID oder wahr zurückgeben, damit der Benutzer diese Organisation betreten kann. Ist sie nicht festgelegt, reicht die Definition einer Rollenzuordnung aus: Der Benutzer darf eintreten, solange eine gültige Rollenzuordnung innerhalb der Organisation für ihn aufgelöst werden kann.",
|
||||
"defaultMappingsSubmit": "Standardzuordnungen speichern",
|
||||
"orgPoliciesEdit": "Organisationsrichtlinie bearbeiten",
|
||||
"org": "Organisation",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Internationale Domain erkannt",
|
||||
"willbestoredas": "Wird gespeichert als:",
|
||||
"roleMappingDescription": "Legen Sie fest, wie den Benutzern Rollen zugewiesen werden, wenn sie sich anmelden, wenn Auto Provision aktiviert ist.",
|
||||
"roleMappingDescription": "Bestimmen Sie, wie Rollen zugewiesen werden, wenn sich Benutzer mit diesem Identitätsanbieter anmelden.",
|
||||
"selectRole": "Wählen Sie eine Rolle",
|
||||
"roleMappingExpression": "Ausdruck",
|
||||
"selectRolePlaceholder": "Rolle auswählen",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Verwendungsanzeige",
|
||||
"description": "Wählen Sie die Lizenz-Ebene, die Ihre beabsichtigte Nutzung genau widerspiegelt. Die Persönliche Lizenz erlaubt die freie Nutzung der Software für individuelle, nicht-kommerzielle oder kleine kommerzielle Aktivitäten mit jährlichen Brutto-Einnahmen von 100.000 USD. Über diese Grenzen hinausgehende Verwendungszwecke – einschließlich der Verwendung innerhalb eines Unternehmens, einer Organisation, oder eine andere umsatzgenerierende Umgebung — erfordert eine gültige Enterprise-Lizenz und die Zahlung der Lizenzgebühr. Alle Benutzer, ob Personal oder Enterprise, müssen die Fossorial Commercial License Bedingungen einhalten."
|
||||
"description": "Wählen Sie die Lizenz-Ebene, die Ihre beabsichtigte Nutzung genau widerspiegelt. Die Persönliche Lizenz erlaubt die freie Nutzung der Software für individuelle, nicht-kommerzielle oder kleine kommerzielle Aktivitäten mit jährlichen Brutto-Einnahmen von 100.000 USD. Über diese Grenzen hinausgehende Verwendungszwecke – einschließlich der Verwendung innerhalb eines Unternehmens, einer Organisation, oder eine andere umsatzgenerierende Umgebung - erfordert eine gültige Enterprise-Lizenz und die Zahlung der Lizenzgebühr. Alle Benutzer, ob Personal oder Enterprise, müssen die Fossorial Commercial License Bedingungen einhalten."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Testperiode Information",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"httpDestFormatSingleTitle": "Ein Ereignis pro Anfrage",
|
||||
"httpDestFormatSingleDescription": "Sendet eine separate HTTP-POST für jedes einzelne Ereignis. Nur für Endpunkte, die Batches nicht handhaben können.",
|
||||
"httpDestLogTypesTitle": "Log-Typen",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
|
||||
"httpDestCreatedSuccess": "Ziel erfolgreich erstellt",
|
||||
"httpDestUpdateFailed": "Fehler beim Aktualisieren des Ziels",
|
||||
"httpDestCreateFailed": "Fehler beim Erstellen des Ziels"
|
||||
"httpDestCreateFailed": "Fehler beim Erstellen des Ziels",
|
||||
"idpAddActionCreateNew": "Neuen Identitätsanbieter erstellen",
|
||||
"idpAddActionImportFromOrg": "Von einer anderen Organisation importieren",
|
||||
"idpImportDialogTitle": "Identitätsanbieter importieren",
|
||||
"idpImportDialogDescription": "Wählen Sie einen Identitätsanbieter aus einer Organisation, in der Sie Administrator sind. Er wird mit dieser Organisation verknüpft.",
|
||||
"idpImportSearchPlaceholder": "Nach Organisation oder Anbieternamen suchen...",
|
||||
"idpImportEmpty": "Keine Identitätsanbieter gefunden.",
|
||||
"idpImportedDescription": "Identitätsanbieter erfolgreich importiert.",
|
||||
"idpDeleteGlobalQuestion": "Sind Sie sicher, dass Sie diesen Identitätsanbieter dauerhaft löschen möchten?",
|
||||
"idpDeleteGlobalDescription": "Dies wird den Identitätsanbieter dauerhaft von allen Organisationen löschen, mit denen er verbunden ist.",
|
||||
"idpUnassociateTitle": "Verknüpfung mit Identitätsanbieter aufheben",
|
||||
"idpUnassociateQuestion": "Sind Sie sicher, dass Sie die Verknüpfung dieses Identitätsanbieters mit dieser Organisation aufheben möchten?",
|
||||
"idpUnassociateDescription": "Alle Benutzer, die mit diesem Identitätsanbieter verbunden sind, werden aus dieser Organisation entfernt, aber der Identitätsanbieter bleibt für andere verbundene Organisationen weiterhin bestehen.",
|
||||
"idpUnassociateConfirm": "Verknüpfung des Identitätsanbieters aufheben bestätigen",
|
||||
"idpUnassociateWarning": "Dies kann für diese Organisation nicht rückgängig gemacht werden.",
|
||||
"idpUnassociatedDescription": "Identitätsanbieter erfolgreich von dieser Organisation gelöst",
|
||||
"idpUnassociateMenu": "Verknüpfung aufheben",
|
||||
"idpDeleteAllOrgsMenu": "Löschen"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"contactSalesEnable": "Contact sales to enable this feature.",
|
||||
"contactSalesBookDemo": "Book a demo",
|
||||
"contactSalesOr": "or",
|
||||
"contactSalesContactUs": "contact us",
|
||||
"setupCreate": "Create the organization, site, and resources",
|
||||
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
|
||||
"headerAuthCompatibility": "Extended compatibility",
|
||||
@@ -163,7 +167,7 @@
|
||||
"proxyResourceTitle": "Manage Public Resources",
|
||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||
"proxyResourcesBannerTitle": "Web-based Public Access",
|
||||
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
|
||||
"proxyResourcesBannerDescription": "Public resources are HTTPS proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
|
||||
"clientResourceTitle": "Manage Private Resources",
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
@@ -380,7 +384,7 @@
|
||||
"userTitle": "Manage All Users",
|
||||
"userDescription": "View and manage all users in the system",
|
||||
"userAbount": "About User Management",
|
||||
"userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.",
|
||||
"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...",
|
||||
"userErrorDelete": "Error deleting user",
|
||||
@@ -523,7 +527,7 @@
|
||||
"userSettings": "User Information",
|
||||
"userSettingsDescription": "Enter the details for the new user",
|
||||
"inviteEmailSent": "Send invite email to user",
|
||||
"inviteValid": "Valid For",
|
||||
"inviteValid": "Invite Valid For (days)",
|
||||
"selectDuration": "Select duration",
|
||||
"selectResource": "Select Resource",
|
||||
"filterByResource": "Filter By Resource",
|
||||
@@ -1257,6 +1261,7 @@
|
||||
"actionViewLogs": "View Logs",
|
||||
"noneSelected": "None selected",
|
||||
"orgNotFound2": "No organizations found.",
|
||||
"search": "Search…",
|
||||
"searchPlaceholder": "Search...",
|
||||
"emptySearchOptions": "No options found",
|
||||
"create": "Create",
|
||||
@@ -1341,10 +1346,138 @@
|
||||
"sidebarGeneral": "Manage",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Blueprints",
|
||||
"sidebarAlerting": "Alerting",
|
||||
"sidebarOrganization": "Organization",
|
||||
"sidebarManagement": "Management",
|
||||
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||
"sidebarLogsAnalytics": "Analytics",
|
||||
"alertingTitle": "Alerting",
|
||||
"alertingDescription": "Define sources, triggers, and actions for notifications",
|
||||
"alertingRules": "Alert rules",
|
||||
"alertingSearchRules": "Search rules…",
|
||||
"alertingAddRule": "Create Rule",
|
||||
"alertingColumnSource": "Source",
|
||||
"alertingColumnTrigger": "Trigger",
|
||||
"alertingColumnActions": "Actions",
|
||||
"alertingColumnEnabled": "Enabled",
|
||||
"alertingDeleteQuestion": "Delete this alert rule? This cannot be undone.",
|
||||
"alertingDeleteRule": "Delete alert rule",
|
||||
"alertingRuleDeleted": "Alert rule deleted",
|
||||
"alertingRuleSaved": "Alert rule saved",
|
||||
"alertingEditRule": "Edit alert rule",
|
||||
"alertingCreateRule": "Create alert rule",
|
||||
"alertingRuleCredenzaDescription": "Choose what to watch, when to fire, and how to notify your team.",
|
||||
"alertingRuleNamePlaceholder": "Production site down",
|
||||
"alertingRuleEnabled": "Rule enabled",
|
||||
"alertingSectionSource": "Source",
|
||||
"alertingSourceType": "Source type",
|
||||
"alertingSourceSite": "Site",
|
||||
"alertingSourceHealthCheck": "Health check",
|
||||
"alertingPickSites": "Sites",
|
||||
"alertingPickHealthChecks": "Health checks",
|
||||
"alertingPickResources": "Resources",
|
||||
"alertingAllSites": "All Sites",
|
||||
"alertingAllSitesDescription": "Alert fires for any site",
|
||||
"alertingSpecificSites": "Specific Sites",
|
||||
"alertingSpecificSitesDescription": "Choose specific sites to watch",
|
||||
"alertingAllHealthChecks": "All Health Checks",
|
||||
"alertingAllHealthChecksDescription": "Alert fires for any health check",
|
||||
"alertingSpecificHealthChecks": "Specific Health Checks",
|
||||
"alertingSpecificHealthChecksDescription": "Choose specific health checks to watch",
|
||||
"alertingAllResources": "All Resources",
|
||||
"alertingAllResourcesDescription": "Alert fires for any resource",
|
||||
"alertingSpecificResources": "Specific resources",
|
||||
"alertingSpecificResourcesDescription": "Choose specific resources to watch",
|
||||
"alertingSelectResources": "Select resources…",
|
||||
"alertingResourcesSelected": "{count} resources selected",
|
||||
"alertingResourcesEmpty": "No resources with targets in the first 10 results.",
|
||||
"alertingSectionTrigger": "Trigger",
|
||||
"alertingTrigger": "When to alert",
|
||||
"alertingTriggerSiteOnline": "Site online",
|
||||
"alertingTriggerSiteOffline": "Site offline",
|
||||
"alertingTriggerSiteToggle": "Site status changes",
|
||||
"alertingTriggerHcHealthy": "Health check healthy",
|
||||
"alertingTriggerHcUnhealthy": "Health check unhealthy",
|
||||
"alertingTriggerHcToggle": "Health check status changes",
|
||||
"alertingTriggerResourceHealthy": "Resource healthy",
|
||||
"alertingTriggerResourceUnhealthy": "Resource unhealthy",
|
||||
"alertingSearchHealthChecks": "Search health checks…",
|
||||
"alertingHealthChecksEmpty": "No health checks available.",
|
||||
"alertingTriggerResourceToggle": "Resource status changes",
|
||||
"alertingSourceResource": "Resource",
|
||||
"alertingSectionActions": "Actions",
|
||||
"alertingAddAction": "Add action",
|
||||
"alertingActionNotify": "Email",
|
||||
"alertingActionNotifyDescription": "Send email notifications to users or roles",
|
||||
"alertingActionWebhook": "Webhook",
|
||||
"alertingActionWebhookDescription": "Send an HTTP request to a custom endpoint",
|
||||
"alertingExternalIntegration": "External Integration",
|
||||
"alertingExternalPagerDutyDescription": "Send alerts to PagerDuty for incident management",
|
||||
"alertingExternalOpsgenieDescription": "Route alerts to Opsgenie for on-call management",
|
||||
"alertingExternalServiceNowDescription": "Create ServiceNow incidents from alert events",
|
||||
"alertingExternalIncidentIoDescription": "Trigger Incident.io workflows from alert events",
|
||||
"alertingActionType": "Action type",
|
||||
"alertingNotifyUsers": "Users",
|
||||
"alertingNotifyRoles": "Roles",
|
||||
"alertingNotifyEmails": "Email addresses",
|
||||
"alertingEmailPlaceholder": "Add email and press Enter",
|
||||
"alertingWebhookMethod": "HTTP method",
|
||||
"alertingWebhookSecret": "Signing secret (optional)",
|
||||
"alertingWebhookSecretPlaceholder": "HMAC secret",
|
||||
"alertingWebhookHeaders": "Headers",
|
||||
"alertingAddHeader": "Add header",
|
||||
"alertingSelectSites": "Select sites…",
|
||||
"alertingSitesSelected": "{count} sites selected",
|
||||
"alertingSelectHealthChecks": "Select health checks…",
|
||||
"alertingHealthChecksSelected": "{count} health checks selected",
|
||||
"alertingNoHealthChecks": "No targets with health checks enabled",
|
||||
"alertingHealthCheckStub": "Health check source selection is not wired up yet - you can still configure triggers and actions.",
|
||||
"alertingSelectUsers": "Select users…",
|
||||
"alertingUsersSelected": "{count} users selected",
|
||||
"alertingSelectRoles": "Select roles…",
|
||||
"alertingRolesSelected": "{count} roles selected",
|
||||
"alertingSummarySites": "Sites ({count})",
|
||||
"alertingSummaryHealthChecks": "Health checks ({count})",
|
||||
"alertingSummaryResources": "Resources ({count})",
|
||||
"alertingErrorNameRequired": "Enter a name",
|
||||
"alertingErrorActionsMin": "Add at least one action",
|
||||
"alertingErrorPickSites": "Select at least one site",
|
||||
"alertingErrorPickHealthChecks": "Select at least one health check",
|
||||
"alertingErrorPickResources": "Select at least one resource",
|
||||
"alertingErrorTriggerSite": "Choose a site trigger",
|
||||
"alertingErrorTriggerHealth": "Choose a health check trigger",
|
||||
"alertingErrorTriggerResource": "Choose a resource trigger",
|
||||
"alertingErrorNotifyRecipients": "Pick users, roles, or at least one email",
|
||||
"alertingConfigureSource": "Configure Source",
|
||||
"alertingConfigureTrigger": "Configure Trigger",
|
||||
"alertingConfigureActions": "Configure Actions",
|
||||
"alertingBackToRules": "Back to Rules",
|
||||
"alertingDraftBadge": "Draft - save to store this rule",
|
||||
"alertingSidebarHint": "Click a step on the canvas to edit it here.",
|
||||
"alertingGraphCanvasTitle": "Rule Flow",
|
||||
"alertingGraphCanvasDescription": "Visual overview of source, trigger, and actions. Select a node to edit it in the panel.",
|
||||
"alertingNodeNotConfigured": "Not configured yet",
|
||||
"alertingNodeActionsCount": "{count, plural, one {# action} other {# actions}}",
|
||||
"alertingNodeRoleSource": "Source",
|
||||
"alertingNodeRoleTrigger": "Trigger",
|
||||
"alertingNodeRoleAction": "Action",
|
||||
"alertingTabRules": "Alert Rules",
|
||||
"alertingTabHealthChecks": "Health Checks",
|
||||
"standaloneHcTableTitle": "Health Checks",
|
||||
"standaloneHcSearchPlaceholder": "Search health checks…",
|
||||
"standaloneHcAddButton": "Create Health Check",
|
||||
"standaloneHcCreateTitle": "Create Health Check",
|
||||
"standaloneHcEditTitle": "Edit Health Check",
|
||||
"standaloneHcDescription": "Configure a HTTP or TCP health check for use in alert rules.",
|
||||
"standaloneHcNameLabel": "Name",
|
||||
"standaloneHcNamePlaceholder": "My HTTP Monitor",
|
||||
"standaloneHcDeleteTitle": "Delete health check",
|
||||
"standaloneHcDeleteQuestion": "Delete this health check? This cannot be undone.",
|
||||
"standaloneHcDeleted": "Health check deleted",
|
||||
"standaloneHcSaved": "Health check saved",
|
||||
"standaloneHcColumnHealth": "Health",
|
||||
"standaloneHcColumnMode": "Mode",
|
||||
"standaloneHcColumnTarget": "Target",
|
||||
"blueprints": "Blueprints",
|
||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||
"blueprintAdd": "Add Blueprint",
|
||||
@@ -1750,8 +1883,8 @@
|
||||
"retryAttempts": "Retry Attempts",
|
||||
"expectedResponseCodes": "Expected Response Codes",
|
||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||
"customHeaders": "Custom Headers",
|
||||
"customHeadersDescription": "Headers new line separated: Header-Name: value",
|
||||
"customHeaders": "Custom Request Headers",
|
||||
"customHeadersDescription": "Request headers sent to the downstream targets. Headers new line separated: Header-Name: value",
|
||||
"headersValidationError": "Headers must be in the format: Header-Name: value",
|
||||
"saveHealthCheck": "Save Health Check",
|
||||
"healthCheckSaved": "Health Check Saved",
|
||||
@@ -1763,8 +1896,17 @@
|
||||
"healthCheckIntervalMin": "Check interval must be at least 5 seconds",
|
||||
"healthCheckTimeoutMin": "Timeout must be at least 1 second",
|
||||
"healthCheckRetryMin": "Retry attempts must be at least 1",
|
||||
"httpMethod": "HTTP Method",
|
||||
"selectHttpMethod": "Select HTTP method",
|
||||
"healthCheckMode": "Check Mode",
|
||||
"healthCheckStrategy": "Strategy",
|
||||
"healthCheckModeDescription": "TCP mode verifies connectivity only. HTTP mode validates the HTTP response.",
|
||||
"healthyThreshold": "Healthy Threshold",
|
||||
"healthyThresholdDescription": "Consecutive successes required before marking as healthy.",
|
||||
"unhealthyThreshold": "Unhealthy Threshold",
|
||||
"unhealthyThresholdDescription": "Consecutive failures required before marking as unhealthy.",
|
||||
"healthCheckHealthyThresholdMin": "Healthy threshold must be at least 1",
|
||||
"healthCheckUnhealthyThresholdMin": "Unhealthy threshold must be at least 1",
|
||||
"httpMethod": "Scheme",
|
||||
"selectHttpMethod": "Select scheme",
|
||||
"domainPickerSubdomainLabel": "Subdomain",
|
||||
"domainPickerBaseDomainLabel": "Base Domain",
|
||||
"domainPickerSearchDomains": "Search domains...",
|
||||
@@ -1822,6 +1964,11 @@
|
||||
"editInternalResourceDialogModePort": "Port",
|
||||
"editInternalResourceDialogModeHost": "Host",
|
||||
"editInternalResourceDialogModeCidr": "CIDR",
|
||||
"editInternalResourceDialogModeHttp": "HTTP",
|
||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||
"editInternalResourceDialogScheme": "Scheme",
|
||||
"editInternalResourceDialogEnableSsl": "Enable SSL",
|
||||
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
||||
"editInternalResourceDialogDestination": "Destination",
|
||||
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||
@@ -1837,6 +1984,7 @@
|
||||
"createInternalResourceDialogName": "Name",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"selectSite": "Select site...",
|
||||
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
|
||||
"noSitesFound": "No sites found.",
|
||||
"createInternalResourceDialogProtocol": "Protocol",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
@@ -1865,11 +2013,19 @@
|
||||
"createInternalResourceDialogModePort": "Port",
|
||||
"createInternalResourceDialogModeHost": "Host",
|
||||
"createInternalResourceDialogModeCidr": "CIDR",
|
||||
"createInternalResourceDialogModeHttp": "HTTP",
|
||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||
"scheme": "Scheme",
|
||||
"createInternalResourceDialogScheme": "Scheme",
|
||||
"createInternalResourceDialogEnableSsl": "Enable SSL",
|
||||
"createInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
|
||||
"createInternalResourceDialogDestination": "Destination",
|
||||
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
|
||||
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
|
||||
"createInternalResourceDialogAlias": "Alias",
|
||||
"createInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
|
||||
"internalResourceDownstreamSchemeRequired": "Scheme is required for HTTP resources",
|
||||
"internalResourceHttpPortRequired": "Destination port is required for HTTP resources",
|
||||
"siteConfiguration": "Configuration",
|
||||
"siteAcceptClientConnections": "Accept Client Connections",
|
||||
"siteAcceptClientConnectionsDescription": "Allow user devices and clients to access resources on this site. This can be changed later.",
|
||||
@@ -1994,7 +2150,7 @@
|
||||
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
|
||||
"introTitle": "Managed Self-Hosted Pangolin",
|
||||
"introDescription": "is a deployment option designed for people who want simplicity and extra reliability while still keeping their data private and self-hosted.",
|
||||
"introDetail": "With this option, you still run your own Pangolin node — your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||
"introDetail": "With this option, you still run your own Pangolin node - your tunnels, SSL termination, and traffic all stay on your server. The difference is that management and monitoring are handled through our cloud dashboard, which unlocks a number of benefits:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Simpler operations",
|
||||
"description": "No need to run your own mail server or set up complex alerting. You'll get health checks and downtime alerts out of the box."
|
||||
@@ -2119,7 +2275,7 @@
|
||||
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||
"domainPickerProvidedDomain": "Provided Domain",
|
||||
"domainPickerFreeProvidedDomain": "Provided Domain",
|
||||
"domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan — no need to bring your own.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan - no need to bring your own.",
|
||||
"domainPickerVerified": "Verified",
|
||||
"domainPickerUnverified": "Unverified",
|
||||
"domainPickerManual": "Manual",
|
||||
@@ -2297,7 +2453,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Usage Disclosure",
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
"description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits - including use within a business, organization, or other revenue-generating environment - requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
@@ -2416,7 +2572,7 @@
|
||||
"action": "Action",
|
||||
"actor": "Actor",
|
||||
"timestamp": "Timestamp",
|
||||
"accessLogs": "Access Logs",
|
||||
"accessLogs": "Authentication Logs",
|
||||
"exportCsv": "Export CSV",
|
||||
"exportError": "Unknown error when exporting CSV",
|
||||
"exportCsvTooltip": "Within Time Range",
|
||||
@@ -2429,6 +2585,7 @@
|
||||
"validPassword": "Valid Password",
|
||||
"validEmail": "Valid email",
|
||||
"validSSO": "Valid SSO",
|
||||
"connectedClient": "Connected Client",
|
||||
"resourceBlocked": "Resource Blocked",
|
||||
"droppedByRule": "Dropped by Rule",
|
||||
"noSessions": "No Sessions",
|
||||
@@ -2436,25 +2593,25 @@
|
||||
"noMoreAuthMethods": "No Valid Auth",
|
||||
"ip": "IP",
|
||||
"reason": "Reason",
|
||||
"requestLogs": "Request Logs",
|
||||
"requestLogs": "HTTPS Request Logs",
|
||||
"requestAnalytics": "Request Analytics",
|
||||
"host": "Host",
|
||||
"location": "Location",
|
||||
"actionLogs": "Action Logs",
|
||||
"sidebarLogsRequest": "Request Logs",
|
||||
"sidebarLogsAccess": "Access Logs",
|
||||
"sidebarLogsAction": "Action Logs",
|
||||
"actionLogs": "Admin Action Logs",
|
||||
"sidebarLogsRequest": "HTTPS Request Logs",
|
||||
"sidebarLogsAccess": "Authentication Logs",
|
||||
"sidebarLogsAction": "Admin Action Logs",
|
||||
"logRetention": "Log Retention",
|
||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||
"requestLogsDescription": "View detailed request logs for resources in this organization",
|
||||
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
||||
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||
"logRetentionRequestLabel": "Request Log Retention",
|
||||
"logRetentionRequestLabel": "HTTPS Request Log Retention",
|
||||
"logRetentionRequestDescription": "How long to retain request logs",
|
||||
"logRetentionAccessLabel": "Access Log Retention",
|
||||
"logRetentionAccessLabel": "Authentication Log Retention",
|
||||
"logRetentionAccessDescription": "How long to retain access logs",
|
||||
"logRetentionActionLabel": "Action Log Retention",
|
||||
"logRetentionActionLabel": "Admin Action Log Retention",
|
||||
"logRetentionActionDescription": "How long to retain action logs",
|
||||
"logRetentionConnectionLabel": "Connection Log Retention",
|
||||
"logRetentionConnectionLabel": "Network Log Retention",
|
||||
"logRetentionConnectionDescription": "How long to retain connection logs",
|
||||
"logRetentionDisabled": "Disabled",
|
||||
"logRetention3Days": "3 days",
|
||||
@@ -2466,10 +2623,10 @@
|
||||
"logRetentionEndOfFollowingYear": "End of following year",
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"connectionLogs": "Connection Logs",
|
||||
"connectionLogsDescription": "View connection logs for tunnels in this organization",
|
||||
"sidebarLogsConnection": "Connection Logs",
|
||||
"sidebarLogsStreaming": "Streaming",
|
||||
"connectionLogs": "Network Logs",
|
||||
"connectionLogsDescription": "View network session logs handled by sites in this organization",
|
||||
"sidebarLogsConnection": "Network Logs",
|
||||
"sidebarLogsStreaming": "Event Streaming",
|
||||
"sourceAddress": "Source Address",
|
||||
"destinationAddress": "Destination Address",
|
||||
"duration": "Duration",
|
||||
@@ -2666,8 +2823,12 @@
|
||||
"editInternalResourceDialogAddUsers": "Add Users",
|
||||
"editInternalResourceDialogAddClients": "Add Clients",
|
||||
"editInternalResourceDialogDestinationLabel": "Destination",
|
||||
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
|
||||
"editInternalResourceDialogDestinationDescription": "Choose where this resource runs and how clients reach it. Selecting multiple sites will create a high availability resource that can be accessed from any of the selected sites.",
|
||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||
"createInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
||||
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
||||
"editInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
||||
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
||||
"editInternalResourceDialogTcp": "TCP",
|
||||
"editInternalResourceDialogUdp": "UDP",
|
||||
"editInternalResourceDialogIcmp": "ICMP",
|
||||
@@ -2706,6 +2867,8 @@
|
||||
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
||||
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
|
||||
"privateMaintenanceScreenTitle": "Private Placeholder Screen",
|
||||
"privateMaintenanceScreenMessage": "This domain is being used on a private resource. Please connect using the Pangolin client to access this resource.",
|
||||
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
|
||||
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
||||
"editDomain": "Edit Domain",
|
||||
@@ -2825,9 +2988,9 @@
|
||||
"streamingHttpWebhookTitle": "HTTP Webhook",
|
||||
"streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.",
|
||||
"streamingS3Title": "Amazon S3",
|
||||
"streamingS3Description": "Stream events to an S3-compatible object storage bucket. Contact support to enable this destination.",
|
||||
"streamingS3Description": "Stream events to an S3-compatible object storage bucket.",
|
||||
"streamingDatadogTitle": "Datadog",
|
||||
"streamingDatadogDescription": "Forward events directly to your Datadog account. Contact support to enable this destination.",
|
||||
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||
"streamingFailedToLoad": "Failed to load destinations",
|
||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||
@@ -2843,6 +3006,14 @@
|
||||
"httpDestAddTitle": "Add HTTP Destination",
|
||||
"httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.",
|
||||
"httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.",
|
||||
"S3DestEditTitle": "Edit Destination",
|
||||
"S3DestAddTitle": "Add S3 Destination",
|
||||
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
||||
"S3DestAddDescription": "Configure a new S3 endpoint to receive your organization's events.",
|
||||
"datadogDestEditTitle": "Edit Destination",
|
||||
"datadogDestAddTitle": "Add Datadog Destination",
|
||||
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
||||
"datadogDestAddDescription": "Configure a new Datadog endpoint to receive your organization's events.",
|
||||
"httpDestTabSettings": "Settings",
|
||||
"httpDestTabHeaders": "Headers",
|
||||
"httpDestTabBody": "Body",
|
||||
@@ -2882,18 +3053,18 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON - one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "One Event Per Request",
|
||||
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
|
||||
"httpDestLogTypesTitle": "Log Types",
|
||||
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
|
||||
"httpDestAccessLogsTitle": "Access Logs",
|
||||
"httpDestAccessLogsTitle": "Authentication Logs",
|
||||
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
|
||||
"httpDestActionLogsTitle": "Action Logs",
|
||||
"httpDestActionLogsTitle": "Admin Action Logs",
|
||||
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
|
||||
"httpDestConnectionLogsTitle": "Connection Logs",
|
||||
"httpDestConnectionLogsTitle": "Network Logs",
|
||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||
"httpDestRequestLogsTitle": "Request Logs",
|
||||
"httpDestRequestLogsTitle": "HTTPS Request Logs",
|
||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||
"httpDestSaveChanges": "Save Changes",
|
||||
"httpDestCreateDestination": "Create Destination",
|
||||
@@ -2901,6 +3072,18 @@
|
||||
"httpDestCreatedSuccess": "Destination created successfully",
|
||||
"httpDestUpdateFailed": "Failed to update destination",
|
||||
"httpDestCreateFailed": "Failed to create destination",
|
||||
"followRedirects": "Follow Redirects",
|
||||
"followRedirectsDescription": "Automatically follow HTTP redirects for requests.",
|
||||
"alertingErrorWebhookUrl": "Please enter a valid URL for the webhook.",
|
||||
"healthCheckStrategyHttp": "Validates connectivity and checks the HTTP response status.",
|
||||
"healthCheckStrategyTcp": "Verifies TCP connectivity only, without inspecting the response.",
|
||||
"healthCheckStrategySnmp": "Makes an SNMP get request to check the health of network devices and infrastructure.",
|
||||
"healthCheckStrategyIcmp": "Uses ICMP echo requests (pings) to check if a resource is reachable and responsive.",
|
||||
"healthCheckTabStrategy": "Strategy",
|
||||
"healthCheckTabConnection": "Connection",
|
||||
"healthCheckTabAdvanced": "Advanced",
|
||||
"healthCheckStrategyNotAvailable": "This strategy is not available. Please contact sales to enable this feature.",
|
||||
"uptime30d": "Uptime (30d)",
|
||||
"idpAddActionCreateNew": "Create new identity provider",
|
||||
"idpAddActionImportFromOrg": "Import from another organization",
|
||||
"idpImportDialogTitle": "Import Identity Provider",
|
||||
@@ -2917,5 +3100,6 @@
|
||||
"idpUnassociateWarning": "This cannot be undone for this organization.",
|
||||
"idpUnassociatedDescription": "Identity provider unassociated from this organization successfully",
|
||||
"idpUnassociateMenu": "Unassociate",
|
||||
"idpDeleteAllOrgsMenu": "Delete"
|
||||
"idpDeleteAllOrgsMenu": "Delete",
|
||||
"publicIpEndpoint": "Endpoint"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Un nombre mostrado para este proveedor de identidad",
|
||||
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios",
|
||||
"idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Puede configurar las configuraciones de provisión automática una vez que se haya creado el proveedor de identidad.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo de proveedor",
|
||||
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Mapeo de Rol por defecto",
|
||||
"defaultMappingsRoleDescription": "El resultado de esta expresión debe devolver el nombre del rol tal y como se define en la organización como una cadena.",
|
||||
"defaultMappingsOrg": "Mapeo de organización por defecto",
|
||||
"defaultMappingsOrgDescription": "Esta expresión debe devolver el ID de org o verdadero para que el usuario pueda acceder a la organización.",
|
||||
"defaultMappingsOrgDescription": "Cuando se establece, esta expresión debe devolver el ID de la organización o verdadero para que el usuario acceda a esa organización. Cuando no se establece, definir un mapeo de roles es suficiente: se permite la entrada del usuario siempre que se pueda resolver un mapeo de roles válido para él dentro de la organización.",
|
||||
"defaultMappingsSubmit": "Guardar asignaciones por defecto",
|
||||
"orgPoliciesEdit": "Editar Política de Organización",
|
||||
"org": "Organización",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Dominio Internacional detectado",
|
||||
"willbestoredas": "Se almacenará como:",
|
||||
"roleMappingDescription": "Determinar cómo se asignan los roles a los usuarios cuando se registran cuando está habilitada la provisión automática.",
|
||||
"roleMappingDescription": "Determine cómo se asignan los roles a los usuarios cuando inician sesión con este proveedor de identidad.",
|
||||
"selectRole": "Seleccione un rol",
|
||||
"roleMappingExpression": "Expresión",
|
||||
"selectRolePlaceholder": "Elija un rol",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Seleccione un dominio para la página de autenticación de la organización",
|
||||
"domainPickerProvidedDomain": "Dominio proporcionado",
|
||||
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||
"domainPickerFreeDomainsPaidFeature": "Los dominios proporcionados son una función de pago. Suscríbete para obtener un dominio incluido con tu plan — no necesitas traer el tuyo propio.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Los dominios proporcionados son una función de pago. Suscríbete para obtener un dominio incluido con tu plan - no necesitas traer el tuyo propio.",
|
||||
"domainPickerVerified": "Verificado",
|
||||
"domainPickerUnverified": "Sin verificar",
|
||||
"domainPickerManual": "Manual",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Divulgación de uso",
|
||||
"description": "Seleccione el nivel de licencia que refleje con precisión su uso previsto. La Licencia Personal permite el uso libre del Software para actividades comerciales individuales, no comerciales o de pequeña escala con ingresos brutos anuales inferiores a $100,000 USD. Cualquier uso más allá de estos límites — incluyendo el uso dentro de una empresa, organización, u otro entorno de generación de ingresos — requiere una Licencia Empresarial válida y el pago de la cuota de licencia aplicable. Todos los usuarios, ya sean personales o empresariales, deben cumplir con las Condiciones de Licencia Comercial Fossorial."
|
||||
"description": "Seleccione el nivel de licencia que refleje con precisión su uso previsto. La Licencia Personal permite el uso libre del Software para actividades comerciales individuales, no comerciales o de pequeña escala con ingresos brutos anuales inferiores a $100,000 USD. Cualquier uso más allá de estos límites - incluyendo el uso dentro de una empresa, organización, u otro entorno de generación de ingresos - requiere una Licencia Empresarial válida y el pago de la cuota de licencia aplicable. Todos los usuarios, ya sean personales o empresariales, deben cumplir con las Condiciones de Licencia Comercial Fossorial."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Información del período de prueba",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
||||
"httpDestFormatJsonArrayDescription": "Una petición por lote, cuerpo es una matriz JSON. Compatible con la mayoría de los webhooks y Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Una petición por lote, el cuerpo es JSON delimitado por línea — un objeto por línea, sin arrays externos. Requerido por Splunk HEC, Elastic / OpenSearch, y Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "Una petición por lote, el cuerpo es JSON delimitado por línea - un objeto por línea, sin arrays externos. Requerido por Splunk HEC, Elastic / OpenSearch, y Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "Un evento por solicitud",
|
||||
"httpDestFormatSingleDescription": "Envía un HTTP POST separado para cada evento individual. Úsalo sólo para los extremos que no pueden manejar lotes.",
|
||||
"httpDestLogTypesTitle": "Tipos de Log",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Destino actualizado correctamente",
|
||||
"httpDestCreatedSuccess": "Destino creado correctamente",
|
||||
"httpDestUpdateFailed": "Error al actualizar destino",
|
||||
"httpDestCreateFailed": "Error al crear el destino"
|
||||
"httpDestCreateFailed": "Error al crear el destino",
|
||||
"idpAddActionCreateNew": "Crear nuevo proveedor de identidad",
|
||||
"idpAddActionImportFromOrg": "Importar de otra organización",
|
||||
"idpImportDialogTitle": "Importar Proveedor de Identidad",
|
||||
"idpImportDialogDescription": "Elija un proveedor de identidad de una organización donde usted sea administrador. Se vinculará a esta organización.",
|
||||
"idpImportSearchPlaceholder": "Buscar por nombre de organización o proveedor...",
|
||||
"idpImportEmpty": "No se encontraron proveedores de identidad.",
|
||||
"idpImportedDescription": "Proveedor de identidad importado con éxito.",
|
||||
"idpDeleteGlobalQuestion": "¿Está seguro de que desea eliminar permanentemente este proveedor de identidad?",
|
||||
"idpDeleteGlobalDescription": "Esto eliminará permanentemente el proveedor de identidad de todas las organizaciones con las que está asociado.",
|
||||
"idpUnassociateTitle": "Desasociar Proveedor de Identidad",
|
||||
"idpUnassociateQuestion": "¿Está seguro de que desea desasociar este proveedor de identidad de esta organización?",
|
||||
"idpUnassociateDescription": "Todos los usuarios asociados con este proveedor de identidad serán eliminados de esta organización, pero el proveedor de identidad continuará existiendo para otras organizaciones asociadas.",
|
||||
"idpUnassociateConfirm": "Confirme Desasociar Proveedor de Identidad",
|
||||
"idpUnassociateWarning": "Esto no se puede deshacer para esta organización.",
|
||||
"idpUnassociatedDescription": "Proveedor de identidad desasociado de esta organización con éxito",
|
||||
"idpUnassociateMenu": "Desasociar",
|
||||
"idpDeleteAllOrgsMenu": "Eliminar"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Un nom d'affichage pour ce fournisseur d'identité",
|
||||
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs",
|
||||
"idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Vous pouvez configurer les paramètres de provisionnement automatique une fois le fournisseur d'identités créé.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Type de fournisseur",
|
||||
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Mappage de rôle par défaut",
|
||||
"defaultMappingsRoleDescription": "JMESPath pour extraire les informations de rôle du jeton ID. Le résultat de cette expression doit renvoyer le nom du rôle tel que défini dans l'organisation sous forme de chaîne.",
|
||||
"defaultMappingsOrg": "Mappage d'organisation par défaut",
|
||||
"defaultMappingsOrgDescription": "JMESPath pour extraire les informations d'organisation du jeton ID. Cette expression doit renvoyer l'ID de l'organisation ou true pour que l'utilisateur soit autorisé à accéder à l'organisation.",
|
||||
"defaultMappingsOrgDescription": "Lorsque défini, cette expression doit renvoyer l'identifiant de l'organisation ou vrai pour que l'utilisateur accède à cette organisation. Lorsqu'indéfini, définir un mappage de rôle est suffisant : l'utilisateur est autorisé tant qu'un mappage de rôle valide peut être résolu pour lui au sein de l'organisation.",
|
||||
"defaultMappingsSubmit": "Enregistrer les mappages par défaut",
|
||||
"orgPoliciesEdit": "Modifier la politique d'organisation",
|
||||
"org": "Organisation",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
|
||||
"introTitle": "Pangolin auto-hébergé géré",
|
||||
"introDescription": "est une option de déploiement conçue pour les personnes qui veulent de la simplicité et de la fiabilité tout en gardant leurs données privées et auto-hébergées.",
|
||||
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin — vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
|
||||
"introDetail": "Avec cette option, vous exécutez toujours votre propre nœud Pangolin - vos tunnels, la terminaison SSL et le trafic restent sur votre serveur. La différence est que la gestion et la surveillance sont gérées via notre tableau de bord du cloud, qui déverrouille un certain nombre d'avantages :",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Opérations plus simples",
|
||||
"description": "Pas besoin de faire tourner votre propre serveur de messagerie ou de configurer des alertes complexes. Vous obtiendrez des contrôles de santé et des alertes de temps d'arrêt par la suite."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Domaine international détecté",
|
||||
"willbestoredas": "Sera stocké comme :",
|
||||
"roleMappingDescription": "Détermine comment les rôles sont assignés aux utilisateurs lorsqu'ils se connectent lorsque la fourniture automatique est activée.",
|
||||
"roleMappingDescription": "Déterminez comment les rôles sont attribués aux utilisateurs lorsqu'ils se connectent avec ce fournisseur d'identité.",
|
||||
"selectRole": "Sélectionnez un rôle",
|
||||
"roleMappingExpression": "Expression",
|
||||
"selectRolePlaceholder": "Choisir un rôle",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Sélectionnez un domaine pour la page d'authentification de l'organisation",
|
||||
"domainPickerProvidedDomain": "Domaine fourni",
|
||||
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||
"domainPickerFreeDomainsPaidFeature": "Les domaines fournis sont une fonctionnalité payante. Abonnez-vous pour obtenir un domaine inclus avec votre plan — plus besoin de fournir le vôtre.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Les domaines fournis sont une fonctionnalité payante. Abonnez-vous pour obtenir un domaine inclus avec votre plan - plus besoin de fournir le vôtre.",
|
||||
"domainPickerVerified": "Vérifié",
|
||||
"domainPickerUnverified": "Non vérifié",
|
||||
"domainPickerManual": "Manuel",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Divulgation d'utilisation",
|
||||
"description": "Sélectionnez le niveau de licence qui correspond exactement à votre utilisation prévue. La Licence Personnelle autorise l'utilisation libre du Logiciel pour des activités commerciales individuelles, non commerciales ou à petite échelle avec un revenu annuel brut inférieur à 100 000 USD. Toute utilisation au-delà de ces limites — y compris l'utilisation au sein d'une entreprise, d'une organisation, ou tout autre environnement générateur de revenus — nécessite une licence d’entreprise valide et le paiement des droits de licence applicables. Tous les utilisateurs, qu'ils soient personnels ou d'entreprise, doivent se conformer aux conditions de licence commerciale Fossorial."
|
||||
"description": "Sélectionnez le niveau de licence qui correspond exactement à votre utilisation prévue. La Licence Personnelle autorise l'utilisation libre du Logiciel pour des activités commerciales individuelles, non commerciales ou à petite échelle avec un revenu annuel brut inférieur à 100 000 USD. Toute utilisation au-delà de ces limites - y compris l'utilisation au sein d'une entreprise, d'une organisation, ou tout autre environnement générateur de revenus - nécessite une licence d’entreprise valide et le paiement des droits de licence applicables. Tous les utilisateurs, qu'ils soient personnels ou d'entreprise, doivent se conformer aux conditions de licence commerciale Fossorial."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Informations sur la période d'essai",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "Tableau JSON",
|
||||
"httpDestFormatJsonArrayDescription": "Une requête par lot, le corps est un tableau JSON. Compatible avec la plupart des webhooks génériques et des datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Une requête par lot, body est un JSON délimité par une nouvelle ligne — un objet par ligne, pas de tableau extérieur. Requis par Splunk HEC, Elastic / OpenSearch, et Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "Une requête par lot, body est un JSON délimité par une nouvelle ligne - un objet par ligne, pas de tableau extérieur. Requis par Splunk HEC, Elastic / OpenSearch, et Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "Un événement par demande",
|
||||
"httpDestFormatSingleDescription": "Envoie un POST HTTP séparé pour chaque événement individuel. Utilisé uniquement pour les terminaux qui ne peuvent pas gérer des lots.",
|
||||
"httpDestLogTypesTitle": "Types de logs",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Destination mise à jour avec succès",
|
||||
"httpDestCreatedSuccess": "Destination créée avec succès",
|
||||
"httpDestUpdateFailed": "Impossible de mettre à jour la destination",
|
||||
"httpDestCreateFailed": "Impossible de créer la destination"
|
||||
"httpDestCreateFailed": "Impossible de créer la destination",
|
||||
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
||||
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
||||
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
||||
"idpImportDialogDescription": "Choisissez un fournisseur d'identités d'une organisation où vous êtes administrateur. Il sera lié à cette organisation.",
|
||||
"idpImportSearchPlaceholder": "Recherche par nom d'organisation ou de fournisseur...",
|
||||
"idpImportEmpty": "Aucun fournisseur d'identités trouvé.",
|
||||
"idpImportedDescription": "Fournisseur d'identités importé avec succès.",
|
||||
"idpDeleteGlobalQuestion": "Êtes-vous sûr de vouloir supprimer définitivement ce fournisseur d'identités?",
|
||||
"idpDeleteGlobalDescription": "Cela supprimera définitivement le fournisseur d'identités de toutes les organisations auxquelles il est associé.",
|
||||
"idpUnassociateTitle": "Dissocier le fournisseur d'identité",
|
||||
"idpUnassociateQuestion": "Êtes-vous sûr de vouloir dissocier ce fournisseur d'identités de cette organisation?",
|
||||
"idpUnassociateDescription": "Tous les utilisateurs associés à ce fournisseur d'identités seront retirés de cette organisation, mais le fournisseur d'identités continuera d'exister pour d'autres organisations associées.",
|
||||
"idpUnassociateConfirm": "Confirmer la dissociation du fournisseur d'identités",
|
||||
"idpUnassociateWarning": "Cela ne peut pas être annulé pour cette organisation.",
|
||||
"idpUnassociatedDescription": "Fournisseur d'identités dissocié de cette organisation avec succès",
|
||||
"idpUnassociateMenu": "Dissocier",
|
||||
"idpDeleteAllOrgsMenu": "Supprimer"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Un nome visualizzato per questo provider di identità",
|
||||
"idpAutoProvisionUsers": "Provisioning Automatico Utenti",
|
||||
"idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Puoi configurare le impostazioni di auto fornitura una volta creato il provider di identità.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo di Provider",
|
||||
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Mappatura Ruolo Predefinito",
|
||||
"defaultMappingsRoleDescription": "JMESPath per estrarre informazioni sul ruolo dal token ID. Il risultato di questa espressione deve restituire il nome del ruolo come definito nell'organizzazione come stringa.",
|
||||
"defaultMappingsOrg": "Mappatura Organizzazione Predefinita",
|
||||
"defaultMappingsOrgDescription": "JMESPath per estrarre informazioni sull'organizzazione dal token ID. Questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere all'organizzazione.",
|
||||
"defaultMappingsOrgDescription": "Quando impostata, questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere a quell'organizzazione. Quando non impostata, è sufficiente definire una mappatura di ruoli: l'utente è autorizzato se esiste una mappatura di ruolo valida per loro all'interno dell'organizzazione.",
|
||||
"defaultMappingsSubmit": "Salva Mappature Predefinite",
|
||||
"orgPoliciesEdit": "Modifica Politica Organizzazione",
|
||||
"org": "Organizzazione",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
|
||||
"introTitle": "Managed Self-Hosted Pangolin",
|
||||
"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 SSL 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:",
|
||||
"introDetail": "Con questa opzione, esegui ancora il tuo nodo Pangolin - i tunnel, la terminazione SSL 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": {
|
||||
"title": "Operazioni più semplici",
|
||||
"description": "Non è necessario eseguire il proprio server di posta o impostare un avviso complesso. Otterrai controlli di salute e avvisi di inattività fuori dalla casella."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Dominio Internazionale Rilevato",
|
||||
"willbestoredas": "Verrà conservato come:",
|
||||
"roleMappingDescription": "Determinare come i ruoli sono assegnati agli utenti quando accedono quando è abilitata la fornitura automatica.",
|
||||
"roleMappingDescription": "Determina come i ruoli vengono assegnati agli utenti quando si accede con questo provider di identità.",
|
||||
"selectRole": "Seleziona un ruolo",
|
||||
"roleMappingExpression": "Espressione",
|
||||
"selectRolePlaceholder": "Scegli un ruolo",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Seleziona un dominio per la pagina di autenticazione dell'organizzazione",
|
||||
"domainPickerProvidedDomain": "Dominio Fornito",
|
||||
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||
"domainPickerFreeDomainsPaidFeature": "I domini forniti sono una funzionalità a pagamento. Abbonati per ricevere un dominio incluso con il tuo piano — non è necessario portare il proprio.",
|
||||
"domainPickerFreeDomainsPaidFeature": "I domini forniti sono una funzionalità a pagamento. Abbonati per ricevere un dominio incluso con il tuo piano - non è necessario portare il proprio.",
|
||||
"domainPickerVerified": "Verificato",
|
||||
"domainPickerUnverified": "Non Verificato",
|
||||
"domainPickerManual": "Manuale",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Trasparenza Di Utilizzo",
|
||||
"description": "Seleziona il livello di licenza che rispecchia accuratamente il tuo utilizzo previsto. La Licenza Personale consente l'uso gratuito del Software per le attività commerciali individuali, non commerciali o su piccola scala con entrate lorde annue inferiori a $100.000 USD. Qualsiasi uso oltre questi limiti — compreso l'uso all'interno di un'azienda, organizzazione, o altro ambiente generatore di entrate — richiede una licenza Enterprise valida e il pagamento della tassa di licenza applicabile. Tutti gli utenti, siano essi personali o aziendali, devono rispettare i termini di licenza commerciale Fossorial."
|
||||
"description": "Seleziona il livello di licenza che rispecchia accuratamente il tuo utilizzo previsto. La Licenza Personale consente l'uso gratuito del Software per le attività commerciali individuali, non commerciali o su piccola scala con entrate lorde annue inferiori a $100.000 USD. Qualsiasi uso oltre questi limiti - compreso l'uso all'interno di un'azienda, organizzazione, o altro ambiente generatore di entrate - richiede una licenza Enterprise valida e il pagamento della tassa di licenza applicabile. Tutti gli utenti, siano essi personali o aziendali, devono rispettare i termini di licenza commerciale Fossorial."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Informazioni Periodo Di Prova",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON Array",
|
||||
"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.",
|
||||
"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.",
|
||||
"httpDestFormatSingleTitle": "Un Evento Per Richiesta",
|
||||
"httpDestFormatSingleDescription": "Invia un HTTP POST separato per ogni singolo evento. Usa solo per gli endpoint che non possono gestire i batch.",
|
||||
"httpDestLogTypesTitle": "Tipi Di Log",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Destinazione aggiornata con successo",
|
||||
"httpDestCreatedSuccess": "Destinazione creata con successo",
|
||||
"httpDestUpdateFailed": "Impossibile aggiornare la destinazione",
|
||||
"httpDestCreateFailed": "Impossibile creare la destinazione"
|
||||
"httpDestCreateFailed": "Impossibile creare la destinazione",
|
||||
"idpAddActionCreateNew": "Crea nuovo provider di identità",
|
||||
"idpAddActionImportFromOrg": "Importa da un'altra organizzazione",
|
||||
"idpImportDialogTitle": "Importa Provider di Identità",
|
||||
"idpImportDialogDescription": "Scegli un provider di identità da un'organizzazione di cui sei amministratore. Verrà collegato a questa organizzazione.",
|
||||
"idpImportSearchPlaceholder": "Cerca per nome organizzazione o provider...",
|
||||
"idpImportEmpty": "Nessun provider di identità trovato.",
|
||||
"idpImportedDescription": "Provider di identità importato con successo.",
|
||||
"idpDeleteGlobalQuestion": "Sei sicuro di voler eliminare definitivamente questo provider di identità?",
|
||||
"idpDeleteGlobalDescription": "Questo eliminerà definitivamente il provider di identità da tutte le organizzazioni con cui è associato.",
|
||||
"idpUnassociateTitle": "Disassociare Provider di Identità",
|
||||
"idpUnassociateQuestion": "Sei sicuro di voler disassociare questo provider di identità da questa organizzazione?",
|
||||
"idpUnassociateDescription": "Tutti gli utenti associati a questo provider di identità verranno rimossi da questa organizzazione, ma il provider di identità continuerà ad esistere per altre organizzazioni associate.",
|
||||
"idpUnassociateConfirm": "Conferma Disassociazione Provider di Identità",
|
||||
"idpUnassociateWarning": "Questo non può essere annullato per questa organizzazione.",
|
||||
"idpUnassociatedDescription": "Provider di identità disassociato con successo da questa organizzazione",
|
||||
"idpUnassociateMenu": "Disassocia",
|
||||
"idpDeleteAllOrgsMenu": "Elimina"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "이 신원 공급자를 위한 표시 이름",
|
||||
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
|
||||
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "아이덴티티 공급자가 생성되면 자동 프로비저닝 설정을 구성할 수 있습니다.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "제공자 유형",
|
||||
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "기본 역할 매핑",
|
||||
"defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.",
|
||||
"defaultMappingsOrg": "기본 조직 매핑",
|
||||
"defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다.",
|
||||
"defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다. 설정되지 않으면, 역할 매핑 정의가 충분합니다: 사용자는 유효한 역할 매핑이 해석되는 한 조직에 허용됩니다.",
|
||||
"defaultMappingsSubmit": "기본 매핑 저장",
|
||||
"orgPoliciesEdit": "조직 정책 편집",
|
||||
"org": "조직",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||
"willbestoredas": "다음으로 저장됩니다:",
|
||||
"roleMappingDescription": "자동 프로비저닝이 활성화되면 사용자가 로그인할 때 역할이 할당되는 방법을 결정합니다.",
|
||||
"roleMappingDescription": "사용자가 이 아이덴티티 공급자로 로그인할 때 역할이 할당되는 방법을 결정합니다.",
|
||||
"selectRole": "역할 선택",
|
||||
"roleMappingExpression": "표현식",
|
||||
"selectRolePlaceholder": "역할 선택",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "조직 인증 페이지에 대한 도메인을 선택하세요.",
|
||||
"domainPickerProvidedDomain": "제공된 도메인",
|
||||
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||
"domainPickerFreeDomainsPaidFeature": "제공된 도메인은 유료 기능입니다. 요금제에 도메인이 포함되도록 구독하세요. — 별도로 도메인을 준비할 필요 없습니다.",
|
||||
"domainPickerFreeDomainsPaidFeature": "제공된 도메인은 유료 기능입니다. 요금제에 도메인이 포함되도록 구독하세요. - 별도로 도메인을 준비할 필요 없습니다.",
|
||||
"domainPickerVerified": "검증됨",
|
||||
"domainPickerUnverified": "검증되지 않음",
|
||||
"domainPickerManual": "수동",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "사용 공개",
|
||||
"description": "당신의 의도된 사용에 정확히 맞는 라이선스 등급을 선택하세요. 개인 라이선스는 연간 총 수익 100,000 USD 이하의 개인, 비상업적 또는 소규모 상업 활동을 위한 소프트웨어의 무료 사용을 허용합니다. 이러한 제한을 넘는 모든 사용 — 비즈니스, 조직 또는 기타 수익 창출 환경 내에서의 사용 — 은 유효한 엔터프라이즈 라이선스 및 해당 라이선스 수수료의 지불이 필요합니다. 개인 또는 기업 사용자는 모두 Fossorial 상용 라이선스 조건을 준수해야 합니다."
|
||||
"description": "당신의 의도된 사용에 정확히 맞는 라이선스 등급을 선택하세요. 개인 라이선스는 연간 총 수익 100,000 USD 이하의 개인, 비상업적 또는 소규모 상업 활동을 위한 소프트웨어의 무료 사용을 허용합니다. 이러한 제한을 넘는 모든 사용 - 비즈니스, 조직 또는 기타 수익 창출 환경 내에서의 사용 - 은 유효한 엔터프라이즈 라이선스 및 해당 라이선스 수수료의 지불이 필요합니다. 개인 또는 기업 사용자는 모두 Fossorial 상용 라이선스 조건을 준수해야 합니다."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "시험 기간 정보",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON 배열",
|
||||
"httpDestFormatJsonArrayDescription": "각 배치마다 요청 하나씩, 본문은 JSON 배열입니다. 대부분의 일반 웹훅 및 Datadog과 호환됩니다.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "각 배치마다 요청 하나씩, 본문은 줄 구분 JSON — 한 라인에 하나의 객체가 있으며 외부 배열이 없습니다. Splunk HEC, Elastic / OpenSearch, Grafana Loki에 필요합니다.",
|
||||
"httpDestFormatNdjsonDescription": "각 배치마다 요청 하나씩, 본문은 줄 구분 JSON - 한 라인에 하나의 객체가 있으며 외부 배열이 없습니다. Splunk HEC, Elastic / OpenSearch, Grafana Loki에 필요합니다.",
|
||||
"httpDestFormatSingleTitle": "각 요청 당 하나의 이벤트",
|
||||
"httpDestFormatSingleDescription": "각 개별 이벤트에 대해 별도의 HTTP POST를 전송합니다. 배치를 처리할 수 없는 엔드포인트에만 사용하세요.",
|
||||
"httpDestLogTypesTitle": "로그 유형",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다",
|
||||
"httpDestCreatedSuccess": "대상지가 성공적으로 생성되었습니다",
|
||||
"httpDestUpdateFailed": "대상지를 업데이트하는 데 실패했습니다",
|
||||
"httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다"
|
||||
"httpDestCreateFailed": "대상지를 생성하는 데 실패했습니다",
|
||||
"idpAddActionCreateNew": "새로운 아이덴티티 공급자 생성",
|
||||
"idpAddActionImportFromOrg": "다른 조직에서 가져오기",
|
||||
"idpImportDialogTitle": "아이덴티티 공급자 가져오기",
|
||||
"idpImportDialogDescription": "관리자인 조직에서 아이덴티티 공급자를 선택하십시오. 이는 이 조직에 연결됩니다.",
|
||||
"idpImportSearchPlaceholder": "조직 또는 공급자 이름으로 검색...",
|
||||
"idpImportEmpty": "아이덴티티 공급자를 찾을 수 없습니다.",
|
||||
"idpImportedDescription": "아이덴티티 공급자가 성공적으로 가져왔습니다.",
|
||||
"idpDeleteGlobalQuestion": "정말로 이 아이덴티티 공급자를 영구적으로 삭제하시겠습니까?",
|
||||
"idpDeleteGlobalDescription": "이것은 연관된 모든 조직에서 아이덴티티 공급자를 영구적으로 삭제합니다.",
|
||||
"idpUnassociateTitle": "아이덴티티 공급자의 연관 해제",
|
||||
"idpUnassociateQuestion": "정말로 이 조직에서 이 아이덴티티 공급자의 연관을 해제하시겠습니까?",
|
||||
"idpUnassociateDescription": "이 아이덴티티 공급자와 연관된 모든 사용자는 이 조직에서 제거될 것이지만, 아이덴티티 공급자는 다른 연관된 조직에 계속해서 존재할 것입니다.",
|
||||
"idpUnassociateConfirm": "아이덴티티 공급자 연관 해제 확인",
|
||||
"idpUnassociateWarning": "이 조직에서 이것은 되돌릴 수 없습니다.",
|
||||
"idpUnassociatedDescription": "아이덴티티 공급자가 이 조직에서 성공적으로 연관 해제되었습니다",
|
||||
"idpUnassociateMenu": "연관 해제",
|
||||
"idpDeleteAllOrgsMenu": "삭제"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Et visningsnavn for denne identitetsleverandøren",
|
||||
"idpAutoProvisionUsers": "Automatisk brukerklargjøring",
|
||||
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisjonsinnstillingene når identitetsleverandøren er opprettet.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Leverandørtype",
|
||||
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Standard rolletilordning",
|
||||
"defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.",
|
||||
"defaultMappingsOrg": "Standard organisasjonstilordning",
|
||||
"defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.",
|
||||
"defaultMappingsOrgDescription": "Når denne er satt, må uttrykket returnere organisasjons-IDen eller «true» for at brukeren skal få tilgang til den organisasjonen. Når den ikke er satt, er det nok å definere en rolletilordning: brukeren gis tilgang så lenge en gyldig rolletilknytting kan løses for dem i organisasjonen.",
|
||||
"defaultMappingsSubmit": "Lagre standard tilordninger",
|
||||
"orgPoliciesEdit": "Rediger Organisasjonspolicy",
|
||||
"org": "Organisasjon",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Internasjonalt domene oppdaget",
|
||||
"willbestoredas": "Vil bli lagret som:",
|
||||
"roleMappingDescription": "Bestem hvordan roller tilordnes brukere når innloggingen er aktivert når autog-rapportering er aktivert.",
|
||||
"roleMappingDescription": "Bestem hvordan roller tildeles brukere når de logger inn med denne identitetsleverandøren.",
|
||||
"selectRole": "Velg en rolle",
|
||||
"roleMappingExpression": "Uttrykk",
|
||||
"selectRolePlaceholder": "Velg en rolle",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON liste",
|
||||
"httpDestFormatJsonArrayDescription": "Én forespørsel per batch, innholdet er en JSON-liste. Kompatibel med de mest generiske webhooks og Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON — et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "Én forespørsel per sats, innholdet er nytt avgrenset JSON - et objekt per linje, ingen ytterarray. Kreves av Splunk HEC, Elastisk/OpenSearch, og Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "En hendelse per forespørsel",
|
||||
"httpDestFormatSingleDescription": "Sender en separat HTTP POST for hver enkelt hendelse. Bruk bare for endepunkter som ikke kan håndtere batcher.",
|
||||
"httpDestLogTypesTitle": "Logg typer",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Målet er oppdatert",
|
||||
"httpDestCreatedSuccess": "Målet er opprettet",
|
||||
"httpDestUpdateFailed": "Kunne ikke oppdatere destinasjon",
|
||||
"httpDestCreateFailed": "Kan ikke opprette mål"
|
||||
"httpDestCreateFailed": "Kan ikke opprette mål",
|
||||
"idpAddActionCreateNew": "Opprett ny identitetsleverandør",
|
||||
"idpAddActionImportFromOrg": "Importer fra en annen organisasjon",
|
||||
"idpImportDialogTitle": "Importer identitetsleverandør",
|
||||
"idpImportDialogDescription": "Velg en identitetsleverandør fra en organisasjon der du er admin. Den vil bli knyttet til denne organisasjonen.",
|
||||
"idpImportSearchPlaceholder": "Søk etter organisasjons- eller leverandørnavn...",
|
||||
"idpImportEmpty": "Ingen identitetsleverandører funnet.",
|
||||
"idpImportedDescription": "Identitetsleverandøren ble importert vellykket.",
|
||||
"idpDeleteGlobalQuestion": "Er du sikker på at du vil slette denne identitetsleverandøren permanent?",
|
||||
"idpDeleteGlobalDescription": "Dette vil slette identitetsleverandøren permanent fra alle organisasjoner den er tilknyttet.",
|
||||
"idpUnassociateTitle": "Frakoble identitetsleverandør",
|
||||
"idpUnassociateQuestion": "Er du sikker på at du vil frakoble denne identitetsleverandøren fra denne organisasjonen?",
|
||||
"idpUnassociateDescription": "Alle brukere knyttet til denne identitetsleverandøren vil bli fjernet fra denne organisasjonen, men identitetsleverandøren vil fortsatt eksistere for andre tilknyttede organisasjoner.",
|
||||
"idpUnassociateConfirm": "Bekreft frakobling av identitetsleverandør",
|
||||
"idpUnassociateWarning": "Dette kan ikke angres for denne organisasjonen.",
|
||||
"idpUnassociatedDescription": "Identitetsleverandør er vellykket frakoblet fra denne organisasjonen",
|
||||
"idpUnassociateMenu": "Frakoble",
|
||||
"idpDeleteAllOrgsMenu": "Slett"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Een weergavenaam voor deze identiteitsprovider",
|
||||
"idpAutoProvisionUsers": "Auto Provisie Gebruikers",
|
||||
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "U kunt automatische voorzieningsinstellingen configureren zodra de identiteitsprovider is aangemaakt.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Type provider",
|
||||
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Standaard Rol Toewijzing",
|
||||
"defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.",
|
||||
"defaultMappingsOrg": "Standaard organisatie mapping",
|
||||
"defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.",
|
||||
"defaultMappingsOrgDescription": "Wanneer ingesteld, moet deze expressie de organisatie-ID of waar retourneren voor de gebruiker om toegang te krijgen tot die organisatie. Als het niet is ingesteld, is het definiëren van een roltoewijzing voldoende: de gebruiker is toegestaan zolang een geldige roltoewijzing voor hen binnen de organisatie kan worden opgelost.",
|
||||
"defaultMappingsSubmit": "Standaard toewijzingen opslaan",
|
||||
"orgPoliciesEdit": "Organisatie beleid bewerken",
|
||||
"org": "Organisatie",
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
|
||||
"willbestoredas": "Zal worden opgeslagen als:",
|
||||
"roleMappingDescription": "Bepaal hoe rollen worden toegewezen aan gebruikers wanneer ze inloggen wanneer Auto Provision is ingeschakeld.",
|
||||
"roleMappingDescription": "Bepaal hoe rollen aan gebruikers worden toegewezen wanneer ze zich aanmelden met deze identiteitsprovider.",
|
||||
"selectRole": "Selecteer een rol",
|
||||
"roleMappingExpression": "Expressie",
|
||||
"selectRolePlaceholder": "Kies een rol",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Selecteer een domein voor de authenticatiepagina van de organisatie",
|
||||
"domainPickerProvidedDomain": "Opgegeven domein",
|
||||
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||
"domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen — je hoeft er zelf geen mee te brengen.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Geleverde domeinen zijn een betaalde functie. Abonneer je om een domein bij je plan te krijgen - je hoeft er zelf geen mee te brengen.",
|
||||
"domainPickerVerified": "Geverifieerd",
|
||||
"domainPickerUnverified": "Ongeverifieerd",
|
||||
"domainPickerManual": "Handleiding",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
|
||||
"httpDestCreatedSuccess": "Bestemming succesvol aangemaakt",
|
||||
"httpDestUpdateFailed": "Bijwerken bestemming mislukt",
|
||||
"httpDestCreateFailed": "Aanmaken bestemming mislukt"
|
||||
"httpDestCreateFailed": "Aanmaken bestemming mislukt",
|
||||
"idpAddActionCreateNew": "Nieuwe identiteitsprovider aanmaken",
|
||||
"idpAddActionImportFromOrg": "Importeer vanuit een andere organisatie",
|
||||
"idpImportDialogTitle": "Importeer Identiteitsprovider",
|
||||
"idpImportDialogDescription": "Kies een identiteitsprovider van een organisatie waar u beheerder bent. Het wordt gekoppeld aan deze organisatie.",
|
||||
"idpImportSearchPlaceholder": "Zoek op organisatie- of providernamen...",
|
||||
"idpImportEmpty": "Geen identiteitsproviders gevonden.",
|
||||
"idpImportedDescription": "Identiteitsprovider succesvol geïmporteerd.",
|
||||
"idpDeleteGlobalQuestion": "Weet u zeker dat u deze identiteitsprovider permanent wilt verwijderen?",
|
||||
"idpDeleteGlobalDescription": "Hiermee wordt de identiteitsprovider permanent verwijderd uit alle organisaties waarmee het is geassocieerd.",
|
||||
"idpUnassociateTitle": "Koppel Identiteitsprovider los",
|
||||
"idpUnassociateQuestion": "Weet u zeker dat u deze identiteitsprovider van deze organisatie wilt loskoppelen?",
|
||||
"idpUnassociateDescription": "Alle gebruikers die aan deze identiteitsprovider zijn gekoppeld, worden uit deze organisatie verwijderd, maar de identiteitsprovider blijft bestaan voor andere gerelateerde organisaties.",
|
||||
"idpUnassociateConfirm": "Bevestig ontkoppelen identiteitsprovider",
|
||||
"idpUnassociateWarning": "Dit kan niet ongedaan worden gemaakt voor deze organisatie.",
|
||||
"idpUnassociatedDescription": "Identiteitsprovider succesvol losgekoppeld van deze organisatie",
|
||||
"idpUnassociateMenu": "Ontkoppelen",
|
||||
"idpDeleteAllOrgsMenu": "Verwijderen"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Nazwa wyświetlana dla tego dostawcy tożsamości",
|
||||
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników",
|
||||
"idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Możesz skonfigurować automatyczne ustawienia provision, gdy dostawca tożsamości zostanie utworzony.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Typ dostawcy",
|
||||
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Domyślne mapowanie roli",
|
||||
"defaultMappingsRoleDescription": "JMESPath do wydobycia informacji o roli z tokena ID. Wynik tego wyrażenia musi zwrócić nazwę roli zdefiniowaną w organizacji jako ciąg znaków.",
|
||||
"defaultMappingsOrg": "Domyślne mapowanie organizacji",
|
||||
"defaultMappingsOrgDescription": "JMESPath do wydobycia informacji o organizacji z tokena ID. To wyrażenie musi zwrócić ID organizacji lub true, aby użytkownik mógł uzyskać dostęp do organizacji.",
|
||||
"defaultMappingsOrgDescription": "Gdy jest ustawiona, ta wyrażenie musi zwrócić identyfikator organizacji lub true, aby użytkownik mógł uzyskać dostęp do tej organizacji. Gdy nie jest ustawiona, wystarczające jest zdefiniowanie mapowania ról: użytkownik jest dopuszczony, o ile można rozwiązać dla niego ważne mapowanie ról w organizacji.",
|
||||
"defaultMappingsSubmit": "Zapisz domyślne mapowania",
|
||||
"orgPoliciesEdit": "Edytuj politykę organizacji",
|
||||
"org": "Organizacja",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
|
||||
"introTitle": "Zarządzany samowystarczalny Pangolin",
|
||||
"introDescription": "jest opcją wdrażania zaprojektowaną dla osób, które chcą prostoty i dodatkowej niezawodności, przy jednoczesnym utrzymaniu swoich danych prywatnych i samodzielnych.",
|
||||
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin — tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
|
||||
"introDetail": "Z tą opcją nadal obsługujesz swój własny węzeł Pangolin - tunele, zakończenie SSL i ruch na Twoim serwerze. Różnica polega na tym, że zarządzanie i monitorowanie odbywa się za pomocą naszej tablicy rozdzielczej, która odblokowuje szereg korzyści:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Uproszczone operacje",
|
||||
"description": "Nie ma potrzeby uruchamiania własnego serwera pocztowego lub ustawiania skomplikowanych powiadomień. Będziesz mieć kontrolę zdrowia i powiadomienia o przestoju."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Wykryto międzynarodową domenę",
|
||||
"willbestoredas": "Będą przechowywane jako:",
|
||||
"roleMappingDescription": "Określ jak role są przypisywane do użytkowników podczas logowania się, gdy automatyczne świadczenie jest włączone.",
|
||||
"roleMappingDescription": "Określ, jak role są przypisywane użytkownikom podczas logowania się z tym dostawcą tożsamości.",
|
||||
"selectRole": "Wybierz rolę",
|
||||
"roleMappingExpression": "Wyrażenie",
|
||||
"selectRolePlaceholder": "Wybierz rolę",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Wybierz domenę dla strony uwierzytelniania organizacji",
|
||||
"domainPickerProvidedDomain": "Dostarczona domena",
|
||||
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||
"domainPickerFreeDomainsPaidFeature": "Dostarczane domeny to funkcja płatna. Subskrybuj, aby uzyskać domenę w ramach swojego planu — nie ma potrzeby przynoszenia własnej.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Dostarczane domeny to funkcja płatna. Subskrybuj, aby uzyskać domenę w ramach swojego planu - nie ma potrzeby przynoszenia własnej.",
|
||||
"domainPickerVerified": "Zweryfikowano",
|
||||
"domainPickerUnverified": "Niezweryfikowane",
|
||||
"domainPickerManual": "Podręcznik",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "Tablica JSON",
|
||||
"httpDestFormatJsonArrayDescription": "Jedna prośba na partię, treść jest tablicą JSON. Kompatybilna z najbardziej ogólnymi webhookami i Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Jedno żądanie na partię, ciałem jest plik JSON rozdzielony na newline-delimited — jeden obiekt na wiersz, bez tablicy zewnętrznej. Wymagane przez Splunk HEC, Elastic / OpenSesearch i Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "Jedno żądanie na partię, ciałem jest plik JSON rozdzielony na newline-delimited - jeden obiekt na wiersz, bez tablicy zewnętrznej. Wymagane przez Splunk HEC, Elastic / OpenSesearch i Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "Jedno wydarzenie na żądanie",
|
||||
"httpDestFormatSingleDescription": "Wysyła oddzielny POST HTTP dla każdego zdarzenia. Użyj tylko dla punktów końcowych, które nie mogą obsługiwać partii.",
|
||||
"httpDestLogTypesTitle": "Typy logów",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Cel został pomyślnie zaktualizowany",
|
||||
"httpDestCreatedSuccess": "Cel został utworzony pomyślnie",
|
||||
"httpDestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego",
|
||||
"httpDestCreateFailed": "Nie udało się utworzyć miejsca docelowego"
|
||||
"httpDestCreateFailed": "Nie udało się utworzyć miejsca docelowego",
|
||||
"idpAddActionCreateNew": "Utwórz nowego dostawcę tożsamości",
|
||||
"idpAddActionImportFromOrg": "Importuj z innej organizacji",
|
||||
"idpImportDialogTitle": "Importuj dostawcę tożsamości",
|
||||
"idpImportDialogDescription": "Wybierz dostawcę tożsamości z organizacji, w której jesteś administratorem. Zostanie on powiązany z tą organizacją.",
|
||||
"idpImportSearchPlaceholder": "Szukaj według nazwy organizacji lub dostawcy...",
|
||||
"idpImportEmpty": "Nie znaleziono dostawców tożsamości.",
|
||||
"idpImportedDescription": "Dostawca tożsamości został pomyślnie zaimportowany.",
|
||||
"idpDeleteGlobalQuestion": "Czy na pewno chcesz trwale usunąć tego dostawcę tożsamości?",
|
||||
"idpDeleteGlobalDescription": "Spowoduje to trwałe usunięcie dostawcy tożsamości ze wszystkich organizacji, z którymi jest powiązany.",
|
||||
"idpUnassociateTitle": "Odłącz dostawcę tożsamości",
|
||||
"idpUnassociateQuestion": "Czy na pewno chcesz odłączyć tego dostawcę tożsamości od tej organizacji?",
|
||||
"idpUnassociateDescription": "Wszystkie użytkownicy powiązani z tym dostawcą tożsamości zostaną usunięci z tej organizacji, ale dostawca tożsamości będzie nadal istniał dla innych powiązanych organizacji.",
|
||||
"idpUnassociateConfirm": "Potwierdź odłączenie dostawcy tożsamości",
|
||||
"idpUnassociateWarning": "Tego nie można cofnąć dla tej organizacji.",
|
||||
"idpUnassociatedDescription": "Dostawca tożsamości pomyślnie odłączony od tej organizacji",
|
||||
"idpUnassociateMenu": "Odłącz",
|
||||
"idpDeleteAllOrgsMenu": "Usuń"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Um nome de exibição para este provedor de identidade",
|
||||
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores",
|
||||
"idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Você pode configurar as definições de auto provisão assim que o provedor de identidade for criado.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Tipo de Provedor",
|
||||
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Mapeamento de Função Padrão",
|
||||
"defaultMappingsRoleDescription": "JMESPath para extrair informações de função do token ID. O resultado desta expressão deve retornar o nome da função como definido na organização como uma string.",
|
||||
"defaultMappingsOrg": "Mapeamento de Organização Padrão",
|
||||
"defaultMappingsOrgDescription": "JMESPath para extrair informações da organização do token ID. Esta expressão deve retornar o ID da organização ou verdadeiro para que o utilizador tenha permissão para aceder à organização.",
|
||||
"defaultMappingsOrgDescription": "Quando definida, esta expressão deve retornar o ID da organização ou verdadeiro para que o usuário acesse essa organização. Quando não definida, a definição de um mapeamento de papel é suficiente: o usuário é permitido desde que um mapeamento de papel válido possa ser resolvido para ele dentro da organização.",
|
||||
"defaultMappingsSubmit": "Guardar Mapeamentos Padrão",
|
||||
"orgPoliciesEdit": "Editar Política da Organização",
|
||||
"org": "Organização",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
|
||||
"introTitle": "Pangolin Auto-Hospedado Gerenciado",
|
||||
"introDescription": "é uma opção de implantação projetada para pessoas que querem simplicidade e confiança adicional, mantendo os seus dados privados e auto-hospedados.",
|
||||
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin — seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
|
||||
"introDetail": "Com esta opção, você ainda roda seu próprio nó Pangolin - seus túneis, terminação SSL e tráfego todos permanecem no seu servidor. A diferença é que a gestão e a monitorização são geridos através do nosso painel de nuvem, que desbloqueia vários benefícios:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Operações simples",
|
||||
"description": "Não é necessário executar o seu próprio servidor de e-mail ou configurar um alerta complexo. Você receberá fora de caixa verificações de saúde e alertas de tempo de inatividade."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Domínio Internacional Detectado",
|
||||
"willbestoredas": "Será armazenado como:",
|
||||
"roleMappingDescription": "Determinar como as funções são atribuídas aos usuários quando eles fazem login quando Auto Provisão está habilitada.",
|
||||
"roleMappingDescription": "Determine como os papéis são atribuídos aos usuários quando eles entram com este provedor de identidade.",
|
||||
"selectRole": "Selecione uma função",
|
||||
"roleMappingExpression": "Expressão",
|
||||
"selectRolePlaceholder": "Escolha uma função",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Selecione um domínio para a página de autenticação da organização",
|
||||
"domainPickerProvidedDomain": "Domínio fornecido",
|
||||
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Divulgação de uso",
|
||||
"description": "Selecione o nível de licença que reflete corretamente seu uso pretendido. A Licença Pessoal permite o uso livre do Software para atividades comerciais individuais, não comerciais ou em pequena escala com rendimento bruto anual inferior a 100.000 USD. Qualquer uso além destes limites — incluindo uso dentro de um negócio, organização, ou outro ambiente gerador de receitas — requer uma Licença Enterprise válida e o pagamento da taxa aplicável de licenciamento. Todos os usuários, pessoais ou empresariais, devem cumprir os Termos da Licença Comercial Fossorial."
|
||||
"description": "Selecione o nível de licença que reflete corretamente seu uso pretendido. A Licença Pessoal permite o uso livre do Software para atividades comerciais individuais, não comerciais ou em pequena escala com rendimento bruto anual inferior a 100.000 USD. Qualquer uso além destes limites - incluindo uso dentro de um negócio, organização, ou outro ambiente gerador de receitas - requer uma Licença Enterprise válida e o pagamento da taxa aplicável de licenciamento. Todos os usuários, pessoais ou empresariais, devem cumprir os Termos da Licença Comercial Fossorial."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Informações do Período de Avaliação",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "Matriz JSON",
|
||||
"httpDestFormatJsonArrayDescription": "Um pedido por lote, o corpo é um array JSON. Compatível com a maioria dos webhooks genéricos e Datadog.",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "Um pedido por lote, o corpo é um JSON delimitado por nova-linha — um objeto por linha, sem array exterior. Requerido pelo Splunk HEC, Elástico / OpenSearch, e Grafana Loki.",
|
||||
"httpDestFormatNdjsonDescription": "Um pedido por lote, o corpo é um JSON delimitado por nova-linha - um objeto por linha, sem array exterior. Requerido pelo Splunk HEC, Elástico / OpenSearch, e Grafana Loki.",
|
||||
"httpDestFormatSingleTitle": "Um Evento por Requisição",
|
||||
"httpDestFormatSingleDescription": "Envia um POST HTTP separado para cada evento. Utilize apenas para endpoints que não podem manipular lotes.",
|
||||
"httpDestLogTypesTitle": "Tipos de log",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Destino atualizado com sucesso",
|
||||
"httpDestCreatedSuccess": "Destino criado com sucesso",
|
||||
"httpDestUpdateFailed": "Falha ao atualizar destino",
|
||||
"httpDestCreateFailed": "Falha ao criar destino"
|
||||
"httpDestCreateFailed": "Falha ao criar destino",
|
||||
"idpAddActionCreateNew": "Criar novo provedor de identidade",
|
||||
"idpAddActionImportFromOrg": "Importar de outra organização",
|
||||
"idpImportDialogTitle": "Importar Provedor de Identidade",
|
||||
"idpImportDialogDescription": "Escolha um provedor de identidade de uma organização onde você é administrador. Ele será vinculado a esta organização.",
|
||||
"idpImportSearchPlaceholder": "Pesquisar por nome de organização ou provedor...",
|
||||
"idpImportEmpty": "Nenhum provedor de identidade encontrado.",
|
||||
"idpImportedDescription": "Provedor de identidade importado com sucesso.",
|
||||
"idpDeleteGlobalQuestion": "Tem certeza de que deseja eliminar permanentemente este provedor de identidade?",
|
||||
"idpDeleteGlobalDescription": "Isso eliminará permanentemente o provedor de identidade de todas as organizações com as quais está associado.",
|
||||
"idpUnassociateTitle": "Desassociar Provedor de Identidade",
|
||||
"idpUnassociateQuestion": "Tem certeza de que deseja desassociar este provedor de identidade desta organização?",
|
||||
"idpUnassociateDescription": "Todos os usuários associados a este provedor de identidade serão removidos desta organização, mas o provedor de identidade continuará a existir para outras organizações associadas.",
|
||||
"idpUnassociateConfirm": "Confirmar Desassociação do Provedor de Identidade",
|
||||
"idpUnassociateWarning": "Isso não pode ser desfeito para esta organização.",
|
||||
"idpUnassociatedDescription": "Provedor de identidade desassociado desta organização com sucesso",
|
||||
"idpUnassociateMenu": "Desassociar",
|
||||
"idpDeleteAllOrgsMenu": "Excluir"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"siteManageSites": "Управление сайтами",
|
||||
"siteDescription": "Создание и управление сайтами, чтобы включить подключение к приватным сетям",
|
||||
"sitesBannerTitle": "Подключить любую сеть",
|
||||
"sitesBannerDescription": "Сайт — это соединение с удаленной сетью, которое позволяет Pangolin предоставлять доступ к ресурсам, будь они общедоступными или частными, пользователям в любом месте. Установите сетевой коннектор сайта (Newt) там, где можно запустить исполняемый файл или контейнер, чтобы установить соединение.",
|
||||
"sitesBannerDescription": "Сайт - это соединение с удаленной сетью, которое позволяет Pangolin предоставлять доступ к ресурсам, будь они общедоступными или частными, пользователям в любом месте. Установите сетевой коннектор сайта (Newt) там, где можно запустить исполняемый файл или контейнер, чтобы установить соединение.",
|
||||
"sitesBannerButtonText": "Установить сайт",
|
||||
"approvalsBannerTitle": "Одобрить или запретить доступ к устройству",
|
||||
"approvalsBannerDescription": "Просмотрите и подтвердите или отклоните запросы на доступ к устройству от пользователей. Когда требуется подтверждение устройства, пользователи должны получить одобрение администратора, прежде чем их устройства смогут подключиться к ресурсам вашей организации.",
|
||||
@@ -163,7 +163,7 @@
|
||||
"proxyResourceTitle": "Управление публичными ресурсами",
|
||||
"proxyResourceDescription": "Создание и управление ресурсами, которые доступны через веб-браузер",
|
||||
"proxyResourcesBannerTitle": "Общедоступный доступ через веб",
|
||||
"proxyResourcesBannerDescription": "Общедоступные ресурсы — это прокси-по HTTPS или TCP/UDP, доступные любому пользователю в Интернете через веб-браузер. В отличие от частных ресурсов, они не требуют программного обеспечения на стороне клиента и могут включать политики доступа на основе идентификации и контекста.",
|
||||
"proxyResourcesBannerDescription": "Общедоступные ресурсы - это прокси-по HTTPS или TCP/UDP, доступные любому пользователю в Интернете через веб-браузер. В отличие от частных ресурсов, они не требуют программного обеспечения на стороне клиента и могут включать политики доступа на основе идентификации и контекста.",
|
||||
"clientResourceTitle": "Управление приватными ресурсами",
|
||||
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
|
||||
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
|
||||
@@ -371,7 +371,7 @@
|
||||
"provisioningKeysUpdated": "Ключ подготовки обновлен",
|
||||
"provisioningKeysUpdatedDescription": "Ваши изменения были сохранены.",
|
||||
"provisioningKeysBannerTitle": "Ключи подготовки сайта",
|
||||
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске — нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
||||
"provisioningKeysBannerDescription": "Создайте ключ настройки и используйте его с соединителем Newt для автоматического создания сайтов при первом запуске - нет необходимости настраивать отдельные учетные данные для каждого сайта.",
|
||||
"provisioningKeysBannerButtonText": "Узнать больше",
|
||||
"pendingSitesBannerTitle": "Ожидающие сайты",
|
||||
"pendingSitesBannerDescription": "Сайты, подключающиеся с помощью ключа настройки, отображаются здесь для проверки.",
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Отображаемое имя для этого поставщика удостоверений",
|
||||
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
|
||||
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Вы можете настроить параметры автоматического обеспечения после создания поставщика удостоверений.",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "Тип поставщика",
|
||||
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Сопоставление ролей по умолчанию",
|
||||
"defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.",
|
||||
"defaultMappingsOrg": "Сопоставление организаций по умолчанию",
|
||||
"defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.",
|
||||
"defaultMappingsOrgDescription": "При установке это выражение должно возвращать ID организации или true, чтобы пользователь мог получить доступ к этой организации. При отсутствии настройка отображения роли достаточно: пользователю разрешено войти, пока для него может быть решено отображение гарантированной роли в организации.",
|
||||
"defaultMappingsSubmit": "Сохранить сопоставления по умолчанию",
|
||||
"orgPoliciesEdit": "Редактировать политику организации",
|
||||
"org": "Организация",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
|
||||
"introTitle": "Управляемый Само-Хост Панголина",
|
||||
"introDescription": "- это вариант развертывания, предназначенный для людей, которые хотят простоты и надёжности, сохраняя при этом свои данные конфиденциальными и самостоятельными.",
|
||||
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin — туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
|
||||
"introDetail": "С помощью этой опции вы по-прежнему используете узел Pangolin - туннели, SSL, и весь остающийся на вашем сервере. Разница заключается в том, что управление и мониторинг осуществляются через нашу панель инструментов из облака, которая открывает ряд преимуществ:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Более простые операции",
|
||||
"description": "Не нужно запускать свой собственный почтовый сервер или настроить комплексное оповещение. Вы будете получать проверки состояния здоровья и оповещения о неисправностях из коробки."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Обнаружен международный домен",
|
||||
"willbestoredas": "Будет храниться как:",
|
||||
"roleMappingDescription": "Определите, как роли, назначаемые пользователям, когда они войдут в систему автоматического профиля.",
|
||||
"roleMappingDescription": "Определите, как роли присваиваются пользователям при входе с этим поставщиком удостоверений.",
|
||||
"selectRole": "Выберите роль",
|
||||
"roleMappingExpression": "Выражение",
|
||||
"selectRolePlaceholder": "Выберите роль",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "Выберите домен для страницы аутентификации организации",
|
||||
"domainPickerProvidedDomain": "Домен предоставлен",
|
||||
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||
"domainPickerFreeDomainsPaidFeature": "Предоставленные домены являются платной функцией. Подпишитесь, чтобы получить домен, включенный в ваш план — не нужно приносить свой собственный.",
|
||||
"domainPickerFreeDomainsPaidFeature": "Предоставленные домены являются платной функцией. Подпишитесь, чтобы получить домен, включенный в ваш план - не нужно приносить свой собственный.",
|
||||
"domainPickerVerified": "Подтверждено",
|
||||
"domainPickerUnverified": "Не подтверждено",
|
||||
"domainPickerManual": "Ручной",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Раскрытие",
|
||||
"description": "Выберите уровень лицензии, который точно отражает ваше предполагаемое использование. Личная Лицензия разрешает свободное использование Программного Обеспечения для частной, некоммерческой или малой коммерческой деятельности с годовым валовым доходом до $100 000 USD. Любое использование сверх этих пределов — включая использование в бизнесе, организацию, или другой приносящей доход среде — требует действительной лицензии предприятия и уплаты соответствующей лицензионной платы. Все пользователи, будь то Личные или Предприятия, обязаны соблюдать условия коммерческой лицензии Fossoral."
|
||||
"description": "Выберите уровень лицензии, который точно отражает ваше предполагаемое использование. Личная Лицензия разрешает свободное использование Программного Обеспечения для частной, некоммерческой или малой коммерческой деятельности с годовым валовым доходом до $100 000 USD. Любое использование сверх этих пределов - включая использование в бизнесе, организацию, или другой приносящей доход среде - требует действительной лицензии предприятия и уплаты соответствующей лицензионной платы. Все пользователи, будь то Личные или Предприятия, обязаны соблюдать условия коммерческой лицензии Fossoral."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Информация о пробном периоде",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Адрес назначения успешно обновлен",
|
||||
"httpDestCreatedSuccess": "Адрес назначения успешно создан",
|
||||
"httpDestUpdateFailed": "Не удалось обновить место назначения",
|
||||
"httpDestCreateFailed": "Не удалось создать место назначения"
|
||||
"httpDestCreateFailed": "Не удалось создать место назначения",
|
||||
"idpAddActionCreateNew": "Создать нового поставщика удостоверений",
|
||||
"idpAddActionImportFromOrg": "Импортировать из другой организации",
|
||||
"idpImportDialogTitle": "Импортировать поставщика удостоверений",
|
||||
"idpImportDialogDescription": "Выберите поставщика удостоверений из организации, где вы являетесь администратором. Он будет связан с этой организацией.",
|
||||
"idpImportSearchPlaceholder": "Поиск по организации или имени поставщика...",
|
||||
"idpImportEmpty": "Поставщики удостоверений не найдены.",
|
||||
"idpImportedDescription": "Поставщик удостоверений успешно импортирован.",
|
||||
"idpDeleteGlobalQuestion": "Вы уверены, что хотите навсегда удалить этого поставщика удостоверений?",
|
||||
"idpDeleteGlobalDescription": "Это навсегда удалит поставщика удостоверений из всех организаций, с которыми он связан.",
|
||||
"idpUnassociateTitle": "Рассоединить провайдера удостоверений",
|
||||
"idpUnassociateQuestion": "Вы уверены, что хотите рассоединить этого поставщика удостоверений с этой организацией?",
|
||||
"idpUnassociateDescription": "Все пользователи, связанные с этим поставщиком удостоверений, будут удалены из этой организации, но поставщик удостоверений будет продолжать существовать для других связанных организаций.",
|
||||
"idpUnassociateConfirm": "Подтвердите рассоединение поставщика удостоверений",
|
||||
"idpUnassociateWarning": "Это не может быть отменено для этой организации.",
|
||||
"idpUnassociatedDescription": "Поставщик удостоверений успешно рассоединен с этой организацией",
|
||||
"idpUnassociateMenu": "Рассоединить",
|
||||
"idpDeleteAllOrgsMenu": "Удалить"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "Bu kimlik sağlayıcı için bir görüntü adı",
|
||||
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
|
||||
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
|
||||
"idpAutoProvisionConfigureAfterCreate": "Kimlik sağlayıcı oluşturulduktan sonra otomatik sağlama ayarlarını yapılandırabilirsiniz.",
|
||||
"licenseBadge": " ",
|
||||
"idpType": "Sağlayıcı Türü",
|
||||
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "Varsayılan Rol Eşleme",
|
||||
"defaultMappingsRoleDescription": "JMESPath to extract role information from the ID token. The result of this expression must return the role name as defined in the organization as a string.",
|
||||
"defaultMappingsOrg": "Varsayılan Kuruluş Eşleme",
|
||||
"defaultMappingsOrgDescription": "JMESPath to extract organization information from the ID token. This expression must return the org ID or true for the user to be allowed to access the organization.",
|
||||
"defaultMappingsOrgDescription": "Ayarladığınızda, bu ifade kullanıcının o kuruluşa erişmesi için kuruluş kimliğini veya doğru değerini döndürmelidir. Ayarlamadığınızda, rol eşleme tanımlamak yeterlidir: kullanıcı, kuruluş içinde onlar için geçerli bir rol eşlemesi çözümlenebildiği sürece erişime izin verilir.",
|
||||
"defaultMappingsSubmit": "Varsayılan Eşlemeleri Kaydet",
|
||||
"orgPoliciesEdit": "Kuruluş Politikasını Düzenle",
|
||||
"org": "Kuruluş",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
|
||||
"introTitle": "Yönetilen Kendi Kendine Barındırılan Pangolin",
|
||||
"introDescription": "Bu, basitlik ve ekstra güvenilirlik arayan, ancak verilerini gizli tutmak ve kendi sunucularında barındırmak isteyen kişiler için tasarlanmış bir dağıtım seçeneğidir.",
|
||||
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz — tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
||||
"introDetail": "Bu seçenekle, kendi Pangolin düğümünüzü çalıştırmaya devam edersiniz - tünelleriniz, SSL bitişiniz ve trafiğiniz tamamen sunucunuzda kalır. Fark, yönetim ve izlemeyi bulut panomuz üzerinden gerçekleştiririz, bu da bir dizi avantaj sağlar:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "Daha basit işlemler",
|
||||
"description": "Kendi e-posta sunucunuzu çalıştırmanıza veya karmaşık uyarılar kurmanıza gerek yok. Sağlık kontrolleri ve kesinti uyarılarını kutudan çıktığı gibi alırsınız."
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
|
||||
"willbestoredas": "Şu şekilde depolanacak:",
|
||||
"roleMappingDescription": "Otomatik Sağlama etkinleştirildiğinde kullanıcıların oturum açarken rollerin nasıl atandığını belirleyin.",
|
||||
"roleMappingDescription": "Bu kimlik sağlayıcı ile oturum açıldığında kullanıcılara rollerin nasıl atandığını belirleyin.",
|
||||
"selectRole": "Bir Rol Seçin",
|
||||
"roleMappingExpression": "İfade",
|
||||
"selectRolePlaceholder": "Bir rol seçin",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "Kullanım Açıklaması",
|
||||
"description": "Kullanım amacınızı doğru bir şekilde yansıtan lisans seviyesini seçin. Kişisel Lisans, yazılımın bireysel, ticari olmayan veya yıllık geliri 100,000 ABD Dolarının altında olan küçük ölçekli ticari faaliyetlerde ücretsiz kullanılmasına izin verir. Bu sınırların ötesinde kullanım — bir işletme, organizasyon veya diğer gelir getirici ortamlarda kullanım dahil olmak üzere — geçerli bir Kurumsal Lisans ve ilgili lisans ücretinin ödenmesini gerektirir. Tüm kullanıcılar, ister Kişisel ister Kurumsal, Fossorial Ticari Lisans Şartlarına uymalıdır."
|
||||
"description": "Kullanım amacınızı doğru bir şekilde yansıtan lisans seviyesini seçin. Kişisel Lisans, yazılımın bireysel, ticari olmayan veya yıllık geliri 100,000 ABD Dolarının altında olan küçük ölçekli ticari faaliyetlerde ücretsiz kullanılmasına izin verir. Bu sınırların ötesinde kullanım - bir işletme, organizasyon veya diğer gelir getirici ortamlarda kullanım dahil olmak üzere - geçerli bir Kurumsal Lisans ve ilgili lisans ücretinin ödenmesini gerektirir. Tüm kullanıcılar, ister Kişisel ister Kurumsal, Fossorial Ticari Lisans Şartlarına uymalıdır."
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Deneme Süresi Bilgileri",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "Hedef başarıyla güncellendi",
|
||||
"httpDestCreatedSuccess": "Hedef başarıyla oluşturuldu",
|
||||
"httpDestUpdateFailed": "Hedef güncellenemedi",
|
||||
"httpDestCreateFailed": "Hedef oluşturulamadı"
|
||||
"httpDestCreateFailed": "Hedef oluşturulamadı",
|
||||
"idpAddActionCreateNew": "Yeni kimlik sağlayıcı oluştur",
|
||||
"idpAddActionImportFromOrg": "Başka bir kuruluştan içe aktar",
|
||||
"idpImportDialogTitle": "Kimlik Sağlayıcı İçe Aktar",
|
||||
"idpImportDialogDescription": "Bir kuruluştan yönetici olduğunuz bir kimlik sağlayıcı seçin. Bu kuruluşla ilişkilendirilecektir.",
|
||||
"idpImportSearchPlaceholder": "Kuruluş veya sağlayıcı adına göre ara...",
|
||||
"idpImportEmpty": "Hiçbir kimlik sağlayıcı bulunamadı.",
|
||||
"idpImportedDescription": "Kimlik sağlayıcı başarıyla içe aktarıldı.",
|
||||
"idpDeleteGlobalQuestion": "Bu kimlik sağlayıcıyı kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"idpDeleteGlobalDescription": "Bu, kimlik sağlayıcıyı ilişkilendirildiği tüm kuruluşlardan kalıcı olarak silecektir.",
|
||||
"idpUnassociateTitle": "Kimlik Sağlayıcının İlişkisini Kes",
|
||||
"idpUnassociateQuestion": "Bu kimlik sağlayıcının bu kuruluştan ilişiğini kesmek istediğinizden emin misiniz?",
|
||||
"idpUnassociateDescription": "Bu kimlik sağlayıcı ile ilişkilendirilen tüm kullanıcılar bu kuruluştan kaldırılacaktır, ancak kimlik sağlayıcı diğer ilişkilendirilen kuruluşlar için var olmaya devam edecektir.",
|
||||
"idpUnassociateConfirm": "Kimlik Sağlayıcının İlişkisinin Kesilmesini Onayla",
|
||||
"idpUnassociateWarning": "Bu işlem bu kuruluş için geri alınamaz.",
|
||||
"idpUnassociatedDescription": "Kimlik sağlayıcı bu kuruluştan başarıyla ayrıldı",
|
||||
"idpUnassociateMenu": "İlişkiyi Kes",
|
||||
"idpDeleteAllOrgsMenu": "Sil"
|
||||
}
|
||||
|
||||
@@ -898,6 +898,7 @@
|
||||
"idpDisplayName": "此身份提供商的显示名称",
|
||||
"idpAutoProvisionUsers": "自动提供用户",
|
||||
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
|
||||
"idpAutoProvisionConfigureAfterCreate": "您可以在创建身份提供者后配置自动配置设置。",
|
||||
"licenseBadge": "EE",
|
||||
"idpType": "提供者类型",
|
||||
"idpTypeDescription": "选择您想要配置的身份提供者类型",
|
||||
@@ -949,7 +950,7 @@
|
||||
"defaultMappingsRole": "默认角色映射",
|
||||
"defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。",
|
||||
"defaultMappingsOrg": "默认组织映射",
|
||||
"defaultMappingsOrgDescription": "此表达式必须返回 组织ID 或 true 才能允许用户访问组织。",
|
||||
"defaultMappingsOrgDescription": "设置时,此表达式必须返回组织ID或true才能让用户访问该组织。如果未设置,定义角色映射就足够了:只要在组织内可以为用户找出有效角色映射,用户就被允许进入。",
|
||||
"defaultMappingsSubmit": "保存默认映射",
|
||||
"orgPoliciesEdit": "编辑组织策略",
|
||||
"org": "组织",
|
||||
@@ -1993,7 +1994,7 @@
|
||||
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
|
||||
"introTitle": "托管自托管的潘戈林公司",
|
||||
"introDescription": "这是一种部署选择,为那些希望简洁和额外可靠的人设计,同时仍然保持他们的数据的私密性和自我托管性。",
|
||||
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 — — 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
|
||||
"introDetail": "通过此选项,您仍然运行您自己的 Pangolin 节点 - - 您的隧道、SSL 终止,并且流量在您的服务器上保持所有状态。 不同之处在于,管理和监测是通过我们的云层仪表板进行的,该仪表板开启了一些好处:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "简单的操作",
|
||||
"description": "无需运行您自己的邮件服务器或设置复杂的警报。您将从方框中获得健康检查和下限提醒。"
|
||||
@@ -2026,7 +2027,7 @@
|
||||
},
|
||||
"internationaldomaindetected": "检测到国际域",
|
||||
"willbestoredas": "储存为:",
|
||||
"roleMappingDescription": "确定当用户启用自动配送时如何分配他们的角色。",
|
||||
"roleMappingDescription": "确定当用户使用此身份提供者登陆时如何分配角色。",
|
||||
"selectRole": "选择角色",
|
||||
"roleMappingExpression": "表达式",
|
||||
"selectRolePlaceholder": "选择角色",
|
||||
@@ -2118,7 +2119,7 @@
|
||||
"selectDomainForOrgAuthPage": "选择组织认证页面的域",
|
||||
"domainPickerProvidedDomain": "提供的域",
|
||||
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||
"domainPickerFreeDomainsPaidFeature": "提供的域名是付费功能。订阅即可将域名包含在您的计划中—无需自带域名。",
|
||||
"domainPickerFreeDomainsPaidFeature": "提供的域名是付费功能。订阅即可将域名包含在您的计划中-无需自带域名。",
|
||||
"domainPickerVerified": "已验证",
|
||||
"domainPickerUnverified": "未验证",
|
||||
"domainPickerManual": "手动",
|
||||
@@ -2296,7 +2297,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "使用情况披露",
|
||||
"description": "选择能准确反映您预定用途的许可等级。 个人许可证允许对个人、非商业性或小型商业活动免费使用软件,年收入毛额不到100 000美元。 超出这些限度的任何用途,包括在企业、组织内的用途。 或其他创收环境——需要有效的企业许可证和支付适用的许可证费用。 所有用户,不论是个人还是企业,都必须遵守寄养商业许可证条款。"
|
||||
"description": "选择能准确反映您预定用途的许可等级。 个人许可证允许对个人、非商业性或小型商业活动免费使用软件,年收入毛额不到100 000美元。 超出这些限度的任何用途,包括在企业、组织内的用途。 或其他创收环境--需要有效的企业许可证和支付适用的许可证费用。 所有用户,不论是个人还是企业,都必须遵守寄养商业许可证条款。"
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "试用期信息",
|
||||
@@ -2881,7 +2882,7 @@
|
||||
"httpDestFormatJsonArrayTitle": "JSON 数组",
|
||||
"httpDestFormatJsonArrayDescription": "每批一个请求,实体是一个 JSON 数组。与大多数通用的 Web 钩子和数据兼容。",
|
||||
"httpDestFormatNdjsonTitle": "NDJSON",
|
||||
"httpDestFormatNdjsonDescription": "每批有一个请求,物体是换行符限制的 JSON ——每行一个对象,不是外部数组。 Sluk HEC、Elastic / OpenSearch和Grafana Loki所需。",
|
||||
"httpDestFormatNdjsonDescription": "每批有一个请求,物体是换行符限制的 JSON --每行一个对象,不是外部数组。 Sluk HEC、Elastic / OpenSearch和Grafana Loki所需。",
|
||||
"httpDestFormatSingleTitle": "每个请求一个事件",
|
||||
"httpDestFormatSingleDescription": "为每个事件单独发送一个 HTTP POST。仅用于无法处理批量的端点。",
|
||||
"httpDestLogTypesTitle": "日志类型",
|
||||
@@ -2899,5 +2900,22 @@
|
||||
"httpDestUpdatedSuccess": "目标已成功更新",
|
||||
"httpDestCreatedSuccess": "目标创建成功",
|
||||
"httpDestUpdateFailed": "更新目标失败",
|
||||
"httpDestCreateFailed": "创建目标失败"
|
||||
"httpDestCreateFailed": "创建目标失败",
|
||||
"idpAddActionCreateNew": "创建新的身份提供者",
|
||||
"idpAddActionImportFromOrg": "从另一个组织导入",
|
||||
"idpImportDialogTitle": "导入身份提供者",
|
||||
"idpImportDialogDescription": "从您是管理员的组织中选择一个身份提供者。它将关联到本组织。",
|
||||
"idpImportSearchPlaceholder": "按组织或提供者名称搜索……",
|
||||
"idpImportEmpty": "未找到身份提供者。",
|
||||
"idpImportedDescription": "身份提供者已成功导入。",
|
||||
"idpDeleteGlobalQuestion": "您确定要永久删除此身份提供者吗?",
|
||||
"idpDeleteGlobalDescription": "这将永久删除与其关联的所有组织中的身份提供者。",
|
||||
"idpUnassociateTitle": "取消关联身份提供者",
|
||||
"idpUnassociateQuestion": "您确定要将此身份提供者从此组织中取消关联吗?",
|
||||
"idpUnassociateDescription": "与此身份提供者关联的所有用户将从该组织中移除,但身份提供者仍会继续存在于关联的其他组织中。",
|
||||
"idpUnassociateConfirm": "确认取消关联身份提供者",
|
||||
"idpUnassociateWarning": "此操作无法对该组织撤销。",
|
||||
"idpUnassociatedDescription": "身份提供者已成功从该组织中取消关联",
|
||||
"idpUnassociateMenu": "取消关联",
|
||||
"idpDeleteAllOrgsMenu": "删除"
|
||||
}
|
||||
|
||||
@@ -1763,7 +1763,7 @@
|
||||
"description": "更可靠、維護成本更低的自架 Pangolin 伺服器,並附帶額外的附加功能",
|
||||
"introTitle": "託管式自架 Pangolin",
|
||||
"introDescription": "這是一種部署選擇,為那些希望簡潔和額外可靠的人設計,同時仍然保持他們的數據的私密性和自我託管性。",
|
||||
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 — — 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
|
||||
"introDetail": "通過此選項,您仍然運行您自己的 Pangolin 節點 - - 您的隧道、SSL 終止,並且流量在您的伺服器上保持所有狀態。 不同之處在於,管理和監測是通過我們的雲層儀錶板進行的,該儀錶板開啟了一些好處:",
|
||||
"benefitSimplerOperations": {
|
||||
"title": "簡單的操作",
|
||||
"description": "無需運行您自己的郵件伺服器或設置複雜的警報。您將從方框中獲得健康檢查和下限提醒。"
|
||||
@@ -2035,7 +2035,7 @@
|
||||
"alerts": {
|
||||
"commercialUseDisclosure": {
|
||||
"title": "使用情況披露",
|
||||
"description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境——需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。"
|
||||
"description": "選擇能準確反映您預定用途的許可等級。 個人許可證允許對個人、非商業性或小型商業活動免費使用軟體,年收入毛額不到 100,000 美元。 超出這些限度的任何用途,包括在企業、組織內的用途。 或其他創收環境--需要有效的企業許可證和支付適用的許可證費用。 所有用戶,不論是個人還是企業,都必須遵守寄養商業許可證條款。"
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "試用期資訊",
|
||||
|
||||
136
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.13.5",
|
||||
"better-sqlite3": "11.9.1",
|
||||
@@ -89,13 +90,13 @@
|
||||
"reodotdev": "1.1.0",
|
||||
"resend": "6.9.2",
|
||||
"semver": "7.7.4",
|
||||
"sshpk": "^1.18.0",
|
||||
"sshpk": "1.18.0",
|
||||
"stripe": "20.4.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.5.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"use-debounce": "^10.1.0",
|
||||
"use-debounce": "10.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
@@ -130,7 +131,7 @@
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/sshpk": "^1.17.4",
|
||||
"@types/sshpk": "1.17.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
@@ -1058,6 +1059,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -2353,6 +2355,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2375,6 +2378,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2397,6 +2401,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2413,6 +2418,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2429,6 +2435,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2445,6 +2452,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2461,6 +2469,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2477,6 +2486,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2493,6 +2503,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2509,6 +2520,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2525,6 +2537,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2541,6 +2554,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2563,6 +2577,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2585,6 +2600,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2607,6 +2623,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2629,6 +2646,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2651,6 +2669,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2673,6 +2692,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2695,6 +2715,7 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2714,6 +2735,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2733,6 +2755,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2752,6 +2775,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3011,6 +3035,7 @@
|
||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
@@ -6957,6 +6982,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz",
|
||||
"integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -8417,6 +8443,7 @@
|
||||
"version": "5.90.21",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
@@ -8532,6 +8559,7 @@
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -8691,7 +8719,6 @@
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
@@ -8807,7 +8834,6 @@
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
@@ -8842,7 +8868,6 @@
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
@@ -8852,7 +8877,6 @@
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
@@ -8879,6 +8903,7 @@
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -8974,6 +8999,7 @@
|
||||
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -9001,6 +9027,7 @@
|
||||
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
@@ -9026,6 +9053,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -9036,6 +9064,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -9122,8 +9151,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
@@ -9197,6 +9225,7 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -9651,6 +9680,38 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz",
|
||||
"integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.68",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.68",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz",
|
||||
"integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -9670,6 +9731,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -10118,6 +10180,7 @@
|
||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.0"
|
||||
}
|
||||
@@ -10189,6 +10252,7 @@
|
||||
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -10317,6 +10381,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -10499,6 +10564,12 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
@@ -11223,6 +11294,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -11663,7 +11735,6 @@
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -12298,6 +12369,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -12383,6 +12455,7 @@
|
||||
"integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
@@ -12519,6 +12592,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -12912,6 +12986,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -15329,7 +15404,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@@ -15340,7 +15414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@@ -15429,6 +15502,7 @@
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz",
|
||||
"integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.14",
|
||||
"@swc/helpers": "0.5.15",
|
||||
@@ -16388,6 +16462,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.12.0",
|
||||
"pg-pool": "^3.13.0",
|
||||
@@ -16892,6 +16967,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -16923,6 +16999,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -17215,6 +17292,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
||||
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -18676,7 +18754,8 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.2",
|
||||
@@ -19151,6 +19230,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -19578,6 +19658,7 @@
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
@@ -19784,6 +19865,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -19799,6 +19881,34 @@
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.90.21",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.13.5",
|
||||
"better-sqlite3": "11.9.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 765 KiB After Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 742 KiB After Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 765 KiB After Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 274 KiB |
BIN
public/third-party/incidentio.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/third-party/opsgenie.png
vendored
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
public/third-party/pgd.png
vendored
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/third-party/servicenow.png
vendored
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
@@ -144,7 +144,19 @@ export enum ActionsEnum {
|
||||
createEventStreamingDestination = "createEventStreamingDestination",
|
||||
updateEventStreamingDestination = "updateEventStreamingDestination",
|
||||
deleteEventStreamingDestination = "deleteEventStreamingDestination",
|
||||
listEventStreamingDestinations = "listEventStreamingDestinations"
|
||||
listEventStreamingDestinations = "listEventStreamingDestinations",
|
||||
createAlertRule = "createAlertRule",
|
||||
updateAlertRule = "updateAlertRule",
|
||||
deleteAlertRule = "deleteAlertRule",
|
||||
listAlertRules = "listAlertRules",
|
||||
getAlertRule = "getAlertRule",
|
||||
createHealthCheck = "createHealthCheck",
|
||||
updateHealthCheck = "updateHealthCheck",
|
||||
deleteHealthCheck = "deleteHealthCheck",
|
||||
listHealthChecks = "listHealthChecks",
|
||||
triggerSiteAlert = "triggerSiteAlert",
|
||||
triggerResourceAlert = "triggerResourceAlert",
|
||||
triggerHealthCheckAlert = "triggerHealthCheckAlert"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -16,11 +16,14 @@ import {
|
||||
domains,
|
||||
orgs,
|
||||
targets,
|
||||
roles,
|
||||
users,
|
||||
exitNodes,
|
||||
sessions,
|
||||
clients,
|
||||
resources,
|
||||
siteResources,
|
||||
targetHealthCheck,
|
||||
sites
|
||||
} from "./schema";
|
||||
|
||||
@@ -425,7 +428,9 @@ export const eventStreamingDestinations = pgTable(
|
||||
orgId: varchar("orgId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
sendConnectionLogs: boolean("sendConnectionLogs").notNull().default(false),
|
||||
sendConnectionLogs: boolean("sendConnectionLogs")
|
||||
.notNull()
|
||||
.default(false),
|
||||
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
|
||||
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
|
||||
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
|
||||
@@ -447,7 +452,9 @@ export const eventStreamingCursors = pgTable(
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
logType: varchar("logType", { length: 50 }).notNull(), // "request" | "action" | "access" | "connection"
|
||||
lastSentId: bigint("lastSentId", { mode: "number" }).notNull().default(0),
|
||||
lastSentId: bigint("lastSentId", { mode: "number" })
|
||||
.notNull()
|
||||
.default(0),
|
||||
lastSentAt: bigint("lastSentAt", { mode: "number" }) // epoch milliseconds, null if never sent
|
||||
},
|
||||
(table) => [
|
||||
@@ -458,6 +465,104 @@ export const eventStreamingCursors = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const alertRules = pgTable("alertRules", {
|
||||
alertRuleId: serial("alertRuleId").primaryKey(),
|
||||
orgId: varchar("orgId", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
// Single field encodes both source and trigger - no redundancy
|
||||
eventType: varchar("eventType", { length: 100 })
|
||||
.$type<
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle"
|
||||
>()
|
||||
.notNull(),
|
||||
// Nullable depending on eventType
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
cooldownSeconds: integer("cooldownSeconds").notNull().default(300),
|
||||
allSites: boolean("allSites").notNull().default(false),
|
||||
allHealthChecks: boolean("allHealthChecks").notNull().default(false),
|
||||
allResources: boolean("allResources").notNull().default(false),
|
||||
lastTriggeredAt: bigint("lastTriggeredAt", { mode: "number" }), // nullable
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const alertSites = pgTable("alertSites", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const alertHealthChecks = pgTable("alertHealthChecks", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
healthCheckId: integer("healthCheckId")
|
||||
.notNull()
|
||||
.references(() => targetHealthCheck.targetHealthCheckId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const alertResources = pgTable("alertResources", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
// Separating channels by type avoids the mixed-shape problem entirely
|
||||
export const alertEmailActions = pgTable("alertEmailActions", {
|
||||
emailActionId: serial("emailActionId").primaryKey(),
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
lastSentAt: bigint("lastSentAt", { mode: "number" }) // nullable
|
||||
});
|
||||
|
||||
export const alertEmailRecipients = pgTable("alertEmailRecipients", {
|
||||
recipientId: serial("recipientId").primaryKey(),
|
||||
emailActionId: integer("emailActionId")
|
||||
.notNull()
|
||||
.references(() => alertEmailActions.emailActionId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
// At least one of these should be set - enforced at app level
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
roleId: integer("roleId").references(() => roles.roleId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
email: varchar("email", { length: 255 }) // external emails not tied to a user
|
||||
});
|
||||
|
||||
export const alertWebhookActions = pgTable("alertWebhookActions", {
|
||||
webhookActionId: serial("webhookActionId").primaryKey(),
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
config: text("config"), // encrypted JSON with auth config (authType, credentials)
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
lastSentAt: bigint("lastSentAt", { mode: "number" }) // nullable
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
@@ -495,3 +600,4 @@ export type EventStreamingDestination = InferSelectModel<
|
||||
export type EventStreamingCursor = InferSelectModel<
|
||||
typeof eventStreamingCursors
|
||||
>;
|
||||
export type AlertResources = InferSelectModel<typeof alertResources>;
|
||||
|
||||
@@ -57,7 +57,9 @@ export const orgs = pgTable("orgs", {
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
settingsLogRetentionDaysConnection: integer(
|
||||
"settingsLogRetentionDaysConnection"
|
||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
@@ -101,7 +103,9 @@ export const sites = pgTable("sites", {
|
||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||
listenPort: integer("listenPort"),
|
||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||
status: varchar("status").$type<"pending" | "approved">().default("approved")
|
||||
status: varchar("status")
|
||||
.$type<"pending" | "approved">()
|
||||
.default("approved")
|
||||
});
|
||||
|
||||
export const resources = pgTable("resources", {
|
||||
@@ -182,9 +186,15 @@ export const targets = pgTable("targets", {
|
||||
|
||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
||||
targetId: integer("targetId")
|
||||
.notNull()
|
||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||
targetId: integer("targetId").references(() => targets.targetId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name"),
|
||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||
hcPath: varchar("hcPath"),
|
||||
hcScheme: varchar("hcScheme"),
|
||||
@@ -201,7 +211,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
hcHealth: text("hcHealth")
|
||||
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName")
|
||||
hcTlsServerName: text("hcTlsServerName"),
|
||||
hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
|
||||
hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
|
||||
});
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
@@ -222,16 +234,23 @@ export const exitNodes = pgTable("exitNodes", {
|
||||
export const siteResources = pgTable("siteResources", {
|
||||
// this is for the clients
|
||||
siteResourceId: serial("siteResourceId").primaryKey(),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
networkId: integer("networkId").references(() => networks.networkId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
defaultNetworkId: integer("defaultNetworkId").references(
|
||||
() => networks.networkId,
|
||||
{
|
||||
onDelete: "restrict"
|
||||
}
|
||||
),
|
||||
niceId: varchar("niceId").notNull(),
|
||||
name: varchar("name").notNull(),
|
||||
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
protocol: varchar("protocol"), // only for port mode
|
||||
ssl: boolean("ssl").notNull().default(false),
|
||||
mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
||||
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||
@@ -244,7 +263,38 @@ export const siteResources = pgTable("siteResources", {
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
.default("site"),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
subdomain: varchar("subdomain"),
|
||||
fullDomain: varchar("fullDomain")
|
||||
});
|
||||
|
||||
export const networks = pgTable("networks", {
|
||||
networkId: serial("networkId").primaryKey(),
|
||||
niceId: text("niceId"),
|
||||
name: text("name"),
|
||||
scope: varchar("scope")
|
||||
.$type<"global" | "resource">()
|
||||
.notNull()
|
||||
.default("global"),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const siteNetworks = pgTable("siteNetworks", {
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
networkId: integer("networkId")
|
||||
.notNull()
|
||||
.references(() => networks.networkId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||
@@ -994,6 +1044,7 @@ export const requestAuditLog = pgTable(
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
siteResourceId: integer("siteResourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
@@ -1041,6 +1092,20 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
||||
complete: boolean("complete").notNull().default(false)
|
||||
});
|
||||
|
||||
export const statusHistory = pgTable("statusHistory", {
|
||||
id: serial("id").primaryKey(),
|
||||
entityType: varchar("entityType").notNull(),
|
||||
entityId: integer("entityId").notNull(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: varchar("status").notNull(),
|
||||
timestamp: integer("timestamp").notNull(),
|
||||
}, (table) => [
|
||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||
]);
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -1107,3 +1172,5 @@ export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
export type Network = InferSelectModel<typeof networks>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
|
||||
@@ -13,9 +13,12 @@ import {
|
||||
domains,
|
||||
exitNodes,
|
||||
orgs,
|
||||
resources,
|
||||
roles,
|
||||
sessions,
|
||||
siteResources,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
users
|
||||
} from "./schema";
|
||||
|
||||
@@ -455,6 +458,94 @@ export const eventStreamingCursors = sqliteTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const alertRules = sqliteTable("alertRules", {
|
||||
alertRuleId: integer("alertRuleId").primaryKey({ autoIncrement: true }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
eventType: text("eventType")
|
||||
.$type<
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle"
|
||||
>()
|
||||
.notNull(),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
cooldownSeconds: integer("cooldownSeconds").notNull().default(300),
|
||||
allSites: integer("allSites", { mode: "boolean" }).notNull().default(false),
|
||||
allHealthChecks: integer("allHealthChecks", { mode: "boolean" }).notNull().default(false),
|
||||
allResources: integer("allResources", { mode: "boolean" }).notNull().default(false),
|
||||
lastTriggeredAt: integer("lastTriggeredAt"),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt").notNull()
|
||||
});
|
||||
|
||||
export const alertSites = sqliteTable("alertSites", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const alertHealthChecks = sqliteTable("alertHealthChecks", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
healthCheckId: integer("healthCheckId")
|
||||
.notNull()
|
||||
.references(() => targetHealthCheck.targetHealthCheckId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
});
|
||||
|
||||
export const alertResources = sqliteTable("alertResources", {
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const alertEmailActions = sqliteTable("alertEmailActions", {
|
||||
emailActionId: integer("emailActionId").primaryKey({ autoIncrement: true }),
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
lastSentAt: integer("lastSentAt")
|
||||
});
|
||||
|
||||
export const alertEmailRecipients = sqliteTable("alertEmailRecipients", {
|
||||
recipientId: integer("recipientId").primaryKey({ autoIncrement: true }),
|
||||
emailActionId: integer("emailActionId")
|
||||
.notNull()
|
||||
.references(() => alertEmailActions.emailActionId, { onDelete: "cascade" }),
|
||||
userId: text("userId").references(() => users.userId, { onDelete: "cascade" }),
|
||||
roleId: integer("roleId").references(() => roles.roleId, { onDelete: "cascade" }),
|
||||
email: text("email")
|
||||
});
|
||||
|
||||
export const alertWebhookActions = sqliteTable("alertWebhookActions", {
|
||||
webhookActionId: integer("webhookActionId").primaryKey({ autoIncrement: true }),
|
||||
alertRuleId: integer("alertRuleId")
|
||||
.notNull()
|
||||
.references(() => alertRules.alertRuleId, { onDelete: "cascade" }),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
config: text("config"), // encrypted JSON with auth config (authType, credentials)
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
lastSentAt: integer("lastSentAt")
|
||||
});
|
||||
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
@@ -486,3 +577,4 @@ export type EventStreamingDestination = InferSelectModel<
|
||||
export type EventStreamingCursor = InferSelectModel<
|
||||
typeof eventStreamingCursors
|
||||
>;
|
||||
export type AlertResources = InferSelectModel<typeof alertResources>;
|
||||
|
||||
@@ -54,7 +54,9 @@ export const orgs = sqliteTable("orgs", {
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
settingsLogRetentionDaysConnection: integer(
|
||||
"settingsLogRetentionDaysConnection"
|
||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||
.notNull()
|
||||
.default(0),
|
||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||
@@ -92,6 +94,9 @@ export const sites = sqliteTable("sites", {
|
||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
networkId: integer("networkId").references(() => networks.networkId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
pubKey: text("pubKey"),
|
||||
subnet: text("subnet"),
|
||||
@@ -204,9 +209,15 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
targetId: integer("targetId")
|
||||
.notNull()
|
||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||
targetId: integer("targetId").references(() => targets.targetId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: text("name"),
|
||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -227,7 +238,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
hcHealth: text("hcHealth")
|
||||
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName")
|
||||
hcTlsServerName: text("hcTlsServerName"),
|
||||
hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
|
||||
hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
|
||||
});
|
||||
|
||||
export const exitNodes = sqliteTable("exitNodes", {
|
||||
@@ -250,16 +263,21 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
siteResourceId: integer("siteResourceId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
networkId: integer("networkId").references(() => networks.networkId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
defaultNetworkId: integer("defaultNetworkId").references(
|
||||
() => networks.networkId,
|
||||
{ onDelete: "restrict" }
|
||||
),
|
||||
niceId: text("niceId").notNull(),
|
||||
name: text("name").notNull(),
|
||||
mode: text("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||
protocol: text("protocol"), // only for port mode
|
||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||
mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
|
||||
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||
proxyPort: integer("proxyPort"), // only for port mode
|
||||
destinationPort: integer("destinationPort"), // only for port mode
|
||||
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||
@@ -274,7 +292,36 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||
authDaemonMode: text("authDaemonMode")
|
||||
.$type<"site" | "remote">()
|
||||
.default("site")
|
||||
.default("site"),
|
||||
domainId: text("domainId").references(() => domains.domainId, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
subdomain: text("subdomain"),
|
||||
fullDomain: text("fullDomain")
|
||||
});
|
||||
|
||||
export const networks = sqliteTable("networks", {
|
||||
networkId: integer("networkId").primaryKey({ autoIncrement: true }),
|
||||
niceId: text("niceId"),
|
||||
name: text("name"),
|
||||
scope: text("scope")
|
||||
.$type<"global" | "resource">()
|
||||
.notNull()
|
||||
.default("global"),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const siteNetworks = sqliteTable("siteNetworks", {
|
||||
siteId: integer("siteId")
|
||||
.notNull()
|
||||
.references(() => sites.siteId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
networkId: integer("networkId")
|
||||
.notNull()
|
||||
.references(() => networks.networkId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||
@@ -1096,6 +1143,7 @@ export const requestAuditLog = sqliteTable(
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
siteResourceId: integer("siteResourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
@@ -1143,6 +1191,20 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
||||
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||
});
|
||||
|
||||
export const statusHistory = sqliteTable("statusHistory", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
||||
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
||||
timestamp: integer("timestamp").notNull(), // unix epoch seconds
|
||||
}, (table) => [
|
||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
||||
]);
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -1195,6 +1257,7 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||
export type Network = InferSelectModel<typeof networks>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||
@@ -1209,3 +1272,4 @@ export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||
export type RoundTripMessageTracker = InferSelectModel<
|
||||
typeof roundTripMessageTracker
|
||||
>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
|
||||
201
server/emails/templates/AlertNotification.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React from "react";
|
||||
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||
import { themeColors } from "./lib/theme";
|
||||
import {
|
||||
EmailContainer,
|
||||
EmailFooter,
|
||||
EmailGreeting,
|
||||
EmailHeading,
|
||||
EmailInfoSection,
|
||||
EmailLetterHead,
|
||||
EmailSignature,
|
||||
EmailText
|
||||
} from "./components/Email";
|
||||
|
||||
export type AlertEventType =
|
||||
| "site_online"
|
||||
| "site_offline"
|
||||
| "site_toggle"
|
||||
| "health_check_healthy"
|
||||
| "health_check_unhealthy"
|
||||
| "health_check_toggle"
|
||||
| "resource_healthy"
|
||||
| "resource_unhealthy"
|
||||
| "resource_toggle";
|
||||
|
||||
interface Props {
|
||||
eventType: AlertEventType;
|
||||
orgId: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function getEventMeta(eventType: AlertEventType): {
|
||||
heading: string;
|
||||
previewText: string;
|
||||
summary: string;
|
||||
statusLabel: string;
|
||||
statusColor: string;
|
||||
} {
|
||||
switch (eventType) {
|
||||
case "site_online":
|
||||
return {
|
||||
heading: "Site Back Online",
|
||||
previewText: "A site in your organization is back online.",
|
||||
summary:
|
||||
"Good news – a site in your organization has come back online and is now reachable.",
|
||||
statusLabel: "Online",
|
||||
statusColor: "#16a34a"
|
||||
};
|
||||
case "site_offline":
|
||||
return {
|
||||
heading: "Site Offline",
|
||||
previewText: "A site in your organization has gone offline.",
|
||||
summary:
|
||||
"A site in your organization has gone offline and is no longer reachable. Please investigate as soon as possible.",
|
||||
statusLabel: "Offline",
|
||||
statusColor: "#dc2626"
|
||||
};
|
||||
case "site_toggle":
|
||||
return {
|
||||
heading: "Site Status Changed",
|
||||
previewText: "A site in your organization has changed status.",
|
||||
summary:
|
||||
"A site in your organization has changed status. Please review the details below and take action if needed.",
|
||||
statusLabel: "Status Changed",
|
||||
statusColor: "#f59e0b"
|
||||
};
|
||||
case "health_check_healthy":
|
||||
return {
|
||||
heading: "Health Check Recovered",
|
||||
previewText:
|
||||
"A health check in your organization is now healthy.",
|
||||
summary:
|
||||
"A health check in your organization has recovered and is now reporting a healthy status.",
|
||||
statusLabel: "Healthy",
|
||||
statusColor: "#16a34a"
|
||||
};
|
||||
case "health_check_unhealthy":
|
||||
return {
|
||||
heading: "Health Check Failing",
|
||||
previewText:
|
||||
"A health check in your organization is not healthy.",
|
||||
summary:
|
||||
"A health check in your organization is currently failing. Please review the details below and take action if needed.",
|
||||
statusLabel: "Not Healthy",
|
||||
statusColor: "#dc2626"
|
||||
};
|
||||
case "health_check_toggle":
|
||||
return {
|
||||
heading: "Health Check Status Changed",
|
||||
previewText:
|
||||
"A health check in your organization has changed status.",
|
||||
summary:
|
||||
"A health check in your organization has changed status. Please review the details below and take action if needed.",
|
||||
statusLabel: "Status Changed",
|
||||
statusColor: "#f59e0b"
|
||||
};
|
||||
case "resource_healthy":
|
||||
return {
|
||||
heading: "Resource Healthy",
|
||||
previewText: "A resource in your organization is now healthy.",
|
||||
summary:
|
||||
"A resource in your organization has recovered and is now reporting a healthy status.",
|
||||
statusLabel: "Healthy",
|
||||
statusColor: "#16a34a"
|
||||
};
|
||||
case "resource_unhealthy":
|
||||
return {
|
||||
heading: "Resource Unhealthy",
|
||||
previewText: "A resource in your organization is not healthy.",
|
||||
summary:
|
||||
"A resource in your organization is currently unhealthy. Please review the details below and take action if needed.",
|
||||
statusLabel: "Unhealthy",
|
||||
statusColor: "#dc2626"
|
||||
};
|
||||
case "resource_toggle":
|
||||
return {
|
||||
heading: "Resource Status Changed",
|
||||
previewText:
|
||||
"A resource in your organization has changed status.",
|
||||
summary:
|
||||
"A resource in your organization has changed status. Please review the details below and take action if needed.",
|
||||
statusLabel: "Status Changed",
|
||||
statusColor: "#f59e0b"
|
||||
};
|
||||
default:
|
||||
return {
|
||||
heading: "Alert Notification",
|
||||
previewText: "An alert event has occurred in your organization.",
|
||||
summary:
|
||||
"An alert event has occurred in your organization. Please review the details below and take action if needed.",
|
||||
statusLabel: "Alert",
|
||||
statusColor: "#f59e0b"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatDataItems(
|
||||
data: Record<string, unknown>
|
||||
): { label: string; value: React.ReactNode }[] {
|
||||
return Object.entries(data)
|
||||
.filter(([key]) => key !== "orgId")
|
||||
.map(([key, value]) => ({
|
||||
label: key
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/^./, (s) => s.toUpperCase())
|
||||
.trim(),
|
||||
value: String(value ?? "-")
|
||||
}));
|
||||
}
|
||||
|
||||
export const AlertNotification = ({ eventType, orgId, data }: Props) => {
|
||||
const meta = getEventMeta(eventType);
|
||||
const dataItems = formatDataItems(data);
|
||||
|
||||
const allItems: { label: string; value: React.ReactNode }[] = [
|
||||
{ label: "Organization", value: orgId },
|
||||
{ label: "Status", value: (
|
||||
<span style={{ color: meta.statusColor, fontWeight: 600 }}>
|
||||
{meta.statusLabel}
|
||||
</span>
|
||||
)},
|
||||
{ label: "Time", value: new Date().toUTCString() },
|
||||
...dataItems
|
||||
];
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{meta.previewText}</Preview>
|
||||
<Tailwind config={themeColors}>
|
||||
<Body className="font-sans bg-gray-50">
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>{meta.heading}</EmailHeading>
|
||||
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>{meta.summary}</EmailText>
|
||||
|
||||
<EmailInfoSection
|
||||
title="Event Details"
|
||||
items={allItems}
|
||||
/>
|
||||
|
||||
<EmailText>
|
||||
Log in to your dashboard to view more details and
|
||||
manage your alert rules.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
<EmailSignature />
|
||||
</EmailFooter>
|
||||
</EmailContainer>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertNotification;
|
||||
@@ -32,7 +32,7 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||
const previewText = personalUseOnly
|
||||
? "Your Enterprise Edition key for personal use is ready"
|
||||
: "Thank you for your purchase — your Enterprise Edition key is ready";
|
||||
: "Thank you for your purchase - your Enterprise Edition key is ready";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||
|
||||
async function startServers() {
|
||||
@@ -39,6 +40,7 @@ async function startServers() {
|
||||
initTelemetryClient();
|
||||
|
||||
initLogCleanupInterval();
|
||||
initAcmeCertSync();
|
||||
|
||||
// Start all servers
|
||||
const apiServer = createApiServer();
|
||||
|
||||
3
server/lib/acmeCertSync.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function initAcmeCertSync(): void {
|
||||
// stub
|
||||
}
|
||||
19
server/lib/alerts/events/healthCheckEvents.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// stub
|
||||
|
||||
export async function fireHealthCheckHealthyAlert(
|
||||
orgId: string,
|
||||
healthCheckId: number,
|
||||
healthCheckName?: string,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fireHealthCheckNotHealthyAlert(
|
||||
orgId: string,
|
||||
healthCheckId: number,
|
||||
healthCheckName?: string,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
19
server/lib/alerts/events/siteEvents.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// stub
|
||||
|
||||
export async function fireSiteOnlineAlert(
|
||||
orgId: string,
|
||||
siteId: number,
|
||||
siteName?: string,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fireSiteOfflineAlert(
|
||||
orgId: string,
|
||||
siteId: number,
|
||||
siteName?: string,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
2
server/lib/alerts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./events/siteEvents";
|
||||
export * from "./events/healthCheckEvents";
|
||||
@@ -20,7 +20,10 @@ export enum TierFeature {
|
||||
FullRbac = "fullRbac",
|
||||
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
|
||||
SIEM = "siem", // handle downgrade by disabling SIEM integrations
|
||||
DomainNamespaces = "domainNamespaces" // handle downgrade by removing custom domain namespaces
|
||||
HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources
|
||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||
AlertingRules = "alertingRules"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
@@ -58,5 +61,8 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
||||
[TierFeature.SIEM]: ["enterprise"],
|
||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||
[TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"],
|
||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.StandaloneHealthChecks]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.AlertingRules]: ["tier2", "tier3", "enterprise"]
|
||||
};
|
||||
|
||||
@@ -121,8 +121,8 @@ export async function applyBlueprint({
|
||||
for (const result of clientResourcesResults) {
|
||||
if (
|
||||
result.oldSiteResource &&
|
||||
result.oldSiteResource.siteId !=
|
||||
result.newSiteResource.siteId
|
||||
JSON.stringify(result.newSites?.sort()) !==
|
||||
JSON.stringify(result.oldSites?.sort())
|
||||
) {
|
||||
// query existing associations
|
||||
const existingRoleIds = await trx
|
||||
@@ -222,38 +222,46 @@ export async function applyBlueprint({
|
||||
trx
|
||||
);
|
||||
} else {
|
||||
const [newSite] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, result.newSiteResource.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
let good = true;
|
||||
for (const newSite of result.newSites) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, newSite.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
.limit(1);
|
||||
|
||||
if (!site) {
|
||||
logger.debug(
|
||||
`No newt sites found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||
);
|
||||
good = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!newSite) {
|
||||
logger.debug(
|
||||
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.siteId}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}`
|
||||
);
|
||||
if (!good) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
{
|
||||
siteId: newSite.sites.siteId,
|
||||
orgId: newSite.sites.orgId
|
||||
},
|
||||
result.newSites.map((site) => ({
|
||||
siteId: site.siteId,
|
||||
orgId: result.newSiteResource.orgId
|
||||
})),
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,104 @@
|
||||
import {
|
||||
clients,
|
||||
clientSiteResources,
|
||||
domains,
|
||||
orgDomains,
|
||||
roles,
|
||||
roleSiteResources,
|
||||
Site,
|
||||
SiteResource,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
Transaction,
|
||||
userOrgs,
|
||||
users,
|
||||
userSiteResources
|
||||
userSiteResources,
|
||||
networks
|
||||
} from "@server/db";
|
||||
import { sites } from "@server/db";
|
||||
import { eq, and, ne, inArray, or } from "drizzle-orm";
|
||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
||||
import { Config } from "./types";
|
||||
import logger from "@server/logger";
|
||||
import { getNextAvailableAliasAddress } from "../ip";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
|
||||
async function getDomainForSiteResource(
|
||||
siteResourceId: number | undefined,
|
||||
fullDomain: string,
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
): Promise<{ subdomain: string | null; domainId: string }> {
|
||||
const [fullDomainExists] = await trx
|
||||
.select({ siteResourceId: siteResources.siteResourceId })
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResources.fullDomain, fullDomain),
|
||||
eq(siteResources.orgId, orgId),
|
||||
siteResourceId
|
||||
? ne(siteResources.siteResourceId, siteResourceId)
|
||||
: isNotNull(siteResources.siteResourceId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (fullDomainExists) {
|
||||
throw new Error(
|
||||
`Site resource already exists with domain: ${fullDomain} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
const possibleDomains = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.innerJoin(orgDomains, eq(domains.domainId, orgDomains.domainId))
|
||||
.where(and(eq(orgDomains.orgId, orgId), eq(domains.verified, true)))
|
||||
.execute();
|
||||
|
||||
if (possibleDomains.length === 0) {
|
||||
throw new Error(
|
||||
`Domain not found for full-domain: ${fullDomain} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
const validDomains = possibleDomains.filter((domain) => {
|
||||
if (domain.domains.type == "ns" || domain.domains.type == "wildcard") {
|
||||
return (
|
||||
fullDomain === domain.domains.baseDomain ||
|
||||
fullDomain.endsWith(`.${domain.domains.baseDomain}`)
|
||||
);
|
||||
} else if (domain.domains.type == "cname") {
|
||||
return fullDomain === domain.domains.baseDomain;
|
||||
}
|
||||
});
|
||||
|
||||
if (validDomains.length === 0) {
|
||||
throw new Error(
|
||||
`Domain not found for full-domain: ${fullDomain} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
const domainSelection = validDomains[0].domains;
|
||||
const baseDomain = domainSelection.baseDomain;
|
||||
|
||||
let subdomain: string | null = null;
|
||||
if (fullDomain !== baseDomain) {
|
||||
subdomain = fullDomain.replace(`.${baseDomain}`, "");
|
||||
}
|
||||
|
||||
await createCertificate(domainSelection.domainId, fullDomain, trx);
|
||||
|
||||
return {
|
||||
subdomain,
|
||||
domainId: domainSelection.domainId
|
||||
};
|
||||
}
|
||||
|
||||
export type ClientResourcesResults = {
|
||||
newSiteResource: SiteResource;
|
||||
oldSiteResource?: SiteResource;
|
||||
newSites: { siteId: number }[];
|
||||
oldSites: { siteId: number }[];
|
||||
}[];
|
||||
|
||||
export async function updateClientResources(
|
||||
@@ -43,53 +123,104 @@ export async function updateClientResources(
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const resourceSiteId = resourceData.site;
|
||||
let site;
|
||||
const existingSiteIds = existingResource?.networkId
|
||||
? await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(siteNetworks)
|
||||
.where(eq(siteNetworks.networkId, existingResource.networkId))
|
||||
: [];
|
||||
|
||||
if (resourceSiteId) {
|
||||
// Look up site by niceId
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, resourceSiteId),
|
||||
eq(sites.orgId, orgId)
|
||||
let allSites: { siteId: number }[] = [];
|
||||
if (resourceData.site) {
|
||||
let siteSingle;
|
||||
const resourceSiteId = resourceData.site;
|
||||
|
||||
if (resourceSiteId) {
|
||||
// Look up site by niceId
|
||||
[siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, resourceSiteId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
|
||||
.limit(1);
|
||||
} else {
|
||||
throw new Error(`Target site is required`);
|
||||
.limit(1);
|
||||
} else if (siteId) {
|
||||
// Use the provided siteId directly, but verify it belongs to the org
|
||||
[siteSingle] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
)
|
||||
.limit(1);
|
||||
} else {
|
||||
throw new Error(`Target site is required`);
|
||||
}
|
||||
|
||||
if (!siteSingle) {
|
||||
throw new Error(
|
||||
`Site not found: ${resourceSiteId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
allSites.push(siteSingle);
|
||||
}
|
||||
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Site not found: ${resourceSiteId} in org ${orgId}`
|
||||
);
|
||||
if (resourceData.sites) {
|
||||
for (const siteNiceId of resourceData.sites) {
|
||||
const [site] = await trx
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
eq(sites.niceId, siteNiceId),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!site) {
|
||||
throw new Error(
|
||||
`Site not found: ${siteId} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
allSites.push(site);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingResource) {
|
||||
let domainInfo:
|
||||
| { subdomain: string | null; domainId: string }
|
||||
| undefined;
|
||||
if (resourceData["full-domain"] && resourceData.mode === "http") {
|
||||
domainInfo = await getDomainForSiteResource(
|
||||
existingResource.siteResourceId,
|
||||
resourceData["full-domain"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Update existing resource
|
||||
const [updatedResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name: resourceData.name || resourceNiceId,
|
||||
siteId: site.siteId,
|
||||
mode: resourceData.mode,
|
||||
ssl: resourceData.ssl,
|
||||
scheme: resourceData.scheme,
|
||||
destination: resourceData.destination,
|
||||
destinationPort: resourceData["destination-port"],
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null,
|
||||
disableIcmp: resourceData["disable-icmp"],
|
||||
tcpPortRangeString: resourceData["tcp-ports"],
|
||||
udpPortRangeString: resourceData["udp-ports"]
|
||||
udpPortRangeString: resourceData["udp-ports"],
|
||||
fullDomain: resourceData["full-domain"] || null,
|
||||
subdomain: domainInfo ? domainInfo.subdomain : null,
|
||||
domainId: domainInfo ? domainInfo.domainId : null
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
@@ -100,7 +231,21 @@ export async function updateClientResources(
|
||||
.returning();
|
||||
|
||||
const siteResourceId = existingResource.siteResourceId;
|
||||
const orgId = existingResource.orgId;
|
||||
|
||||
if (updatedResource.networkId) {
|
||||
await trx
|
||||
.delete(siteNetworks)
|
||||
.where(
|
||||
eq(siteNetworks.networkId, updatedResource.networkId)
|
||||
);
|
||||
|
||||
for (const site of allSites) {
|
||||
await trx.insert(siteNetworks).values({
|
||||
siteId: site.siteId,
|
||||
networkId: updatedResource.networkId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await trx
|
||||
.delete(clientSiteResources)
|
||||
@@ -204,37 +349,72 @@ export async function updateClientResources(
|
||||
|
||||
results.push({
|
||||
newSiteResource: updatedResource,
|
||||
oldSiteResource: existingResource
|
||||
oldSiteResource: existingResource,
|
||||
newSites: allSites,
|
||||
oldSites: existingSiteIds
|
||||
});
|
||||
} else {
|
||||
let aliasAddress: string | null = null;
|
||||
if (resourceData.mode == "host") {
|
||||
// we can only have an alias on a host
|
||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||
}
|
||||
|
||||
let domainInfo:
|
||||
| { subdomain: string | null; domainId: string }
|
||||
| undefined;
|
||||
if (resourceData["full-domain"] && resourceData.mode === "http") {
|
||||
domainInfo = await getDomainForSiteResource(
|
||||
undefined,
|
||||
resourceData["full-domain"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
const [network] = await trx
|
||||
.insert(networks)
|
||||
.values({
|
||||
scope: "resource",
|
||||
orgId: orgId
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Create new resource
|
||||
const [newResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
orgId: orgId,
|
||||
siteId: site.siteId,
|
||||
niceId: resourceNiceId,
|
||||
networkId: network.networkId,
|
||||
defaultNetworkId: network.networkId,
|
||||
name: resourceData.name || resourceNiceId,
|
||||
mode: resourceData.mode,
|
||||
ssl: resourceData.ssl,
|
||||
scheme: resourceData.scheme,
|
||||
destination: resourceData.destination,
|
||||
destinationPort: resourceData["destination-port"],
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null,
|
||||
aliasAddress: aliasAddress,
|
||||
disableIcmp: resourceData["disable-icmp"],
|
||||
tcpPortRangeString: resourceData["tcp-ports"],
|
||||
udpPortRangeString: resourceData["udp-ports"]
|
||||
udpPortRangeString: resourceData["udp-ports"],
|
||||
fullDomain: resourceData["full-domain"] || null,
|
||||
subdomain: domainInfo ? domainInfo.subdomain : null,
|
||||
domainId: domainInfo ? domainInfo.domainId : null
|
||||
})
|
||||
.returning();
|
||||
|
||||
const siteResourceId = newResource.siteResourceId;
|
||||
|
||||
for (const site of allSites) {
|
||||
await trx.insert(siteNetworks).values({
|
||||
siteId: site.siteId,
|
||||
networkId: network.networkId
|
||||
});
|
||||
}
|
||||
|
||||
const [adminRole] = await trx
|
||||
.select()
|
||||
.from(roles)
|
||||
@@ -324,7 +504,11 @@ export async function updateClientResources(
|
||||
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
|
||||
);
|
||||
|
||||
results.push({ newSiteResource: newResource });
|
||||
results.push({
|
||||
newSiteResource: newResource,
|
||||
newSites: allSites,
|
||||
oldSites: existingSiteIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,9 @@ export async function updateProxyResources(
|
||||
const [newHealthcheck] = await trx
|
||||
.insert(targetHealthCheck)
|
||||
.values({
|
||||
name: `${targetData.hostname}:${targetData.port}`,
|
||||
targetId: newTarget.targetId,
|
||||
orgId: orgId,
|
||||
hcEnabled: healthcheckData?.enabled || false,
|
||||
hcPath: healthcheckData?.path,
|
||||
hcScheme: healthcheckData?.scheme,
|
||||
@@ -158,7 +160,9 @@ export async function updateProxyResources(
|
||||
healthcheckData?.["follow-redirects"],
|
||||
hcMethod: healthcheckData?.method,
|
||||
hcStatus: healthcheckData?.status,
|
||||
hcHealth: "unknown"
|
||||
hcHealth: "unknown",
|
||||
hcHealthyThreshold: healthcheckData?.["healthy-threshold"],
|
||||
hcUnhealthyThreshold: healthcheckData?.["unhealthy-threshold"]
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -522,7 +526,9 @@ export async function updateProxyResources(
|
||||
healthcheckData?.followRedirects ||
|
||||
healthcheckData?.["follow-redirects"],
|
||||
hcMethod: healthcheckData?.method,
|
||||
hcStatus: healthcheckData?.status
|
||||
hcStatus: healthcheckData?.status,
|
||||
hcHealthyThreshold: healthcheckData?.["healthy-threshold"],
|
||||
hcUnhealthyThreshold: healthcheckData?.["unhealthy-threshold"]
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
@@ -1081,6 +1087,8 @@ function checkIfHealthcheckChanged(
|
||||
JSON.stringify(incoming.hcHeaders)
|
||||
)
|
||||
return true;
|
||||
if (existing.hcHealthyThreshold !== incoming.hcHealthyThreshold) return true;
|
||||
if (existing.hcUnhealthyThreshold !== incoming.hcUnhealthyThreshold) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1100,7 +1108,7 @@ function checkIfTargetChanged(
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getDomain(
|
||||
export async function getDomain(
|
||||
resourceId: number | undefined,
|
||||
fullDomain: string,
|
||||
orgId: string,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const TargetHealthCheckSchema = z.object({
|
||||
hostname: z.string(),
|
||||
port: z.int().min(1).max(65535),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
path: z.string().optional().default("/"),
|
||||
path: z.string().optional(),
|
||||
scheme: z.string().optional(),
|
||||
mode: z.string().default("http"),
|
||||
interval: z.int().default(30),
|
||||
@@ -26,8 +26,10 @@ export const TargetHealthCheckSchema = z.object({
|
||||
.default(null),
|
||||
"follow-redirects": z.boolean().default(true),
|
||||
followRedirects: z.boolean().optional(), // deprecated alias
|
||||
method: z.string().default("GET"),
|
||||
status: z.int().optional()
|
||||
method: z.string().optional(),
|
||||
status: z.int().optional(),
|
||||
"healthy-threshold": z.int().min(1).optional().default(1),
|
||||
"unhealthy-threshold": z.int().min(1).optional().default(1)
|
||||
});
|
||||
|
||||
// Schema for individual target within a resource
|
||||
@@ -164,6 +166,7 @@ export const ResourceSchema = z
|
||||
name: z.string().optional(),
|
||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
scheme: z.enum(["http", "https"]).optional(),
|
||||
"full-domain": z.string().optional(),
|
||||
"proxy-port": z.int().min(1).max(65535).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -325,16 +328,20 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
||||
export const ClientResourceSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
mode: z.enum(["host", "cidr"]),
|
||||
site: z.string(),
|
||||
mode: z.enum(["host", "cidr", "http"]),
|
||||
site: z.string(), // DEPRECATED IN FAVOR OF sites
|
||||
sites: z.array(z.string()).optional().default([]),
|
||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||
// proxyPort: z.int().positive().optional(),
|
||||
// destinationPort: z.int().positive().optional(),
|
||||
"destination-port": z.int().positive().optional(),
|
||||
destination: z.string().min(1),
|
||||
// enabled: z.boolean().default(true),
|
||||
"tcp-ports": portRangeStringSchema.optional().default("*"),
|
||||
"udp-ports": portRangeStringSchema.optional().default("*"),
|
||||
"disable-icmp": z.boolean().optional().default(false),
|
||||
"full-domain": z.string().optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
scheme: z.enum(["http", "https"]).optional().nullable(),
|
||||
alias: z
|
||||
.string()
|
||||
.regex(
|
||||
@@ -477,6 +484,39 @@ export const ConfigSchema = z
|
||||
});
|
||||
}
|
||||
|
||||
// Enforce the full-domain uniqueness across client-resources in the same stack
|
||||
const clientFullDomainMap = new Map<string, string[]>();
|
||||
|
||||
Object.entries(config["client-resources"]).forEach(
|
||||
([resourceKey, resource]) => {
|
||||
const fullDomain = resource["full-domain"];
|
||||
if (fullDomain) {
|
||||
if (!clientFullDomainMap.has(fullDomain)) {
|
||||
clientFullDomainMap.set(fullDomain, []);
|
||||
}
|
||||
clientFullDomainMap.get(fullDomain)!.push(resourceKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const clientFullDomainDuplicates = Array.from(
|
||||
clientFullDomainMap.entries()
|
||||
)
|
||||
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
|
||||
.map(
|
||||
([fullDomain, resourceKeys]) =>
|
||||
`'${fullDomain}' used by resources: ${resourceKeys.join(", ")}`
|
||||
)
|
||||
.join("; ");
|
||||
|
||||
if (clientFullDomainDuplicates.length !== 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["client-resources"],
|
||||
message: `Duplicate 'full-domain' values found: ${clientFullDomainDuplicates}`
|
||||
});
|
||||
}
|
||||
|
||||
// Enforce proxy-port uniqueness within proxy-resources per protocol
|
||||
const protocolPortMap = new Map<string, string[]>();
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
export function encryptData(data: string, key: Buffer): string {
|
||||
const algorithm = "aes-256-gcm";
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
|
||||
let encrypted = cipher.update(data, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Combine IV, auth tag, and encrypted data
|
||||
return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
|
||||
}
|
||||
|
||||
// Helper function to decrypt data (you'll need this to read certificates)
|
||||
export function decryptData(encryptedData: string, key: Buffer): string {
|
||||
const algorithm = "aes-256-gcm";
|
||||
const parts = encryptedData.split(":");
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted data format");
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const authTag = Buffer.from(parts[1], "hex");
|
||||
const encrypted = parts[2];
|
||||
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// openssl rand -hex 32 > config/encryption.key
|
||||
@@ -5,6 +5,7 @@ import config from "@server/lib/config";
|
||||
import z from "zod";
|
||||
import logger from "@server/logger";
|
||||
import semver from "semver";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
|
||||
interface IPRange {
|
||||
start: bigint;
|
||||
@@ -477,9 +478,9 @@ export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||
|
||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||
return allSiteResources
|
||||
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
||||
.filter((sr) => sr.aliasAddress && ((sr.alias && sr.mode == "host") || (sr.fullDomain && sr.mode == "http")))
|
||||
.map((sr) => ({
|
||||
alias: sr.alias,
|
||||
alias: sr.alias || sr.fullDomain,
|
||||
aliasAddress: sr.aliasAddress
|
||||
}));
|
||||
}
|
||||
@@ -582,16 +583,26 @@ export type SubnetProxyTargetV2 = {
|
||||
protocol: "tcp" | "udp";
|
||||
}[];
|
||||
resourceId?: number;
|
||||
protocol?: "http" | "https"; // if set, this target only applies to the specified protocol
|
||||
httpTargets?: HTTPTarget[];
|
||||
tlsCert?: string;
|
||||
tlsKey?: string;
|
||||
};
|
||||
|
||||
export function generateSubnetProxyTargetV2(
|
||||
export type HTTPTarget = {
|
||||
destAddr: string; // must be an IP or hostname
|
||||
destPort: number;
|
||||
scheme: "http" | "https";
|
||||
};
|
||||
|
||||
export async function generateSubnetProxyTargetV2(
|
||||
siteResource: SiteResource,
|
||||
clients: {
|
||||
clientId: number;
|
||||
pubKey: string | null;
|
||||
subnet: string | null;
|
||||
}[]
|
||||
): SubnetProxyTargetV2[] | undefined {
|
||||
): Promise<SubnetProxyTargetV2[] | undefined> {
|
||||
if (clients.length === 0) {
|
||||
logger.debug(
|
||||
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
||||
@@ -642,6 +653,67 @@ export function generateSubnetProxyTargetV2(
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId
|
||||
});
|
||||
} else if (siteResource.mode == "http") {
|
||||
let destination = siteResource.destination;
|
||||
// check if this is a valid ip
|
||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||
if (ipSchema.safeParse(destination).success) {
|
||||
destination = `${destination}/32`;
|
||||
}
|
||||
|
||||
if (
|
||||
!siteResource.aliasAddress ||
|
||||
!siteResource.destinationPort ||
|
||||
!siteResource.scheme ||
|
||||
!siteResource.fullDomain
|
||||
) {
|
||||
logger.debug(
|
||||
`Site resource ${siteResource.siteResourceId} is in HTTP mode but is missing alias or alias address or destinationPort or scheme, skipping alias target generation.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// also push a match for the alias address
|
||||
let tlsCert: string | undefined;
|
||||
let tlsKey: string | undefined;
|
||||
|
||||
if (siteResource.ssl && siteResource.fullDomain) {
|
||||
try {
|
||||
const certs = await getValidCertificatesForDomains(
|
||||
new Set([siteResource.fullDomain]),
|
||||
true
|
||||
);
|
||||
if (certs.length > 0 && certs[0].certFile && certs[0].keyFile) {
|
||||
tlsCert = certs[0].certFile;
|
||||
tlsKey = certs[0].keyFile;
|
||||
} else {
|
||||
logger.warn(
|
||||
`No valid certificate found for SSL site resource ${siteResource.siteResourceId} with domain ${siteResource.fullDomain}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Failed to retrieve certificate for site resource ${siteResource.siteResourceId} domain ${siteResource.fullDomain}: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
targets.push({
|
||||
sourcePrefixes: [],
|
||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||
rewriteTo: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId,
|
||||
protocol: siteResource.ssl ? "https" : "http",
|
||||
httpTargets: [
|
||||
{
|
||||
destAddr: siteResource.destination,
|
||||
destPort: siteResource.destinationPort,
|
||||
scheme: siteResource.scheme
|
||||
}
|
||||
],
|
||||
...(tlsCert && tlsKey ? { tlsCert, tlsKey } : {})
|
||||
});
|
||||
}
|
||||
|
||||
if (targets.length == 0) {
|
||||
|
||||
@@ -11,17 +11,16 @@ import {
|
||||
roleSiteResources,
|
||||
Site,
|
||||
SiteResource,
|
||||
siteNetworks,
|
||||
siteResources,
|
||||
sites,
|
||||
Transaction,
|
||||
userOrgRoles,
|
||||
userOrgs,
|
||||
userSiteResources
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||
|
||||
import {
|
||||
addPeer as newtAddPeer,
|
||||
deletePeer as newtDeletePeer
|
||||
} from "@server/routers/newt/peers";
|
||||
import {
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
generateRemoteSubnets,
|
||||
generateSubnetProxyTargetV2,
|
||||
parseEndpoint,
|
||||
formatEndpoint
|
||||
} from "@server/lib/ip";
|
||||
import {
|
||||
addPeerData,
|
||||
@@ -48,15 +46,27 @@ export async function getClientSiteResourceAccess(
|
||||
siteResource: SiteResource,
|
||||
trx: Transaction | typeof db = db
|
||||
) {
|
||||
// get the site
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteResource.siteId))
|
||||
.limit(1);
|
||||
// get all sites associated with this siteResource via its network
|
||||
const sitesList = siteResource.networkId
|
||||
? await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.siteId, sites.siteId)
|
||||
)
|
||||
.where(eq(siteNetworks.networkId, siteResource.networkId))
|
||||
.then((rows) => rows.map((row) => row.sites))
|
||||
: [];
|
||||
|
||||
if (!site) {
|
||||
throw new Error(`Site with ID ${siteResource.siteId} not found`);
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
|
||||
);
|
||||
|
||||
if (sitesList.length === 0) {
|
||||
logger.warn(
|
||||
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
|
||||
);
|
||||
}
|
||||
|
||||
const roleIds = await trx
|
||||
@@ -136,8 +146,12 @@ export async function getClientSiteResourceAccess(
|
||||
const mergedAllClients = Array.from(allClientsMap.values());
|
||||
const mergedAllClientIds = mergedAllClients.map((c) => c.clientId);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} mergedClientCount=${mergedAllClientIds.length} clientIds=[${mergedAllClientIds.join(", ")}] (userBased=${newAllClients.length} direct=${directClients.length})`
|
||||
);
|
||||
|
||||
return {
|
||||
site,
|
||||
sitesList,
|
||||
mergedAllClients,
|
||||
mergedAllClientIds
|
||||
};
|
||||
@@ -153,40 +167,59 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
subnet: string | null;
|
||||
}[];
|
||||
}> {
|
||||
const siteId = siteResource.siteId;
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] START siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} orgId=${siteResource.orgId}`
|
||||
);
|
||||
|
||||
const { site, mergedAllClients, mergedAllClientIds } =
|
||||
const { sitesList, mergedAllClients, mergedAllClientIds } =
|
||||
await getClientSiteResourceAccess(siteResource, trx);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] access resolved siteResourceId=${siteResource.siteResourceId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}] mergedClientCount=${mergedAllClients.length} clientIds=[${mergedAllClientIds.join(", ")}]`
|
||||
);
|
||||
|
||||
/////////// process the client-siteResource associations ///////////
|
||||
|
||||
// get all of the clients associated with other resources on this site
|
||||
const allUpdatedClientsFromOtherResourcesOnThisSite = await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResources.siteId, siteId),
|
||||
ne(siteResources.siteResourceId, siteResource.siteResourceId)
|
||||
)
|
||||
);
|
||||
// get all of the clients associated with other resources in the same network,
|
||||
// joined through siteNetworks so we know which siteId each client belongs to
|
||||
const allUpdatedClientsFromOtherResourcesOnThisSite = siteResource.networkId
|
||||
? await trx
|
||||
.select({
|
||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(clientSiteResourcesAssociationsCache)
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResources.networkId, siteResource.networkId),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
)
|
||||
: [];
|
||||
|
||||
const allClientIdsFromOtherResourcesOnThisSite = Array.from(
|
||||
new Set(
|
||||
allUpdatedClientsFromOtherResourcesOnThisSite.map(
|
||||
(row) => row.clientId
|
||||
)
|
||||
)
|
||||
);
|
||||
// Build a per-site map so the loop below can check by siteId rather than
|
||||
// across the entire network.
|
||||
const clientsFromOtherResourcesBySite = new Map<number, Set<number>>();
|
||||
for (const row of allUpdatedClientsFromOtherResourcesOnThisSite) {
|
||||
if (!clientsFromOtherResourcesBySite.has(row.siteId)) {
|
||||
clientsFromOtherResourcesBySite.set(row.siteId, new Set());
|
||||
}
|
||||
clientsFromOtherResourcesBySite.get(row.siteId)!.add(row.clientId);
|
||||
}
|
||||
|
||||
const existingClientSiteResources = await trx
|
||||
.select({
|
||||
@@ -204,6 +237,10 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
(row) => row.clientId
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} existingResourceClientIds=[${existingClientSiteResourceIds.join(", ")}]`
|
||||
);
|
||||
|
||||
// Get full client details for existing resource clients (needed for sending delete messages)
|
||||
const existingResourceClients =
|
||||
existingClientSiteResourceIds.length > 0
|
||||
@@ -223,6 +260,10 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
(clientId) => !existingClientSiteResourceIds.includes(clientId)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} resourceClients toAdd=[${clientSiteResourcesToAdd.join(", ")}]`
|
||||
);
|
||||
|
||||
const clientSiteResourcesToInsert = clientSiteResourcesToAdd.map(
|
||||
(clientId) => ({
|
||||
clientId,
|
||||
@@ -231,17 +272,34 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
);
|
||||
|
||||
if (clientSiteResourcesToInsert.length > 0) {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} inserting ${clientSiteResourcesToInsert.length} clientSiteResource association(s)`
|
||||
);
|
||||
await trx
|
||||
.insert(clientSiteResourcesAssociationsCache)
|
||||
.values(clientSiteResourcesToInsert)
|
||||
.returning();
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} inserted clientSiteResource associations`
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} no clientSiteResource associations to insert`
|
||||
);
|
||||
}
|
||||
|
||||
const clientSiteResourcesToRemove = existingClientSiteResourceIds.filter(
|
||||
(clientId) => !mergedAllClientIds.includes(clientId)
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} resourceClients toRemove=[${clientSiteResourcesToRemove.join(", ")}]`
|
||||
);
|
||||
|
||||
if (clientSiteResourcesToRemove.length > 0) {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} deleting ${clientSiteResourcesToRemove.length} clientSiteResource association(s)`
|
||||
);
|
||||
await trx
|
||||
.delete(clientSiteResourcesAssociationsCache)
|
||||
.where(
|
||||
@@ -260,82 +318,127 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
|
||||
/////////// process the client-site associations ///////////
|
||||
|
||||
const existingClientSites = await trx
|
||||
.select({
|
||||
clientId: clientSitesAssociationsCache.clientId
|
||||
})
|
||||
.from(clientSitesAssociationsCache)
|
||||
.where(eq(clientSitesAssociationsCache.siteId, siteResource.siteId));
|
||||
|
||||
const existingClientSiteIds = existingClientSites.map(
|
||||
(row) => row.clientId
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteResourceId=${siteResource.siteResourceId} beginning client-site association loop over ${sitesList.length} site(s)`
|
||||
);
|
||||
|
||||
// Get full client details for existing clients (needed for sending delete messages)
|
||||
const existingClients = await trx
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(inArray(clients.clientId, existingClientSiteIds));
|
||||
for (const site of sitesList) {
|
||||
const siteId = site.siteId;
|
||||
|
||||
const clientSitesToAdd = mergedAllClientIds.filter(
|
||||
(clientId) =>
|
||||
!existingClientSiteIds.includes(clientId) &&
|
||||
!allClientIdsFromOtherResourcesOnThisSite.includes(clientId) // dont remove if there is still another connection for another site resource
|
||||
);
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] processing siteId=${siteId} for siteResourceId=${siteResource.siteResourceId}`
|
||||
);
|
||||
|
||||
const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
|
||||
clientId,
|
||||
siteId
|
||||
}));
|
||||
const existingClientSites = await trx
|
||||
.select({
|
||||
clientId: clientSitesAssociationsCache.clientId
|
||||
})
|
||||
.from(clientSitesAssociationsCache)
|
||||
.where(eq(clientSitesAssociationsCache.siteId, siteId));
|
||||
|
||||
if (clientSitesToInsert.length > 0) {
|
||||
await trx
|
||||
.insert(clientSitesAssociationsCache)
|
||||
.values(clientSitesToInsert)
|
||||
.returning();
|
||||
}
|
||||
const existingClientSiteIds = existingClientSites.map(
|
||||
(row) => row.clientId
|
||||
);
|
||||
|
||||
// Now remove any client-site associations that should no longer exist
|
||||
const clientSitesToRemove = existingClientSiteIds.filter(
|
||||
(clientId) =>
|
||||
!mergedAllClientIds.includes(clientId) &&
|
||||
!allClientIdsFromOtherResourcesOnThisSite.includes(clientId) // dont remove if there is still another connection for another site resource
|
||||
);
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} existingClientSiteIds=[${existingClientSiteIds.join(", ")}]`
|
||||
);
|
||||
|
||||
if (clientSitesToRemove.length > 0) {
|
||||
await trx
|
||||
.delete(clientSitesAssociationsCache)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSitesAssociationsCache.siteId, siteId),
|
||||
inArray(
|
||||
clientSitesAssociationsCache.clientId,
|
||||
clientSitesToRemove
|
||||
)
|
||||
)
|
||||
// Get full client details for existing clients (needed for sending delete messages)
|
||||
const existingClients =
|
||||
existingClientSiteIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(inArray(clients.clientId, existingClientSiteIds))
|
||||
: [];
|
||||
|
||||
const otherResourceClientIds = clientsFromOtherResourcesBySite.get(siteId) ?? new Set<number>();
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} otherResourceClientIds=[${[...otherResourceClientIds].join(", ")}] mergedAllClientIds=[${mergedAllClientIds.join(", ")}]`
|
||||
);
|
||||
|
||||
const clientSitesToAdd = mergedAllClientIds.filter(
|
||||
(clientId) =>
|
||||
!existingClientSiteIds.includes(clientId) &&
|
||||
!otherResourceClientIds.has(clientId) // dont add if already connected via another site resource
|
||||
);
|
||||
|
||||
const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
|
||||
clientId,
|
||||
siteId
|
||||
}));
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toAdd=[${clientSitesToAdd.join(", ")}]`
|
||||
);
|
||||
|
||||
if (clientSitesToInsert.length > 0) {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserting ${clientSitesToInsert.length} clientSite association(s)`
|
||||
);
|
||||
await trx
|
||||
.insert(clientSitesAssociationsCache)
|
||||
.values(clientSitesToInsert)
|
||||
.returning();
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} inserted clientSite associations`
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} no clientSite associations to insert`
|
||||
);
|
||||
}
|
||||
|
||||
// Now remove any client-site associations that should no longer exist
|
||||
const clientSitesToRemove = existingClientSiteIds.filter(
|
||||
(clientId) =>
|
||||
!mergedAllClientIds.includes(clientId) &&
|
||||
!otherResourceClientIds.has(clientId) // dont remove if there is still another connection for another site resource
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} clientSites toRemove=[${clientSitesToRemove.join(", ")}]`
|
||||
);
|
||||
|
||||
if (clientSitesToRemove.length > 0) {
|
||||
logger.debug(
|
||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] siteId=${siteId} deleting ${clientSitesToRemove.length} clientSite association(s)`
|
||||
);
|
||||
await trx
|
||||
.delete(clientSitesAssociationsCache)
|
||||
.where(
|
||||
and(
|
||||
eq(clientSitesAssociationsCache.siteId, siteId),
|
||||
inArray(
|
||||
clientSitesAssociationsCache.clientId,
|
||||
clientSitesToRemove
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Now handle the messages to add/remove peers on both the newt and olm sides
|
||||
await handleMessagesForSiteClients(
|
||||
site,
|
||||
siteId,
|
||||
mergedAllClients,
|
||||
existingClients,
|
||||
clientSitesToAdd,
|
||||
clientSitesToRemove,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
/////////// send the messages ///////////
|
||||
|
||||
// Now handle the messages to add/remove peers on both the newt and olm sides
|
||||
await handleMessagesForSiteClients(
|
||||
site,
|
||||
siteId,
|
||||
mergedAllClients,
|
||||
existingClients,
|
||||
clientSitesToAdd,
|
||||
clientSitesToRemove,
|
||||
trx
|
||||
);
|
||||
|
||||
// Handle subnet proxy target updates for the resource associations
|
||||
await handleSubnetProxyTargetUpdates(
|
||||
siteResource,
|
||||
sitesList,
|
||||
mergedAllClients,
|
||||
existingResourceClients,
|
||||
clientSiteResourcesToAdd,
|
||||
@@ -624,6 +727,7 @@ export async function updateClientSiteDestinations(
|
||||
|
||||
async function handleSubnetProxyTargetUpdates(
|
||||
siteResource: SiteResource,
|
||||
sitesList: Site[],
|
||||
allClients: {
|
||||
clientId: number;
|
||||
pubKey: string | null;
|
||||
@@ -638,125 +742,138 @@ async function handleSubnetProxyTargetUpdates(
|
||||
clientSiteResourcesToRemove: number[],
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<void> {
|
||||
// Get the newt for this site
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteResource.siteId))
|
||||
.limit(1);
|
||||
const proxyJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
|
||||
if (!newt) {
|
||||
logger.warn(
|
||||
`Newt not found for site ${siteResource.siteId}, skipping subnet proxy target updates`
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const siteData of sitesList) {
|
||||
const siteId = siteData.siteId;
|
||||
|
||||
const proxyJobs = [];
|
||||
const olmJobs = [];
|
||||
// Generate targets for added associations
|
||||
if (clientSiteResourcesToAdd.length > 0) {
|
||||
const addedClients = allClients.filter((client) =>
|
||||
clientSiteResourcesToAdd.includes(client.clientId)
|
||||
);
|
||||
// Get the newt for this site
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (addedClients.length > 0) {
|
||||
const targetsToAdd = generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
addedClients
|
||||
if (!newt) {
|
||||
logger.warn(
|
||||
`Newt not found for site ${siteId}, skipping subnet proxy target updates`
|
||||
);
|
||||
|
||||
if (targetsToAdd) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToAdd,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const client of addedClients) {
|
||||
olmJobs.push(
|
||||
addPeerData(
|
||||
client.clientId,
|
||||
siteResource.siteId,
|
||||
generateRemoteSubnets([siteResource]),
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// here we use the existingSiteResource from BEFORE we updated the destination so we dont need to worry about updating destinations here
|
||||
|
||||
// Generate targets for removed associations
|
||||
if (clientSiteResourcesToRemove.length > 0) {
|
||||
const removedClients = existingClients.filter((client) =>
|
||||
clientSiteResourcesToRemove.includes(client.clientId)
|
||||
);
|
||||
|
||||
if (removedClients.length > 0) {
|
||||
const targetsToRemove = generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
removedClients
|
||||
// Generate targets for added associations
|
||||
if (clientSiteResourcesToAdd.length > 0) {
|
||||
const addedClients = allClients.filter((client) =>
|
||||
clientSiteResourcesToAdd.includes(client.clientId)
|
||||
);
|
||||
|
||||
if (targetsToRemove) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToRemove,
|
||||
newt.version
|
||||
)
|
||||
if (addedClients.length > 0) {
|
||||
const targetsToAdd = await generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
addedClients
|
||||
);
|
||||
}
|
||||
|
||||
for (const client of removedClients) {
|
||||
// Check if this client still has access to another resource on this site with the same destination
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteResources.siteId, siteResource.siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
siteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
if (targetsToAdd) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToAdd,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Only remove remote subnet if no other resource uses the same destination
|
||||
const remoteSubnetsToRemove =
|
||||
destinationStillInUse.length > 0
|
||||
? []
|
||||
: generateRemoteSubnets([siteResource]);
|
||||
for (const client of addedClients) {
|
||||
olmJobs.push(
|
||||
addPeerData(
|
||||
client.clientId,
|
||||
siteId,
|
||||
generateRemoteSubnets([siteResource]),
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
siteResource.siteId,
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
// here we use the existingSiteResource from BEFORE we updated the destination so we dont need to worry about updating destinations here
|
||||
|
||||
// Generate targets for removed associations
|
||||
if (clientSiteResourcesToRemove.length > 0) {
|
||||
const removedClients = existingClients.filter((client) =>
|
||||
clientSiteResourcesToRemove.includes(client.clientId)
|
||||
);
|
||||
|
||||
if (removedClients.length > 0) {
|
||||
const targetsToRemove = await generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
removedClients
|
||||
);
|
||||
|
||||
if (targetsToRemove) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
targetsToRemove,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const client of removedClients) {
|
||||
// Check if this client still has access to another resource
|
||||
// on this specific site with the same destination. We scope
|
||||
// by siteId (via siteNetworks) rather than networkId because
|
||||
// removePeerData operates per-site - a resource on a different
|
||||
// site sharing the same network should not block removal here.
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
siteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Only remove remote subnet if no other resource uses the same destination
|
||||
const remoteSubnetsToRemove =
|
||||
destinationStillInUse.length > 0
|
||||
? []
|
||||
: generateRemoteSubnets([siteResource]);
|
||||
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
siteId,
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -863,10 +980,25 @@ export async function rebuildClientAssociationsFromClient(
|
||||
)
|
||||
: [];
|
||||
|
||||
// Group by siteId for site-level associations
|
||||
const newSiteIds = Array.from(
|
||||
new Set(newSiteResources.map((sr) => sr.siteId))
|
||||
// Group by siteId for site-level associations - look up via siteNetworks since
|
||||
// siteResources no longer carries a direct siteId column.
|
||||
const networkIds = Array.from(
|
||||
new Set(
|
||||
newSiteResources
|
||||
.map((sr) => sr.networkId)
|
||||
.filter((id): id is number => id !== null)
|
||||
)
|
||||
);
|
||||
const newSiteIds =
|
||||
networkIds.length > 0
|
||||
? await trx
|
||||
.select({ siteId: siteNetworks.siteId })
|
||||
.from(siteNetworks)
|
||||
.where(inArray(siteNetworks.networkId, networkIds))
|
||||
.then((rows) =>
|
||||
Array.from(new Set(rows.map((r) => r.siteId)))
|
||||
)
|
||||
: [];
|
||||
|
||||
/////////// Process client-siteResource associations ///////////
|
||||
|
||||
@@ -1139,13 +1271,45 @@ async function handleMessagesForClientResources(
|
||||
resourcesToAdd.includes(r.siteResourceId)
|
||||
);
|
||||
|
||||
// Build (resource, siteId) pairs by looking up siteNetworks for each resource's networkId
|
||||
const addedNetworkIds = Array.from(
|
||||
new Set(
|
||||
addedResources
|
||||
.map((r) => r.networkId)
|
||||
.filter((id): id is number => id !== null)
|
||||
)
|
||||
);
|
||||
const addedSiteNetworkRows =
|
||||
addedNetworkIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
networkId: siteNetworks.networkId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(siteNetworks)
|
||||
.where(inArray(siteNetworks.networkId, addedNetworkIds))
|
||||
: [];
|
||||
const addedNetworkToSites = new Map<number, number[]>();
|
||||
for (const row of addedSiteNetworkRows) {
|
||||
if (!addedNetworkToSites.has(row.networkId)) {
|
||||
addedNetworkToSites.set(row.networkId, []);
|
||||
}
|
||||
addedNetworkToSites.get(row.networkId)!.push(row.siteId);
|
||||
}
|
||||
|
||||
// Group by site for proxy updates
|
||||
const addedBySite = new Map<number, SiteResource[]>();
|
||||
for (const resource of addedResources) {
|
||||
if (!addedBySite.has(resource.siteId)) {
|
||||
addedBySite.set(resource.siteId, []);
|
||||
const siteIds =
|
||||
resource.networkId != null
|
||||
? (addedNetworkToSites.get(resource.networkId) ?? [])
|
||||
: [];
|
||||
for (const siteId of siteIds) {
|
||||
if (!addedBySite.has(siteId)) {
|
||||
addedBySite.set(siteId, []);
|
||||
}
|
||||
addedBySite.get(siteId)!.push(resource);
|
||||
}
|
||||
addedBySite.get(resource.siteId)!.push(resource);
|
||||
}
|
||||
|
||||
// Add subnet proxy targets for each site
|
||||
@@ -1164,7 +1328,7 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
for (const resource of resources) {
|
||||
const targets = generateSubnetProxyTargetV2(resource, [
|
||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
clientId: client.clientId,
|
||||
pubKey: client.pubKey,
|
||||
@@ -1187,7 +1351,7 @@ async function handleMessagesForClientResources(
|
||||
olmJobs.push(
|
||||
addPeerData(
|
||||
client.clientId,
|
||||
resource.siteId,
|
||||
siteId,
|
||||
generateRemoteSubnets([resource]),
|
||||
generateAliasConfig([resource])
|
||||
)
|
||||
@@ -1199,7 +1363,7 @@ async function handleMessagesForClientResources(
|
||||
error.message.includes("not found")
|
||||
) {
|
||||
logger.debug(
|
||||
`Olm data not found for client ${client.clientId} and site ${resource.siteId}, skipping removal`
|
||||
`Olm data not found for client ${client.clientId} and site ${siteId}, skipping addition`
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
@@ -1216,13 +1380,45 @@ async function handleMessagesForClientResources(
|
||||
.from(siteResources)
|
||||
.where(inArray(siteResources.siteResourceId, resourcesToRemove));
|
||||
|
||||
// Build (resource, siteId) pairs via siteNetworks
|
||||
const removedNetworkIds = Array.from(
|
||||
new Set(
|
||||
removedResources
|
||||
.map((r) => r.networkId)
|
||||
.filter((id): id is number => id !== null)
|
||||
)
|
||||
);
|
||||
const removedSiteNetworkRows =
|
||||
removedNetworkIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
networkId: siteNetworks.networkId,
|
||||
siteId: siteNetworks.siteId
|
||||
})
|
||||
.from(siteNetworks)
|
||||
.where(inArray(siteNetworks.networkId, removedNetworkIds))
|
||||
: [];
|
||||
const removedNetworkToSites = new Map<number, number[]>();
|
||||
for (const row of removedSiteNetworkRows) {
|
||||
if (!removedNetworkToSites.has(row.networkId)) {
|
||||
removedNetworkToSites.set(row.networkId, []);
|
||||
}
|
||||
removedNetworkToSites.get(row.networkId)!.push(row.siteId);
|
||||
}
|
||||
|
||||
// Group by site for proxy updates
|
||||
const removedBySite = new Map<number, SiteResource[]>();
|
||||
for (const resource of removedResources) {
|
||||
if (!removedBySite.has(resource.siteId)) {
|
||||
removedBySite.set(resource.siteId, []);
|
||||
const siteIds =
|
||||
resource.networkId != null
|
||||
? (removedNetworkToSites.get(resource.networkId) ?? [])
|
||||
: [];
|
||||
for (const siteId of siteIds) {
|
||||
if (!removedBySite.has(siteId)) {
|
||||
removedBySite.set(siteId, []);
|
||||
}
|
||||
removedBySite.get(siteId)!.push(resource);
|
||||
}
|
||||
removedBySite.get(resource.siteId)!.push(resource);
|
||||
}
|
||||
|
||||
// Remove subnet proxy targets for each site
|
||||
@@ -1241,7 +1437,7 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
for (const resource of resources) {
|
||||
const targets = generateSubnetProxyTargetV2(resource, [
|
||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
clientId: client.clientId,
|
||||
pubKey: client.pubKey,
|
||||
@@ -1260,7 +1456,11 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this client still has access to another resource on this site with the same destination
|
||||
// Check if this client still has access to another resource
|
||||
// on this specific site with the same destination. We scope
|
||||
// by siteId (via siteNetworks) rather than networkId because
|
||||
// removePeerData operates per-site - a resource on a different
|
||||
// site sharing the same network should not block removal here.
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
@@ -1271,13 +1471,17 @@ async function handleMessagesForClientResources(
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
siteNetworks,
|
||||
eq(siteNetworks.networkId, siteResources.networkId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteResources.siteId, resource.siteId),
|
||||
eq(siteNetworks.siteId, siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
resource.destination
|
||||
@@ -1299,7 +1503,7 @@ async function handleMessagesForClientResources(
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
resource.siteId,
|
||||
siteId,
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([resource])
|
||||
)
|
||||
@@ -1311,7 +1515,7 @@ async function handleMessagesForClientResources(
|
||||
error.message.includes("not found")
|
||||
) {
|
||||
logger.debug(
|
||||
`Olm data not found for client ${client.clientId} and site ${resource.siteId}, skipping removal`
|
||||
`Olm data not found for client ${client.clientId} and site ${siteId}, skipping removal`
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
|
||||
133
server/lib/statusHistory.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const statusHistoryQuerySchema = z
|
||||
.object({
|
||||
days: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? parseInt(v, 10) : 90)),
|
||||
})
|
||||
.pipe(
|
||||
z.object({
|
||||
days: z.number().int().min(1).max(365),
|
||||
})
|
||||
);
|
||||
|
||||
export interface StatusHistoryDayBucket {
|
||||
date: string; // ISO date "YYYY-MM-DD"
|
||||
uptimePercent: number; // 0-100
|
||||
totalDowntimeSeconds: number;
|
||||
downtimeWindows: { start: number; end: number | null; status: string }[];
|
||||
status: "good" | "degraded" | "bad" | "no_data";
|
||||
}
|
||||
|
||||
export interface StatusHistoryResponse {
|
||||
entityType: string;
|
||||
entityId: number;
|
||||
days: StatusHistoryDayBucket[];
|
||||
overallUptimePercent: number;
|
||||
totalDowntimeSeconds: number;
|
||||
}
|
||||
|
||||
export function computeBuckets(
|
||||
events: { entityType: string; entityId: number; orgId: string; status: string; timestamp: number; id: number }[],
|
||||
days: number
|
||||
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const buckets: StatusHistoryDayBucket[] = [];
|
||||
let totalDowntime = 0;
|
||||
|
||||
for (let d = 0; d < days; d++) {
|
||||
const dayStartSec = nowSec - (days - d) * 86400;
|
||||
const dayEndSec = dayStartSec + 86400;
|
||||
|
||||
const dayEvents = events.filter(
|
||||
(e) => e.timestamp >= dayStartSec && e.timestamp < dayEndSec
|
||||
);
|
||||
|
||||
// Determine the status at the start of this day (last event before dayStart)
|
||||
const lastBeforeDay = [...events]
|
||||
.filter((e) => e.timestamp < dayStartSec)
|
||||
.at(-1);
|
||||
|
||||
const currentStatus = lastBeforeDay?.status ?? null;
|
||||
|
||||
const windows: { start: number; end: number | null; status: string }[] = [];
|
||||
let dayDowntime = 0;
|
||||
|
||||
let windowStart = dayStartSec;
|
||||
let windowStatus = currentStatus;
|
||||
|
||||
for (const evt of dayEvents) {
|
||||
if (windowStatus !== null && windowStatus !== evt.status) {
|
||||
const windowEnd = evt.timestamp;
|
||||
const isDown =
|
||||
windowStatus === "offline" ||
|
||||
windowStatus === "unhealthy" ||
|
||||
windowStatus === "unknown";
|
||||
if (isDown) {
|
||||
dayDowntime += windowEnd - windowStart;
|
||||
windows.push({
|
||||
start: windowStart,
|
||||
end: windowEnd,
|
||||
status: windowStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
windowStart = evt.timestamp;
|
||||
windowStatus = evt.status;
|
||||
}
|
||||
|
||||
// Close the final window at the end of the day (or now if day hasn't ended)
|
||||
if (windowStatus !== null) {
|
||||
const finalEnd = Math.min(dayEndSec, nowSec);
|
||||
const isDown =
|
||||
windowStatus === "offline" ||
|
||||
windowStatus === "unhealthy" ||
|
||||
windowStatus === "unknown";
|
||||
if (isDown && finalEnd > windowStart) {
|
||||
dayDowntime += finalEnd - windowStart;
|
||||
windows.push({
|
||||
start: windowStart,
|
||||
end: finalEnd,
|
||||
status: windowStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
totalDowntime += dayDowntime;
|
||||
|
||||
const effectiveDayLength = Math.max(
|
||||
0,
|
||||
Math.min(dayEndSec, nowSec) - dayStartSec
|
||||
);
|
||||
const uptimePct =
|
||||
effectiveDayLength > 0
|
||||
? Math.max(
|
||||
0,
|
||||
((effectiveDayLength - dayDowntime) /
|
||||
effectiveDayLength) *
|
||||
100
|
||||
)
|
||||
: 100;
|
||||
|
||||
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
|
||||
|
||||
let status: StatusHistoryDayBucket["status"] = "no_data";
|
||||
if (currentStatus !== null || dayEvents.length > 0) {
|
||||
if (uptimePct >= 99) status = "good";
|
||||
else if (uptimePct >= 50) status = "degraded";
|
||||
else status = "bad";
|
||||
}
|
||||
|
||||
buckets.push({
|
||||
date: dateStr,
|
||||
uptimePercent: Math.round(uptimePct * 100) / 100,
|
||||
totalDowntimeSeconds: dayDowntime,
|
||||
downtimeWindows: windows,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
return { buckets, totalDowntime };
|
||||
}
|
||||
@@ -1011,7 +1011,7 @@ export class TraefikConfigManager {
|
||||
);
|
||||
|
||||
if (!isUnused) {
|
||||
// Domain is still active — remove from pending deletion if it was queued
|
||||
// Domain is still active - remove from pending deletion if it was queued
|
||||
if (this.pendingDeletion.has(dirName)) {
|
||||
logger.info(
|
||||
`Certificate ${dirName} is active again, cancelling pending deletion`
|
||||
@@ -1021,7 +1021,7 @@ export class TraefikConfigManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Domain is unused — add to pending deletion or decrement its counter
|
||||
// Domain is unused - add to pending deletion or decrement its counter
|
||||
if (!this.pendingDeletion.has(dirName)) {
|
||||
const graceCycles = 3;
|
||||
logger.info(
|
||||
@@ -1036,7 +1036,7 @@ export class TraefikConfigManager {
|
||||
);
|
||||
this.pendingDeletion.set(dirName, remaining);
|
||||
} else {
|
||||
// Grace period expired — actually delete now
|
||||
// Grace period expired - actually delete now
|
||||
this.pendingDeletion.delete(dirName);
|
||||
|
||||
const domainDir = path.join(certsPath, dirName);
|
||||
|
||||
@@ -24,7 +24,7 @@ function encodePath(path: string | null | undefined): string {
|
||||
|
||||
/**
|
||||
* Exact replica of the OLD key computation from upstream main.
|
||||
* Uses sanitize() for paths — this is what had the collision bug.
|
||||
* Uses sanitize() for paths - this is what had the collision bug.
|
||||
*/
|
||||
function oldKeyComputation(
|
||||
resourceId: number,
|
||||
@@ -44,7 +44,7 @@ function oldKeyComputation(
|
||||
|
||||
/**
|
||||
* Replica of the NEW key computation from our fix.
|
||||
* Uses encodePath() for paths — collision-free.
|
||||
* Uses encodePath() for paths - collision-free.
|
||||
*/
|
||||
function newKeyComputation(
|
||||
resourceId: number,
|
||||
@@ -195,11 +195,11 @@ function runTests() {
|
||||
true,
|
||||
"/a/b and /a-b MUST have different keys"
|
||||
);
|
||||
console.log(" PASS: collision fix — /a/b vs /a-b have different keys");
|
||||
console.log(" PASS: collision fix - /a/b vs /a-b have different keys");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: demonstrate the old bug — old code maps /a/b and /a-b to same key
|
||||
// Test 9: demonstrate the old bug - old code maps /a/b and /a-b to same key
|
||||
{
|
||||
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
||||
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
||||
@@ -208,11 +208,11 @@ function runTests() {
|
||||
oldKeyDash,
|
||||
"old code MUST have this collision (confirms the bug exists)"
|
||||
);
|
||||
console.log(" PASS: confirmed old code bug — /a/b and /a-b collided");
|
||||
console.log(" PASS: confirmed old code bug - /a/b and /a-b collided");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: /api/v1 and /api-v1 — old code collision, new code fixes it
|
||||
// Test 10: /api/v1 and /api-v1 - old code collision, new code fixes it
|
||||
{
|
||||
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||
@@ -229,11 +229,11 @@ function runTests() {
|
||||
true,
|
||||
"new code must separate /api/v1 and /api-v1"
|
||||
);
|
||||
console.log(" PASS: collision fix — /api/v1 vs /api-v1");
|
||||
console.log(" PASS: collision fix - /api/v1 vs /api-v1");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed
|
||||
// Test 11: /app.v2 and /app/v2 and /app-v2 - three-way collision fixed
|
||||
{
|
||||
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
||||
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
||||
@@ -245,14 +245,14 @@ function runTests() {
|
||||
"three paths must produce three unique keys"
|
||||
);
|
||||
console.log(
|
||||
" PASS: collision fix — three-way /app.v2, /app/v2, /app-v2"
|
||||
" PASS: collision fix - three-way /app.v2, /app/v2, /app-v2"
|
||||
);
|
||||
passed++;
|
||||
}
|
||||
|
||||
// ── Edge cases ───────────────────────────────────────────────────
|
||||
|
||||
// Test 12: same path in different resources — always separate
|
||||
// Test 12: same path in different resources - always separate
|
||||
{
|
||||
const key1 = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const key2 = newKeyComputation(2, "/api", "prefix", null, null);
|
||||
@@ -261,11 +261,11 @@ function runTests() {
|
||||
true,
|
||||
"different resources with same path must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different resources");
|
||||
console.log(" PASS: edge case - same path, different resources");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 13: same resource, different pathMatchType — separate keys
|
||||
// Test 13: same resource, different pathMatchType - separate keys
|
||||
{
|
||||
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
||||
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
@@ -274,11 +274,11 @@ function runTests() {
|
||||
true,
|
||||
"exact vs prefix must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different match types");
|
||||
console.log(" PASS: edge case - same path, different match types");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 14: same resource and path, different rewrite config — separate keys
|
||||
// Test 14: same resource and path, different rewrite config - separate keys
|
||||
{
|
||||
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const withRewrite = newKeyComputation(
|
||||
@@ -293,7 +293,7 @@ function runTests() {
|
||||
true,
|
||||
"with vs without rewrite must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different rewrite config");
|
||||
console.log(" PASS: edge case - same path, different rewrite config");
|
||||
passed++;
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ function runTests() {
|
||||
paths.length,
|
||||
"special URL chars must produce unique keys"
|
||||
);
|
||||
console.log(" PASS: edge case — special URL characters in paths");
|
||||
console.log(" PASS: edge case - special URL characters in paths");
|
||||
passed++;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function verifyDomainAccess(
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const domainId =
|
||||
req.params.domainId || req.body.apiKeyId || req.query.apiKeyId;
|
||||
req.params.domainId;
|
||||
const orgId = req.params.orgId;
|
||||
|
||||
if (!userId) {
|
||||
|
||||
478
server/private/lib/acmeCertSync.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* 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 fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import {
|
||||
certificates,
|
||||
clients,
|
||||
clientSiteResourcesAssociationsCache,
|
||||
db,
|
||||
domains,
|
||||
newts,
|
||||
siteNetworks,
|
||||
SiteResource,
|
||||
siteResources
|
||||
} from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import config from "@server/lib/config";
|
||||
import {
|
||||
generateSubnetProxyTargetV2,
|
||||
SubnetProxyTargetV2
|
||||
} from "@server/lib/ip";
|
||||
import { updateTargets } from "@server/routers/client/targets";
|
||||
import cache from "#private/lib/cache";
|
||||
import { build } from "@server/build";
|
||||
|
||||
interface AcmeCert {
|
||||
domain: { main: string; sans?: string[] };
|
||||
certificate: string;
|
||||
key: string;
|
||||
Store: string;
|
||||
}
|
||||
|
||||
interface AcmeJson {
|
||||
[resolver: string]: {
|
||||
Certificates: AcmeCert[];
|
||||
};
|
||||
}
|
||||
|
||||
async function pushCertUpdateToAffectedNewts(
|
||||
domain: string,
|
||||
domainId: string | null,
|
||||
oldCertPem: string | null,
|
||||
oldKeyPem: string | null
|
||||
): Promise<void> {
|
||||
// Find all SSL-enabled HTTP site resources that use this cert's domain
|
||||
let affectedResources: SiteResource[] = [];
|
||||
|
||||
if (domainId) {
|
||||
affectedResources = await db
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResources.domainId, domainId),
|
||||
eq(siteResources.ssl, true)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Fallback: match by exact fullDomain when no domainId is available
|
||||
affectedResources = await db
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResources.fullDomain, domain),
|
||||
eq(siteResources.ssl, true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (affectedResources.length === 0) {
|
||||
logger.debug(
|
||||
`acmeCertSync: no affected site resources for cert domain "${domain}"`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`acmeCertSync: pushing cert update to ${affectedResources.length} affected site resource(s) for domain "${domain}"`
|
||||
);
|
||||
|
||||
for (const resource of affectedResources) {
|
||||
try {
|
||||
// Get all sites for this resource via siteNetworks
|
||||
const resourceSiteRows = resource.networkId
|
||||
? await db
|
||||
.select({ siteId: siteNetworks.siteId })
|
||||
.from(siteNetworks)
|
||||
.where(eq(siteNetworks.networkId, resource.networkId))
|
||||
: [];
|
||||
|
||||
if (resourceSiteRows.length === 0) {
|
||||
logger.debug(
|
||||
`acmeCertSync: no sites for resource ${resource.siteResourceId}, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all clients with access to this resource
|
||||
const resourceClients = await db
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clients.clientId,
|
||||
clientSiteResourcesAssociationsCache.clientId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
resource.siteResourceId
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceClients.length === 0) {
|
||||
logger.debug(
|
||||
`acmeCertSync: no clients for resource ${resource.siteResourceId}, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invalidate the cert cache so generateSubnetProxyTargetV2 fetches fresh data
|
||||
if (resource.fullDomain) {
|
||||
await cache.del(`cert:${resource.fullDomain}`);
|
||||
}
|
||||
|
||||
// Generate target once - same cert applies to all sites for this resource
|
||||
const newTargets = await generateSubnetProxyTargetV2(
|
||||
resource,
|
||||
resourceClients
|
||||
);
|
||||
|
||||
if (!newTargets) {
|
||||
logger.debug(
|
||||
`acmeCertSync: could not generate target for resource ${resource.siteResourceId}, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the old targets - same routing shape but with the previous cert/key.
|
||||
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
|
||||
// semantics correct so the update message accurately reflects what changed.
|
||||
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
|
||||
...t,
|
||||
tlsCert: oldCertPem ?? undefined,
|
||||
tlsKey: oldKeyPem ?? undefined
|
||||
}));
|
||||
|
||||
// Push update to each site's newt
|
||||
for (const { siteId } of resourceSiteRows) {
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!newt) {
|
||||
logger.debug(
|
||||
`acmeCertSync: no newt found for site ${siteId}, skipping resource ${resource.siteResourceId}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{ oldTargets: oldTargets, newTargets: newTargets },
|
||||
newt.version
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`acmeCertSync: pushed cert update to newt for site ${siteId}, resource ${resource.siteResourceId}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`acmeCertSync: error pushing cert update for resource ${resource?.siteResourceId}: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findDomainId(certDomain: string): Promise<string | null> {
|
||||
// Strip wildcard prefix before lookup (*.example.com -> example.com)
|
||||
const lookupDomain = certDomain.startsWith("*.")
|
||||
? certDomain.slice(2)
|
||||
: certDomain;
|
||||
|
||||
// 1. Exact baseDomain match (any domain type)
|
||||
const exactMatch = await db
|
||||
.select({ domainId: domains.domainId })
|
||||
.from(domains)
|
||||
.where(eq(domains.baseDomain, lookupDomain))
|
||||
.limit(1);
|
||||
|
||||
if (exactMatch.length > 0) {
|
||||
return exactMatch[0].domainId;
|
||||
}
|
||||
|
||||
// 2. Walk up the domain hierarchy looking for a wildcard-type domain whose
|
||||
// baseDomain is a suffix of the cert domain. e.g. cert "sub.example.com"
|
||||
// matches a wildcard domain with baseDomain "example.com".
|
||||
const parts = lookupDomain.split(".");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const candidate = parts.slice(i).join(".");
|
||||
if (!candidate) continue;
|
||||
|
||||
const wildcardMatch = await db
|
||||
.select({ domainId: domains.domainId })
|
||||
.from(domains)
|
||||
.where(
|
||||
and(
|
||||
eq(domains.baseDomain, candidate),
|
||||
eq(domains.type, "wildcard")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (wildcardMatch.length > 0) {
|
||||
return wildcardMatch[0].domainId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractFirstCert(pemBundle: string): string | null {
|
||||
const match = pemBundle.match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/
|
||||
);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
async function syncAcmeCerts(
|
||||
acmeJsonPath: string,
|
||||
resolver: string
|
||||
): Promise<void> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(acmeJsonPath, "utf8");
|
||||
} catch (err) {
|
||||
logger.debug(`acmeCertSync: could not read ${acmeJsonPath}: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let acmeJson: AcmeJson;
|
||||
try {
|
||||
acmeJson = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
logger.debug(`acmeCertSync: could not parse acme.json: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolverData = acmeJson[resolver];
|
||||
if (!resolverData || !Array.isArray(resolverData.Certificates)) {
|
||||
logger.debug(
|
||||
`acmeCertSync: no certificates found for resolver "${resolver}"`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const cert of resolverData.Certificates) {
|
||||
const domain = cert.domain?.main;
|
||||
|
||||
if (!domain) {
|
||||
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cert.certificate || !cert.key) {
|
||||
logger.debug(
|
||||
`acmeCertSync: skipping cert for ${domain} - empty certificate or key field`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const certPem = Buffer.from(cert.certificate, "base64").toString(
|
||||
"utf8"
|
||||
);
|
||||
const keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
||||
|
||||
if (!certPem.trim() || !keyPem.trim()) {
|
||||
logger.debug(
|
||||
`acmeCertSync: skipping cert for ${domain} - blank PEM after base64 decode`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if cert already exists in DB
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(eq(certificates.domain, domain))
|
||||
.limit(1);
|
||||
|
||||
let oldCertPem: string | null = null;
|
||||
let oldKeyPem: string | null = null;
|
||||
|
||||
if (existing.length > 0 && existing[0].certFile) {
|
||||
try {
|
||||
const storedCertPem = decrypt(
|
||||
existing[0].certFile,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
if (storedCertPem === certPem) {
|
||||
logger.debug(
|
||||
`acmeCertSync: cert for ${domain} is unchanged, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Cert has changed; capture old values so we can send a correct
|
||||
// update message to the newt after the DB write.
|
||||
oldCertPem = storedCertPem;
|
||||
if (existing[0].keyFile) {
|
||||
try {
|
||||
oldKeyPem = decrypt(
|
||||
existing[0].keyFile,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
} catch (keyErr) {
|
||||
logger.debug(
|
||||
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Decryption failure means we should proceed with the update
|
||||
logger.debug(
|
||||
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse cert expiry from the first cert in the PEM bundle
|
||||
let expiresAt: number | null = null;
|
||||
const firstCertPem = extractFirstCert(certPem);
|
||||
if (firstCertPem) {
|
||||
try {
|
||||
const x509 = new crypto.X509Certificate(firstCertPem);
|
||||
expiresAt = Math.floor(new Date(x509.validTo).getTime() / 1000);
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const wildcard = domain.startsWith("*.");
|
||||
const encryptedCert = encrypt(
|
||||
certPem,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
const encryptedKey = encrypt(
|
||||
keyPem,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const domainId = await findDomainId(domain);
|
||||
if (domainId) {
|
||||
logger.debug(
|
||||
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(certificates)
|
||||
.set({
|
||||
certFile: encryptedCert,
|
||||
keyFile: encryptedKey,
|
||||
status: "valid",
|
||||
expiresAt,
|
||||
updatedAt: now,
|
||||
wildcard,
|
||||
...(domainId !== null && { domainId })
|
||||
})
|
||||
.where(eq(certificates.domain, domain));
|
||||
|
||||
logger.info(
|
||||
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||
);
|
||||
|
||||
await pushCertUpdateToAffectedNewts(
|
||||
domain,
|
||||
domainId,
|
||||
oldCertPem,
|
||||
oldKeyPem
|
||||
);
|
||||
} else {
|
||||
await db.insert(certificates).values({
|
||||
domain,
|
||||
domainId,
|
||||
certFile: encryptedCert,
|
||||
keyFile: encryptedKey,
|
||||
status: "valid",
|
||||
expiresAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
wildcard
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||
);
|
||||
|
||||
// For a brand-new cert, push to any SSL resources that were waiting for it
|
||||
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initAcmeCertSync(): void {
|
||||
if (build == "saas") {
|
||||
logger.debug(`acmeCertSync: skipping ACME cert sync in SaaS build`);
|
||||
return;
|
||||
}
|
||||
|
||||
const privateConfigData = privateConfig.getRawPrivateConfig();
|
||||
|
||||
if (!privateConfigData.flags?.enable_acme_cert_sync) {
|
||||
logger.debug(
|
||||
`acmeCertSync: ACME cert sync is disabled by config flag, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (privateConfigData.flags.use_pangolin_dns) {
|
||||
logger.debug(
|
||||
`acmeCertSync: ACME cert sync requires use_pangolin_dns flag to be disabled, skipping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const acmeJsonPath =
|
||||
privateConfigData.acme?.acme_json_path ??
|
||||
"config/letsencrypt/acme.json";
|
||||
const resolver = privateConfigData.acme?.resolver ?? "letsencrypt";
|
||||
const intervalMs = privateConfigData.acme?.sync_interval_ms ?? 5000;
|
||||
|
||||
logger.info(
|
||||
`acmeCertSync: starting ACME cert sync from "${acmeJsonPath}" using resolver "${resolver}" every ${intervalMs}ms`
|
||||
);
|
||||
|
||||
// Run immediately on init, then on the configured interval
|
||||
syncAcmeCerts(acmeJsonPath, resolver).catch((err) => {
|
||||
logger.error(`acmeCertSync: error during initial sync: ${err}`);
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
syncAcmeCerts(acmeJsonPath, resolver).catch((err) => {
|
||||
logger.error(`acmeCertSync: error during sync: ${err}`);
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
91
server/private/lib/alerts/events/healthCheckEvents.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 { processAlerts } from "../processAlerts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fire a `health_check_healthy` alert for the given health check.
|
||||
*
|
||||
* Call this after a previously-failing health check has recovered so that any
|
||||
* matching `alertRules` can dispatch their email and webhook actions.
|
||||
*
|
||||
* @param orgId - Organisation that owns the health check.
|
||||
* @param healthCheckId - Numeric primary key of the health check.
|
||||
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
||||
* @param extra - Any additional key/value pairs to include in the payload.
|
||||
*/
|
||||
export async function fireHealthCheckHealthyAlert(
|
||||
orgId: string,
|
||||
healthCheckId: number,
|
||||
healthCheckName?: string | null,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await processAlerts({
|
||||
eventType: "health_check_healthy",
|
||||
orgId,
|
||||
healthCheckId,
|
||||
data: {
|
||||
healthCheckId,
|
||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||
...extra
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`fireHealthCheckHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a `health_check_unhealthy` alert for the given health check.
|
||||
*
|
||||
* Call this after a health check has been detected as failing so that any
|
||||
* matching `alertRules` can dispatch their email and webhook actions.
|
||||
*
|
||||
* @param orgId - Organisation that owns the health check.
|
||||
* @param healthCheckId - Numeric primary key of the health check.
|
||||
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
||||
* @param extra - Any additional key/value pairs to include in the payload.
|
||||
*/
|
||||
export async function fireHealthCheckNotHealthyAlert(
|
||||
orgId: string,
|
||||
healthCheckId: number,
|
||||
healthCheckName?: string | null,
|
||||
extra?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await processAlerts({
|
||||
eventType: "health_check_unhealthy",
|
||||
orgId,
|
||||
healthCheckId,
|
||||
data: {
|
||||
healthCheckId,
|
||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||
...extra
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`fireHealthCheckNotHealthyAlert: unexpected error for healthCheckId ${healthCheckId}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||