Compare commits
166 commits
Author | SHA1 | Date | |
---|---|---|---|
|
cbe1e4decb |
||
|
17e3568b0d |
||
|
1f827d8ecf |
||
|
ee5bfb96c7 |
||
|
2c3f44891e |
||
|
35ae064ce9 |
||
|
91e0accc2a |
||
|
63010c7889 |
||
|
0ecfde982d |
||
|
c9fd050673 |
||
|
0ea0b241c1 |
||
|
bf4de08896 |
||
|
3e7712d7fd |
||
|
c206bd120d |
||
|
4e55368d83 |
||
|
38db3f4180 |
||
|
a450521714 |
||
|
aba894eedf |
||
|
bcf4523689 |
||
|
b1255ddb1e |
||
|
5f43268b88 |
||
|
7a17518054 |
||
|
8992e99c07 |
||
|
bf95b0a659 |
||
|
4b824f653f |
||
|
edf1cea738 |
||
|
5d87e7371a |
||
|
7223761c43 |
||
|
0568e74821 |
||
|
87c0bd7717 |
||
|
d916dd802e |
||
|
94a7aae289 |
||
|
311df4cb0f |
||
|
52bdfd2506 |
||
|
a60038b79b |
||
|
8aa92176cb |
||
|
5ba9bcd7ba |
||
|
e55bc27992 |
||
|
fbd1534ed3 |
||
|
1f0de303cf |
||
|
32f7d9f527 |
||
|
98cb271d26 |
||
|
4a35189c18 |
||
|
476cd915ab |
||
|
f7293a3933 |
||
|
47b1d52f67 |
||
|
cb80c6eca4 |
||
|
8d91fdb987 |
||
|
2e037d2c0f |
||
|
ea01e019c7 |
||
|
c74cc48b83 |
||
|
f33a0d4fc8 |
||
|
911faf9843 |
||
|
fa115b455d |
||
|
30b6d42047 |
||
|
2a04f58f0d |
||
|
ef0bfc8844 |
||
|
aac4a93550 |
||
|
451aa5ecfd |
||
|
4496610714 |
||
|
d46fa115a8 |
||
|
dec208cdea |
||
|
5984079995 |
||
|
aa43d1936f |
||
|
d7d1af59b5 |
||
|
adca1348b6 |
||
|
915fa3e8e1 |
||
|
cc009a14d6 |
||
|
48ab3f9a80 |
||
|
52e56744c0 |
||
|
3b74f84665 |
||
|
901c0483c1 |
||
|
1bc5b6b472 |
||
|
009f7b23d9 |
||
|
f9ea02065e |
||
|
fa36aeca1c |
||
|
a0b7f14b9e |
||
|
7f6ee7c88d |
||
|
8f29c42ab4 |
||
|
7f4d410cbd |
||
|
76e19508f0 |
||
|
6d210b128d |
||
|
ef5f713bfb |
||
|
fc24331988 |
||
|
75de3118ab |
||
|
597c15b83d |
||
|
3027a0dc13 |
||
|
d27ed4d523 |
||
|
0522b93369 |
||
|
47eb1262e0 |
||
|
b503d73e46 |
||
|
2bacfa8002 |
||
|
0d684c1d01 |
||
|
e93a2a6f4d |
||
|
5223de6b92 |
||
|
824e8a2818 |
||
|
96e4a28a2a |
||
|
c2ac48400b |
||
|
f63668dc1a |
||
|
52c659feb9 |
||
|
4220f518d0 |
||
|
bb57f7537e |
||
|
1ea1e2806e |
||
|
599228274f |
||
|
471a15f3a0 |
||
|
bd425f5cfe |
||
|
50e94d2d1f |
||
|
997cbca98e |
||
|
3a1eddea43 |
||
|
764778ecc2 |
||
|
22f3dc9dac |
||
|
65b4b53064 |
||
|
f404b3bf35 |
||
|
16c16b81bc |
||
|
d868545d0d |
||
|
383f70f92f |
||
|
22b9f926dc |
||
|
f46e190f4a |
||
|
cf11ac8853 |
||
|
d040ca4208 |
||
|
34d9641aab |
||
|
4a70868d82 |
||
|
fb0fee244f |
||
|
60a4e31ff5 |
||
|
01fef8ab09 |
||
|
f7afdca5c0 |
||
|
567e74046d |
||
|
452338e67b |
||
|
c9dfff290b |
||
|
dac9608bef |
||
|
38c8f576e0 |
||
|
553cc30836 |
||
|
3538bb793e |
||
|
58ebd1275b |
||
|
791bcb2df1 |
||
|
227c898583 |
||
|
f889e54052 |
||
|
2814d2410b |
||
|
1b9ed8aab4 |
||
|
1cdecf8480 |
||
|
d1890a2756 |
||
|
a6df41cb43 |
||
|
068d8484fb |
||
|
cbee67e475 |
||
|
60d392742d |
||
|
bc151df638 |
||
|
685be473bc |
||
|
26cc0f8184 |
||
|
5efd1d5405 |
||
|
5fd411a54e |
||
|
35facd1d4c |
||
|
956db6ba2e |
||
|
49d10eed33 |
||
|
d55e4ef498 |
||
|
ce320ca2cf |
||
|
dba4918edf |
||
|
adc57375f2 |
||
|
be2c91415a |
||
|
c1ba993ac8 |
||
|
16cbe9bb69 |
||
|
52b465b289 |
||
|
eba7f55a62 |
||
|
e8585f2a66 |
||
|
459853ca99 |
||
|
a21de9f1cb |
||
|
f9f47f3869 |
191 changed files with 8630 additions and 1655 deletions
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.24
|
||||||
|
|
||||||
|
|
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|
15
README.md
15
README.md
|
@ -20,15 +20,24 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
<a href="https://docs.digpangolin.com/self-host/quick-install-managed">
|
||||||
Install Guide
|
Quick Install Guide
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="mailto:numbat@fossorial.io">
|
<a href="mailto:contact@fossorial.io">
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://digpangolin.com/slack">
|
||||||
|
Slack
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://discord.gg/HCJR8Xhme4">
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
[](https://digpangolin.com/slack)
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||

|

|
||||||
[](https://discord.gg/HCJR8Xhme4)
|
[](https://discord.gg/HCJR8Xhme4)
|
||||||
|
|
72
blueprint.py
Normal file
72
blueprint.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import requests
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# The file path for the YAML file to be read
|
||||||
|
# You can change this to the path of your YAML file
|
||||||
|
YAML_FILE_PATH = 'blueprint.yaml'
|
||||||
|
|
||||||
|
# The API endpoint and headers from the curl request
|
||||||
|
API_URL = 'http://localhost:3004/v1/org/test/blueprint'
|
||||||
|
HEADERS = {
|
||||||
|
'accept': '*/*',
|
||||||
|
'Authorization': 'Bearer v7ix7xha1bmq2on.tzsden374mtmkeczm3tx44uzxsljnrst7nmg7ccr',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_and_send(file_path, url, headers):
|
||||||
|
"""
|
||||||
|
Reads a YAML file, converts its content to a JSON payload,
|
||||||
|
and sends it via a PUT request to a specified URL.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Read the YAML file content
|
||||||
|
with open(file_path, 'r') as file:
|
||||||
|
yaml_content = file.read()
|
||||||
|
|
||||||
|
# Parse the YAML string to a Python dictionary
|
||||||
|
# This will be used to ensure the YAML is valid before sending
|
||||||
|
parsed_yaml = yaml.safe_load(yaml_content)
|
||||||
|
|
||||||
|
# convert the parsed YAML to a JSON string
|
||||||
|
json_payload = json.dumps(parsed_yaml)
|
||||||
|
print("Converted JSON payload:")
|
||||||
|
print(json_payload)
|
||||||
|
|
||||||
|
# Encode the JSON string to Base64
|
||||||
|
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
# Create the final payload with the base64 encoded data
|
||||||
|
final_payload = {
|
||||||
|
"blueprint": encoded_json
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Sending the following Base64 encoded JSON payload:")
|
||||||
|
print(final_payload)
|
||||||
|
print("-" * 20)
|
||||||
|
|
||||||
|
# Make the PUT request with the base64 encoded payload
|
||||||
|
response = requests.put(url, headers=headers, json=final_payload)
|
||||||
|
|
||||||
|
# Print the API response for debugging
|
||||||
|
print(f"API Response Status Code: {response.status_code}")
|
||||||
|
print("API Response Content:")
|
||||||
|
print(response.text)
|
||||||
|
|
||||||
|
# Raise an exception for bad status codes (4xx or 5xx)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: The file '{file_path}' was not found.")
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
print(f"Error parsing YAML file: {e}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"An error occurred during the API request: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
# Run the function
|
||||||
|
if __name__ == "__main__":
|
||||||
|
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
||||||
|
|
69
blueprint.yaml
Normal file
69
blueprint.yaml
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
client-resources:
|
||||||
|
client-resource-nice-id-uno:
|
||||||
|
name: this is my resource
|
||||||
|
protocol: tcp
|
||||||
|
proxy-port: 3001
|
||||||
|
hostname: localhost
|
||||||
|
internal-port: 3000
|
||||||
|
site: lively-yosemite-toad
|
||||||
|
client-resource-nice-id-duce:
|
||||||
|
name: this is my resource
|
||||||
|
protocol: udp
|
||||||
|
proxy-port: 3000
|
||||||
|
hostname: localhost
|
||||||
|
internal-port: 3000
|
||||||
|
site: lively-yosemite-toad
|
||||||
|
|
||||||
|
proxy-resources:
|
||||||
|
resource-nice-id-uno:
|
||||||
|
name: this is my resource
|
||||||
|
protocol: http
|
||||||
|
full-domain: duce.test.example.com
|
||||||
|
host-header: example.com
|
||||||
|
tls-server-name: example.com
|
||||||
|
# auth:
|
||||||
|
# pincode: 123456
|
||||||
|
# password: sadfasdfadsf
|
||||||
|
# sso-enabled: true
|
||||||
|
# sso-roles:
|
||||||
|
# - Member
|
||||||
|
# sso-users:
|
||||||
|
# - owen@fossorial.io
|
||||||
|
# whitelist-users:
|
||||||
|
# - owen@fossorial.io
|
||||||
|
headers:
|
||||||
|
- name: X-Example-Header
|
||||||
|
value: example-value
|
||||||
|
- name: X-Another-Header
|
||||||
|
value: another-value
|
||||||
|
rules:
|
||||||
|
- action: allow
|
||||||
|
match: ip
|
||||||
|
value: 1.1.1.1
|
||||||
|
- action: deny
|
||||||
|
match: cidr
|
||||||
|
value: 2.2.2.2/32
|
||||||
|
- action: pass
|
||||||
|
match: path
|
||||||
|
value: /admin
|
||||||
|
targets:
|
||||||
|
- site: lively-yosemite-toad
|
||||||
|
path: /path
|
||||||
|
pathMatchType: prefix
|
||||||
|
hostname: localhost
|
||||||
|
method: http
|
||||||
|
port: 8000
|
||||||
|
- site: slim-alpine-chipmunk
|
||||||
|
hostname: localhost
|
||||||
|
path: /yoman
|
||||||
|
pathMatchType: exact
|
||||||
|
method: http
|
||||||
|
port: 8001
|
||||||
|
resource-nice-id-duce:
|
||||||
|
name: this is other resource
|
||||||
|
protocol: tcp
|
||||||
|
proxy-port: 3000
|
||||||
|
targets:
|
||||||
|
- site: lively-yosemite-toad
|
||||||
|
hostname: localhost
|
||||||
|
port: 3000
|
|
@ -16,8 +16,9 @@ http:
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`)"
|
||||||
service: next-service
|
service: next-service
|
||||||
|
priority: 10
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
tls:
|
tls:
|
||||||
|
@ -27,15 +28,7 @@ http:
|
||||||
api-router:
|
api-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
priority: 100
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
# WebSocket router
|
|
||||||
ws-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
|
||||||
service: api-service
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
tls:
|
tls:
|
||||||
|
|
|
@ -13,7 +13,6 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- ENVIRONMENT=dev
|
- ENVIRONMENT=dev
|
||||||
- DB_TYPE=pg
|
|
||||||
volumes:
|
volumes:
|
||||||
# Mount source code for hot reload
|
# Mount source code for hot reload
|
||||||
- ./src:/app/src
|
- ./src:/app/src
|
||||||
|
|
|
@ -52,7 +52,7 @@ esbuild
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: argv.out,
|
outfile: argv.out,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
minify: true,
|
minify: false,
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,7 @@ esbuild
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
sourcemap: "external",
|
sourcemap: "inline",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module installer
|
module installer
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.34.0
|
golang.org/x/term v0.35.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.35.0 // indirect
|
require golang.org/x/sys v0.36.0 // indirect
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -15,14 +17,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||||
func loadVersions(config *Config) {
|
func loadVersions(config *Config) {
|
||||||
config.PangolinVersion = "replaceme"
|
config.PangolinVersion = "1.9.4"
|
||||||
config.GerbilVersion = "replaceme"
|
config.GerbilVersion = "1.2.1"
|
||||||
config.BadgerVersion = "replaceme"
|
config.BadgerVersion = "1.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed config/*
|
//go:embed config/*
|
||||||
|
@ -74,7 +75,7 @@ func main() {
|
||||||
if err := checkPortsAvailable(p); err != nil {
|
if err := checkPortsAvailable(p); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
|
||||||
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
|
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly. If you already have the Pangolin stack running, shut them down before proceeding.\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,7 +127,7 @@ func main() {
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker(reader)
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
installDocker()
|
||||||
|
@ -204,8 +205,17 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.InstallationContainerType = podmanOrDocker(reader)
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
installCrowdsec(config)
|
err := installCrowdsec(config)
|
||||||
|
if (err != nil) {
|
||||||
|
fmt.Printf("Error installing CrowdSec: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("CrowdSec installed successfully!")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,13 +332,18 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
|
|
||||||
if config.HybridMode {
|
if config.HybridMode {
|
||||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
||||||
|
|
||||||
if alreadyHaveCreds {
|
if alreadyHaveCreds {
|
||||||
config.HybridId = readString(reader, "Enter your ID", "")
|
config.HybridId = readString(reader, "Enter your ID", "")
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
config.HybridSecret = readString(reader, "Enter your secret", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
// Try to get public IP as default
|
||||||
|
publicIP := getPublicIP()
|
||||||
|
if publicIP != "" {
|
||||||
|
fmt.Printf("Detected public IP: %s\n", publicIP)
|
||||||
|
}
|
||||||
|
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
|
||||||
config.InstallGerbil = true
|
config.InstallGerbil = true
|
||||||
} else {
|
} else {
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
@ -345,7 +360,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
|
@ -353,7 +368,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if config.BaseDomain == "" {
|
if config.BaseDomain == "" {
|
||||||
fmt.Println("Error: Domain name is required")
|
fmt.Println("Error: Domain name is required")
|
||||||
|
@ -584,6 +599,32 @@ func generateRandomSecretKey() string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPublicIP() string {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get("https://ifconfig.io/ip")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := strings.TrimSpace(string(body))
|
||||||
|
|
||||||
|
// Validate that it's a valid IP address
|
||||||
|
if net.ParseIP(ip) != nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// Run external commands with stdio/stderr attached.
|
// Run external commands with stdio/stderr attached.
|
||||||
func run(name string, args ...string) error {
|
func run(name string, args ...string) error {
|
||||||
cmd := exec.Command(name, args...)
|
cmd := exec.Command(name, args...)
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
||||||
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"matchPath": "Match Path",
|
||||||
"ipAddressRange": "IP Range",
|
"ipAddressRange": "IP Range",
|
||||||
"rulesErrorFetch": "Failed to fetch rules",
|
"rulesErrorFetch": "Failed to fetch rules",
|
||||||
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Connected",
|
"idpConnectingToFinished": "Connected",
|
||||||
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
||||||
"idpErrorNotFound": "IdP not found",
|
"idpErrorNotFound": "IdP not found",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invalid Invite",
|
"inviteInvalid": "Invalid Invite",
|
||||||
"inviteInvalidDescription": "The invite link is invalid.",
|
"inviteInvalidDescription": "The invite link is invalid.",
|
||||||
"inviteErrorWrongUser": "Invite is not for this user",
|
"inviteErrorWrongUser": "Invite is not for this user",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Professional Edition Required",
|
"licenseTierProfessionalRequired": "Professional Edition Required",
|
||||||
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
||||||
"actionGetOrg": "Get Organization",
|
"actionGetOrg": "Get Organization",
|
||||||
|
"updateOrgUser": "Update Org User",
|
||||||
|
"createOrgUser": "Create Org User",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Update Organization",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Update User",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Socket",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||||
"enableDockerSocketLink": "Learn More",
|
"enableDockerSocketLink": "Learn More",
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Update Available",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
||||||
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
||||||
"domainPickerTabAll": "All",
|
"domainPickerTabAll": "All",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocol",
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
"editInternalResourceDialogSitePort": "Site Port",
|
"editInternalResourceDialogSitePort": "Site Port",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"editInternalResourceDialogDestinationIP": "Destination IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"editInternalResourceDialogCancel": "Cancel",
|
"editInternalResourceDialogCancel": "Cancel",
|
||||||
"editInternalResourceDialogSaveResource": "Save Resource",
|
"editInternalResourceDialogSaveResource": "Save Resource",
|
||||||
"editInternalResourceDialogSuccess": "Success",
|
"editInternalResourceDialogSuccess": "Success",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site Port",
|
"createInternalResourceDialogSitePort": "Site Port",
|
||||||
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"createInternalResourceDialogDestinationIP": "Destination IP",
|
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
||||||
"createInternalResourceDialogCancel": "Cancel",
|
"createInternalResourceDialogCancel": "Cancel",
|
||||||
"createInternalResourceDialogCreateResource": "Create Resource",
|
"createInternalResourceDialogCreateResource": "Create Resource",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "International Domain Detected",
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
"willbestoredas": "Will be stored as:"
|
"willbestoredas": "Will be stored as:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Custom Headers",
|
||||||
|
"headersValidationError": "Headers must be in the format: Header-Name: value.",
|
||||||
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
|
"domainPickerVerified": "Verified",
|
||||||
|
"domainPickerUnverified": "Unverified",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||||
|
"domainPickerError": "Error",
|
||||||
|
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||||
|
"domainPickerErrorCheckAvailability": "Failed to check domain availability",
|
||||||
|
"domainPickerInvalidSubdomain": "Invalid subdomain",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Edit file: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
||||||
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"matchPath": "Match Path",
|
||||||
"ipAddressRange": "IP Range",
|
"ipAddressRange": "IP Range",
|
||||||
"rulesErrorFetch": "Failed to fetch rules",
|
"rulesErrorFetch": "Failed to fetch rules",
|
||||||
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Connected",
|
"idpConnectingToFinished": "Connected",
|
||||||
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
||||||
"idpErrorNotFound": "IdP not found",
|
"idpErrorNotFound": "IdP not found",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invalid Invite",
|
"inviteInvalid": "Invalid Invite",
|
||||||
"inviteInvalidDescription": "The invite link is invalid.",
|
"inviteInvalidDescription": "The invite link is invalid.",
|
||||||
"inviteErrorWrongUser": "Invite is not for this user",
|
"inviteErrorWrongUser": "Invite is not for this user",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Professional Edition Required",
|
"licenseTierProfessionalRequired": "Professional Edition Required",
|
||||||
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
||||||
"actionGetOrg": "Get Organization",
|
"actionGetOrg": "Get Organization",
|
||||||
|
"updateOrgUser": "Update Org User",
|
||||||
|
"createOrgUser": "Create Org User",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Update Organization",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Update User",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Socket",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||||
"enableDockerSocketLink": "Learn More",
|
"enableDockerSocketLink": "Learn More",
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Update Available",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
||||||
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
||||||
"domainPickerTabAll": "All",
|
"domainPickerTabAll": "All",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocol",
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
"editInternalResourceDialogSitePort": "Site Port",
|
"editInternalResourceDialogSitePort": "Site Port",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"editInternalResourceDialogDestinationIP": "Destination IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"editInternalResourceDialogCancel": "Cancel",
|
"editInternalResourceDialogCancel": "Cancel",
|
||||||
"editInternalResourceDialogSaveResource": "Save Resource",
|
"editInternalResourceDialogSaveResource": "Save Resource",
|
||||||
"editInternalResourceDialogSuccess": "Success",
|
"editInternalResourceDialogSuccess": "Success",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site Port",
|
"createInternalResourceDialogSitePort": "Site Port",
|
||||||
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"createInternalResourceDialogDestinationIP": "Destination IP",
|
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
||||||
"createInternalResourceDialogCancel": "Cancel",
|
"createInternalResourceDialogCancel": "Cancel",
|
||||||
"createInternalResourceDialogCreateResource": "Create Resource",
|
"createInternalResourceDialogCreateResource": "Create Resource",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "International Domain Detected",
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
"willbestoredas": "Will be stored as:"
|
"willbestoredas": "Will be stored as:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Custom Headers",
|
||||||
|
"headersValidationError": "Headers must be in the format: Header-Name: value.",
|
||||||
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
|
"domainPickerVerified": "Verified",
|
||||||
|
"domainPickerUnverified": "Unverified",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||||
|
"domainPickerError": "Error",
|
||||||
|
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||||
|
"domainPickerErrorCheckAvailability": "Failed to check domain availability",
|
||||||
|
"domainPickerInvalidSubdomain": "Invalid subdomain",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Edit file: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.",
|
"accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.",
|
||||||
"userSaved": "Benutzer gespeichert",
|
"userSaved": "Benutzer gespeichert",
|
||||||
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
|
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
|
||||||
|
"autoProvisioned": "Automatisch vorgesehen",
|
||||||
|
"autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter",
|
||||||
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
|
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
|
||||||
"accessControlsSubmit": "Zugriffskontrollen speichern",
|
"accessControlsSubmit": "Zugriffskontrollen speichern",
|
||||||
"roles": "Rollen",
|
"roles": "Rollen",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Ungültiges IP-Adressformat",
|
"ipAddressErrorInvalidFormat": "Ungültiges IP-Adressformat",
|
||||||
"ipAddressErrorInvalidOctet": "Ungültiges IP-Adress-Oktett",
|
"ipAddressErrorInvalidOctet": "Ungültiges IP-Adress-Oktett",
|
||||||
"path": "Pfad",
|
"path": "Pfad",
|
||||||
|
"matchPath": "Unterverzeichnis",
|
||||||
"ipAddressRange": "IP-Bereich",
|
"ipAddressRange": "IP-Bereich",
|
||||||
"rulesErrorFetch": "Fehler beim Abrufen der Regeln",
|
"rulesErrorFetch": "Fehler beim Abrufen der Regeln",
|
||||||
"rulesErrorFetchDescription": "Beim Abrufen der Regeln ist ein Fehler aufgetreten",
|
"rulesErrorFetchDescription": "Beim Abrufen der Regeln ist ein Fehler aufgetreten",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Verbunden",
|
"idpConnectingToFinished": "Verbunden",
|
||||||
"idpErrorConnectingTo": "Es gab ein Problem bei der Verbindung zu {name}. Bitte kontaktieren Sie Ihren Administrator.",
|
"idpErrorConnectingTo": "Es gab ein Problem bei der Verbindung zu {name}. Bitte kontaktieren Sie Ihren Administrator.",
|
||||||
"idpErrorNotFound": "IdP nicht gefunden",
|
"idpErrorNotFound": "IdP nicht gefunden",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Ungültige Einladung",
|
"inviteInvalid": "Ungültige Einladung",
|
||||||
"inviteInvalidDescription": "Der Einladungslink ist ungültig.",
|
"inviteInvalidDescription": "Der Einladungslink ist ungültig.",
|
||||||
"inviteErrorWrongUser": "Einladung ist nicht für diesen Benutzer",
|
"inviteErrorWrongUser": "Einladung ist nicht für diesen Benutzer",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Professional Edition erforderlich",
|
"licenseTierProfessionalRequired": "Professional Edition erforderlich",
|
||||||
"licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.",
|
"licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.",
|
||||||
"actionGetOrg": "Organisation abrufen",
|
"actionGetOrg": "Organisation abrufen",
|
||||||
|
"updateOrgUser": "Org Benutzer aktualisieren",
|
||||||
|
"createOrgUser": "Org Benutzer erstellen",
|
||||||
"actionUpdateOrg": "Organisation aktualisieren",
|
"actionUpdateOrg": "Organisation aktualisieren",
|
||||||
"actionUpdateUser": "Benutzer aktualisieren",
|
"actionUpdateUser": "Benutzer aktualisieren",
|
||||||
"actionGetUser": "Benutzer abrufen",
|
"actionGetUser": "Benutzer abrufen",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Standort löschen",
|
"actionDeleteSite": "Standort löschen",
|
||||||
"actionGetSite": "Standort abrufen",
|
"actionGetSite": "Standort abrufen",
|
||||||
"actionListSites": "Standorte auflisten",
|
"actionListSites": "Standorte auflisten",
|
||||||
|
"actionApplyBlueprint": "Blaupause anwenden",
|
||||||
"setupToken": "Setup-Token",
|
"setupToken": "Setup-Token",
|
||||||
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
||||||
"setupTokenRequired": "Setup-Token ist erforderlich",
|
"setupTokenRequired": "Setup-Token ist erforderlich",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Lizenz",
|
"sidebarLicense": "Lizenz",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Docker Socket aktivieren",
|
"enableDockerSocket": "Docker Blaupause aktivieren",
|
||||||
"enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.",
|
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||||
"enableDockerSocketLink": "Mehr erfahren",
|
"enableDockerSocketLink": "Mehr erfahren",
|
||||||
"viewDockerContainers": "Docker Container anzeigen",
|
"viewDockerContainers": "Docker Container anzeigen",
|
||||||
"containersIn": "Container in {siteName}",
|
"containersIn": "Container in {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Update verfügbar",
|
"newtUpdateAvailable": "Update verfügbar",
|
||||||
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
|
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.",
|
"domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.",
|
||||||
"domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen",
|
"domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen",
|
||||||
"domainPickerTabAll": "Alle",
|
"domainPickerTabAll": "Alle",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protokoll",
|
"editInternalResourceDialogProtocol": "Protokoll",
|
||||||
"editInternalResourceDialogSitePort": "Site-Port",
|
"editInternalResourceDialogSitePort": "Site-Port",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
"editInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
||||||
"editInternalResourceDialogDestinationIP": "Ziel-IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Ziel-Port",
|
|
||||||
"editInternalResourceDialogCancel": "Abbrechen",
|
"editInternalResourceDialogCancel": "Abbrechen",
|
||||||
"editInternalResourceDialogSaveResource": "Ressource speichern",
|
"editInternalResourceDialogSaveResource": "Ressource speichern",
|
||||||
"editInternalResourceDialogSuccess": "Erfolg",
|
"editInternalResourceDialogSuccess": "Erfolg",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site-Port",
|
"createInternalResourceDialogSitePort": "Site-Port",
|
||||||
"createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.",
|
"createInternalResourceDialogSitePortDescription": "Verwenden Sie diesen Port, um bei Verbindung mit einem Client auf die Ressource an der Site zuzugreifen.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
"createInternalResourceDialogTargetConfiguration": "Zielkonfiguration",
|
||||||
"createInternalResourceDialogDestinationIP": "Ziel-IP",
|
"createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse oder Hostname Adresse der Ressource im Netzwerk der Website.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse der Ressource im Netzwerkstandort der Site.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Ziel-Port",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.",
|
"createInternalResourceDialogDestinationPortDescription": "Der Port auf der Ziel-IP, unter dem die Ressource zugänglich ist.",
|
||||||
"createInternalResourceDialogCancel": "Abbrechen",
|
"createInternalResourceDialogCancel": "Abbrechen",
|
||||||
"createInternalResourceDialogCreateResource": "Ressource erstellen",
|
"createInternalResourceDialogCreateResource": "Ressource erstellen",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Diesen Knoten in Managed Self-Hosted umwandeln"
|
"convertButton": "Diesen Knoten in Managed Self-Hosted umwandeln"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Internationale Domain erkannt",
|
"internationaldomaindetected": "Internationale Domain erkannt",
|
||||||
"willbestoredas": "Wird gespeichert als:"
|
"willbestoredas": "Wird gespeichert als:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC Provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Eigene Kopfzeilen",
|
||||||
|
"headersValidationError": "Header müssen im Format Header-Name: Wert sein.",
|
||||||
|
"domainPickerProvidedDomain": "Angegebene Domain",
|
||||||
|
"domainPickerFreeProvidedDomain": "Kostenlose Domain",
|
||||||
|
"domainPickerVerified": "Verifiziert",
|
||||||
|
"domainPickerUnverified": "Nicht verifiziert",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Diese Subdomain enthält ungültige Zeichen oder Struktur. Sie wird beim Speichern automatisch bereinigt.",
|
||||||
|
"domainPickerError": "Fehler",
|
||||||
|
"domainPickerErrorLoadDomains": "Fehler beim Laden der Organisations-Domänen",
|
||||||
|
"domainPickerErrorCheckAvailability": "Fehler beim Prüfen der Domain-Verfügbarkeit",
|
||||||
|
"domainPickerInvalidSubdomain": "Ungültige Subdomain",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "Die Eingabe \"{sub}\" wurde entfernt, weil sie nicht gültig ist.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" konnte nicht für {domain} gültig gemacht werden.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomain bereinigt",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" wurde korrigiert zu \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Datei bearbeiten: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Datei bearbeiten: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
|
||||||
"userSaved": "User saved",
|
"userSaved": "User saved",
|
||||||
"userSavedDescription": "The user has been updated.",
|
"userSavedDescription": "The user has been updated.",
|
||||||
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
|
||||||
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
"accessControlsDescription": "Manage what this user can access and do in the organization",
|
||||||
"accessControlsSubmit": "Save Access Controls",
|
"accessControlsSubmit": "Save Access Controls",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
"ipAddressErrorInvalidFormat": "Invalid IP address format",
|
||||||
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
"ipAddressErrorInvalidOctet": "Invalid IP address octet",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
|
"matchPath": "Match Path",
|
||||||
"ipAddressRange": "IP Range",
|
"ipAddressRange": "IP Range",
|
||||||
"rulesErrorFetch": "Failed to fetch rules",
|
"rulesErrorFetch": "Failed to fetch rules",
|
||||||
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
"rulesErrorFetchDescription": "An error occurred while fetching rules",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Connected",
|
"idpConnectingToFinished": "Connected",
|
||||||
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
"idpErrorConnectingTo": "There was a problem connecting to {name}. Please contact your administrator.",
|
||||||
"idpErrorNotFound": "IdP not found",
|
"idpErrorNotFound": "IdP not found",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invalid Invite",
|
"inviteInvalid": "Invalid Invite",
|
||||||
"inviteInvalidDescription": "The invite link is invalid.",
|
"inviteInvalidDescription": "The invite link is invalid.",
|
||||||
"inviteErrorWrongUser": "Invite is not for this user",
|
"inviteErrorWrongUser": "Invite is not for this user",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Professional Edition Required",
|
"licenseTierProfessionalRequired": "Professional Edition Required",
|
||||||
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
"licenseTierProfessionalRequiredDescription": "This feature is only available in the Professional Edition.",
|
||||||
"actionGetOrg": "Get Organization",
|
"actionGetOrg": "Get Organization",
|
||||||
|
"updateOrgUser": "Update Org User",
|
||||||
|
"createOrgUser": "Create Org User",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Update Organization",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Update User",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Socket",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
||||||
"enableDockerSocketLink": "Learn More",
|
"enableDockerSocketLink": "Learn More",
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Update Available",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
|
||||||
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
|
||||||
"domainPickerTabAll": "All",
|
"domainPickerTabAll": "All",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocol",
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
"editInternalResourceDialogSitePort": "Site Port",
|
"editInternalResourceDialogSitePort": "Site Port",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"editInternalResourceDialogDestinationIP": "Destination IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"editInternalResourceDialogCancel": "Cancel",
|
"editInternalResourceDialogCancel": "Cancel",
|
||||||
"editInternalResourceDialogSaveResource": "Save Resource",
|
"editInternalResourceDialogSaveResource": "Save Resource",
|
||||||
"editInternalResourceDialogSuccess": "Success",
|
"editInternalResourceDialogSuccess": "Success",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site Port",
|
"createInternalResourceDialogSitePort": "Site Port",
|
||||||
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
"createInternalResourceDialogDestinationIP": "Destination IP",
|
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Destination Port",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
||||||
"createInternalResourceDialogCancel": "Cancel",
|
"createInternalResourceDialogCancel": "Cancel",
|
||||||
"createInternalResourceDialogCreateResource": "Create Resource",
|
"createInternalResourceDialogCreateResource": "Create Resource",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "International Domain Detected",
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
"willbestoredas": "Will be stored as:"
|
"willbestoredas": "Will be stored as:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Custom Headers",
|
||||||
|
"headersValidationError": "Headers must be in the format: Header-Name: value.",
|
||||||
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
|
"domainPickerVerified": "Verified",
|
||||||
|
"domainPickerUnverified": "Unverified",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||||
|
"domainPickerError": "Error",
|
||||||
|
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||||
|
"domainPickerErrorCheckAvailability": "Failed to check domain availability",
|
||||||
|
"domainPickerInvalidSubdomain": "Invalid subdomain",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "The input \"{sub}\" was removed because it's not valid.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Edit file: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Edit file: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Ocurrió un error mientras se añadía el usuario al rol.",
|
"accessRoleErrorAddDescription": "Ocurrió un error mientras se añadía el usuario al rol.",
|
||||||
"userSaved": "Usuario guardado",
|
"userSaved": "Usuario guardado",
|
||||||
"userSavedDescription": "El usuario ha sido actualizado.",
|
"userSavedDescription": "El usuario ha sido actualizado.",
|
||||||
|
"autoProvisioned": "Auto asegurado",
|
||||||
|
"autoProvisionedDescription": "Permitir a este usuario ser administrado automáticamente por el proveedor de identidad",
|
||||||
"accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización",
|
"accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización",
|
||||||
"accessControlsSubmit": "Guardar controles de acceso",
|
"accessControlsSubmit": "Guardar controles de acceso",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Formato de dirección IP inválido",
|
"ipAddressErrorInvalidFormat": "Formato de dirección IP inválido",
|
||||||
"ipAddressErrorInvalidOctet": "Octet de dirección IP no válido",
|
"ipAddressErrorInvalidOctet": "Octet de dirección IP no válido",
|
||||||
"path": "Ruta",
|
"path": "Ruta",
|
||||||
|
"matchPath": "Coincidir ruta",
|
||||||
"ipAddressRange": "Rango IP",
|
"ipAddressRange": "Rango IP",
|
||||||
"rulesErrorFetch": "Error al obtener las reglas",
|
"rulesErrorFetch": "Error al obtener las reglas",
|
||||||
"rulesErrorFetchDescription": "Se ha producido un error al recuperar las reglas",
|
"rulesErrorFetchDescription": "Se ha producido un error al recuperar las reglas",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Conectado",
|
"idpConnectingToFinished": "Conectado",
|
||||||
"idpErrorConnectingTo": "Hubo un problema al conectar con {name}. Por favor, póngase en contacto con su administrador.",
|
"idpErrorConnectingTo": "Hubo un problema al conectar con {name}. Por favor, póngase en contacto con su administrador.",
|
||||||
"idpErrorNotFound": "IdP no encontrado",
|
"idpErrorNotFound": "IdP no encontrado",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invitación inválida",
|
"inviteInvalid": "Invitación inválida",
|
||||||
"inviteInvalidDescription": "El enlace de invitación no es válido.",
|
"inviteInvalidDescription": "El enlace de invitación no es válido.",
|
||||||
"inviteErrorWrongUser": "La invitación no es para este usuario",
|
"inviteErrorWrongUser": "La invitación no es para este usuario",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Edición Profesional requerida",
|
"licenseTierProfessionalRequired": "Edición Profesional requerida",
|
||||||
"licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.",
|
"licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.",
|
||||||
"actionGetOrg": "Obtener organización",
|
"actionGetOrg": "Obtener organización",
|
||||||
|
"updateOrgUser": "Actualizar usuario Org",
|
||||||
|
"createOrgUser": "Crear usuario Org",
|
||||||
"actionUpdateOrg": "Actualizar organización",
|
"actionUpdateOrg": "Actualizar organización",
|
||||||
"actionUpdateUser": "Actualizar usuario",
|
"actionUpdateUser": "Actualizar usuario",
|
||||||
"actionGetUser": "Obtener usuario",
|
"actionGetUser": "Obtener usuario",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Eliminar sitio",
|
"actionDeleteSite": "Eliminar sitio",
|
||||||
"actionGetSite": "Obtener sitio",
|
"actionGetSite": "Obtener sitio",
|
||||||
"actionListSites": "Listar sitios",
|
"actionListSites": "Listar sitios",
|
||||||
|
"actionApplyBlueprint": "Aplicar plano",
|
||||||
"setupToken": "Configuración de token",
|
"setupToken": "Configuración de token",
|
||||||
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
|
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
|
||||||
"setupTokenRequired": "Se requiere el token de configuración",
|
"setupTokenRequired": "Se requiere el token de configuración",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Licencia",
|
"sidebarLicense": "Licencia",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clientes (Beta)",
|
||||||
"sidebarDomains": "Dominios",
|
"sidebarDomains": "Dominios",
|
||||||
"enableDockerSocket": "Habilitar conector Docker",
|
"enableDockerSocket": "Habilitar Plano Docker",
|
||||||
"enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.",
|
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
|
||||||
"enableDockerSocketLink": "Saber más",
|
"enableDockerSocketLink": "Saber más",
|
||||||
"viewDockerContainers": "Ver contenedores Docker",
|
"viewDockerContainers": "Ver contenedores Docker",
|
||||||
"containersIn": "Contenedores en {siteName}",
|
"containersIn": "Contenedores en {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Nueva actualización disponible",
|
"newtUpdateAvailable": "Nueva actualización disponible",
|
||||||
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
|
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
|
||||||
"domainPickerEnterDomain": "Dominio",
|
"domainPickerEnterDomain": "Dominio",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp",
|
"domainPickerPlaceholder": "miapp.ejemplo.com",
|
||||||
"domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.",
|
"domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.",
|
||||||
"domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles",
|
"domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles",
|
||||||
"domainPickerTabAll": "Todo",
|
"domainPickerTabAll": "Todo",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocolo",
|
"editInternalResourceDialogProtocol": "Protocolo",
|
||||||
"editInternalResourceDialogSitePort": "Puerto del sitio",
|
"editInternalResourceDialogSitePort": "Puerto del sitio",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
"editInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
||||||
"editInternalResourceDialogDestinationIP": "IP de destino",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Puerto de destino",
|
|
||||||
"editInternalResourceDialogCancel": "Cancelar",
|
"editInternalResourceDialogCancel": "Cancelar",
|
||||||
"editInternalResourceDialogSaveResource": "Guardar recurso",
|
"editInternalResourceDialogSaveResource": "Guardar recurso",
|
||||||
"editInternalResourceDialogSuccess": "Éxito",
|
"editInternalResourceDialogSuccess": "Éxito",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Puerto del sitio",
|
"createInternalResourceDialogSitePort": "Puerto del sitio",
|
||||||
"createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.",
|
"createInternalResourceDialogSitePortDescription": "Use este puerto para acceder al recurso en el sitio cuando se conecta con un cliente.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
"createInternalResourceDialogTargetConfiguration": "Configuración de objetivos",
|
||||||
"createInternalResourceDialogDestinationIP": "IP de destino",
|
"createInternalResourceDialogDestinationIPDescription": "La dirección IP o nombre de host del recurso en la red del sitio.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "La dirección IP del recurso en la red del sitio.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Puerto de destino",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.",
|
"createInternalResourceDialogDestinationPortDescription": "El puerto en la IP de destino donde el recurso es accesible.",
|
||||||
"createInternalResourceDialogCancel": "Cancelar",
|
"createInternalResourceDialogCancel": "Cancelar",
|
||||||
"createInternalResourceDialogCreateResource": "Crear recurso",
|
"createInternalResourceDialogCreateResource": "Crear recurso",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Convierte este nodo a autoalojado administrado"
|
"convertButton": "Convierte este nodo a autoalojado administrado"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Dominio Internacional detectado",
|
"internationaldomaindetected": "Dominio Internacional detectado",
|
||||||
"willbestoredas": "Se almacenará como:"
|
"willbestoredas": "Se almacenará como:",
|
||||||
|
"idpGoogleDescription": "Proveedor OAuth2/OIDC de Google",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Cabeceras personalizadas",
|
||||||
|
"headersValidationError": "Los encabezados deben estar en el formato: Nombre de cabecera: valor.",
|
||||||
|
"domainPickerProvidedDomain": "Dominio proporcionado",
|
||||||
|
"domainPickerFreeProvidedDomain": "Dominio proporcionado gratis",
|
||||||
|
"domainPickerVerified": "Verificado",
|
||||||
|
"domainPickerUnverified": "Sin verificar",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Este subdominio contiene caracteres o estructura no válidos. Se limpiará automáticamente al guardar.",
|
||||||
|
"domainPickerError": "Error",
|
||||||
|
"domainPickerErrorLoadDomains": "Error al cargar los dominios de la organización",
|
||||||
|
"domainPickerErrorCheckAvailability": "No se pudo comprobar la disponibilidad del dominio",
|
||||||
|
"domainPickerInvalidSubdomain": "Subdominio inválido",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "La entrada \"{sub}\" fue eliminada porque no es válida.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "No se ha podido hacer válido \"{sub}\" para {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdominio saneado",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" fue corregido a \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Editar archivo: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Editar archivo: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
||||||
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
||||||
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
||||||
"welcome": "Bienvenue à Pangolin",
|
"welcome": "Bienvenue sur Pangolin",
|
||||||
"welcomeTo": "Bienvenue chez",
|
"welcomeTo": "Bienvenue chez",
|
||||||
"componentsCreateOrg": "Créer une organisation",
|
"componentsCreateOrg": "Créer une organisation",
|
||||||
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
||||||
|
@ -34,13 +34,13 @@
|
||||||
"confirmPassword": "Confirmer le mot de passe",
|
"confirmPassword": "Confirmer le mot de passe",
|
||||||
"createAccount": "Créer un compte",
|
"createAccount": "Créer un compte",
|
||||||
"viewSettings": "Afficher les paramètres",
|
"viewSettings": "Afficher les paramètres",
|
||||||
"delete": "Supprimez",
|
"delete": "Supprimer",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"online": "En ligne",
|
"online": "En ligne",
|
||||||
"offline": "Hors ligne",
|
"offline": "Hors ligne",
|
||||||
"site": "Site",
|
"site": "Site",
|
||||||
"dataIn": "Données dans",
|
"dataIn": "Données reçues",
|
||||||
"dataOut": "Données épuisées",
|
"dataOut": "Données envoyées",
|
||||||
"connectionType": "Type de connexion",
|
"connectionType": "Type de connexion",
|
||||||
"tunnelType": "Type de tunnel",
|
"tunnelType": "Type de tunnel",
|
||||||
"local": "Locale",
|
"local": "Locale",
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
"resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS",
|
"resourceHTTPSSettingsDescription": "Configurer comment votre ressource sera accédée via HTTPS",
|
||||||
"domainType": "Type de domaine",
|
"domainType": "Type de domaine",
|
||||||
"subdomain": "Sous-domaine",
|
"subdomain": "Sous-domaine",
|
||||||
"baseDomain": "Domaine de base",
|
"baseDomain": "Domaine racine",
|
||||||
"subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.",
|
"subdomnainDescription": "Le sous-domaine où votre ressource sera accessible.",
|
||||||
"resourceRawSettings": "Paramètres TCP/UDP",
|
"resourceRawSettings": "Paramètres TCP/UDP",
|
||||||
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP",
|
"resourceRawSettingsDescription": "Configurer comment votre ressource sera accédée via TCP/UDP",
|
||||||
|
@ -309,7 +309,7 @@
|
||||||
"numberOfSites": "Nombre de sites",
|
"numberOfSites": "Nombre de sites",
|
||||||
"licenseKeySearch": "Rechercher des clés de licence...",
|
"licenseKeySearch": "Rechercher des clés de licence...",
|
||||||
"licenseKeyAdd": "Ajouter une clé de licence",
|
"licenseKeyAdd": "Ajouter une clé de licence",
|
||||||
"type": "Type de texte",
|
"type": "Type",
|
||||||
"licenseKeyRequired": "La clé de licence est requise",
|
"licenseKeyRequired": "La clé de licence est requise",
|
||||||
"licenseTermsAgree": "Vous devez accepter les conditions de licence",
|
"licenseTermsAgree": "Vous devez accepter les conditions de licence",
|
||||||
"licenseErrorKeyLoad": "Impossible de charger les clés de licence",
|
"licenseErrorKeyLoad": "Impossible de charger les clés de licence",
|
||||||
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Une erreur s'est produite lors de l'ajout de l'utilisateur au rôle.",
|
"accessRoleErrorAddDescription": "Une erreur s'est produite lors de l'ajout de l'utilisateur au rôle.",
|
||||||
"userSaved": "Utilisateur enregistré",
|
"userSaved": "Utilisateur enregistré",
|
||||||
"userSavedDescription": "L'utilisateur a été mis à jour.",
|
"userSavedDescription": "L'utilisateur a été mis à jour.",
|
||||||
|
"autoProvisioned": "Auto-provisionné",
|
||||||
|
"autoProvisionedDescription": "Permettre à cet utilisateur d'être géré automatiquement par le fournisseur d'identité",
|
||||||
"accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation",
|
"accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation",
|
||||||
"accessControlsSubmit": "Enregistrer les contrôles d'accès",
|
"accessControlsSubmit": "Enregistrer les contrôles d'accès",
|
||||||
"roles": "Rôles",
|
"roles": "Rôles",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Format d'adresse IP invalide",
|
"ipAddressErrorInvalidFormat": "Format d'adresse IP invalide",
|
||||||
"ipAddressErrorInvalidOctet": "Octet d'adresse IP invalide",
|
"ipAddressErrorInvalidOctet": "Octet d'adresse IP invalide",
|
||||||
"path": "Chemin",
|
"path": "Chemin",
|
||||||
|
"matchPath": "Chemin de correspondance",
|
||||||
"ipAddressRange": "Plage IP",
|
"ipAddressRange": "Plage IP",
|
||||||
"rulesErrorFetch": "Échec de la récupération des règles",
|
"rulesErrorFetch": "Échec de la récupération des règles",
|
||||||
"rulesErrorFetchDescription": "Une erreur s'est produite lors de la récupération des règles",
|
"rulesErrorFetchDescription": "Une erreur s'est produite lors de la récupération des règles",
|
||||||
|
@ -595,7 +598,7 @@
|
||||||
"newtId": "ID Newt",
|
"newtId": "ID Newt",
|
||||||
"newtSecretKey": "Clé secrète Newt",
|
"newtSecretKey": "Clé secrète Newt",
|
||||||
"architecture": "Architecture",
|
"architecture": "Architecture",
|
||||||
"sites": "Espaces",
|
"sites": "Sites",
|
||||||
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
|
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
|
||||||
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
|
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
|
||||||
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
|
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Connecté",
|
"idpConnectingToFinished": "Connecté",
|
||||||
"idpErrorConnectingTo": "Un problème est survenu lors de la connexion à {name}. Veuillez contacter votre administrateur.",
|
"idpErrorConnectingTo": "Un problème est survenu lors de la connexion à {name}. Veuillez contacter votre administrateur.",
|
||||||
"idpErrorNotFound": "IdP introuvable",
|
"idpErrorNotFound": "IdP introuvable",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invitation invalide",
|
"inviteInvalid": "Invitation invalide",
|
||||||
"inviteInvalidDescription": "Le lien d'invitation n'est pas valide.",
|
"inviteInvalidDescription": "Le lien d'invitation n'est pas valide.",
|
||||||
"inviteErrorWrongUser": "L'invitation n'est pas pour cet utilisateur",
|
"inviteErrorWrongUser": "L'invitation n'est pas pour cet utilisateur",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Édition Professionnelle Requise",
|
"licenseTierProfessionalRequired": "Édition Professionnelle Requise",
|
||||||
"licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.",
|
"licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.",
|
||||||
"actionGetOrg": "Obtenir l'organisation",
|
"actionGetOrg": "Obtenir l'organisation",
|
||||||
|
"updateOrgUser": "Mise à jour de l'utilisateur Org",
|
||||||
|
"createOrgUser": "Créer un utilisateur Org",
|
||||||
"actionUpdateOrg": "Mettre à jour l'organisation",
|
"actionUpdateOrg": "Mettre à jour l'organisation",
|
||||||
"actionUpdateUser": "Mettre à jour l'utilisateur",
|
"actionUpdateUser": "Mettre à jour l'utilisateur",
|
||||||
"actionGetUser": "Obtenir l'utilisateur",
|
"actionGetUser": "Obtenir l'utilisateur",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Supprimer un site",
|
"actionDeleteSite": "Supprimer un site",
|
||||||
"actionGetSite": "Obtenir un site",
|
"actionGetSite": "Obtenir un site",
|
||||||
"actionListSites": "Lister les sites",
|
"actionListSites": "Lister les sites",
|
||||||
|
"actionApplyBlueprint": "Appliquer le Plan",
|
||||||
"setupToken": "Jeton de configuration",
|
"setupToken": "Jeton de configuration",
|
||||||
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
|
||||||
"setupTokenRequired": "Le jeton de configuration est requis.",
|
"setupTokenRequired": "Le jeton de configuration est requis.",
|
||||||
|
@ -1120,7 +1128,7 @@
|
||||||
"sidebarOverview": "Aperçu",
|
"sidebarOverview": "Aperçu",
|
||||||
"sidebarHome": "Domicile",
|
"sidebarHome": "Domicile",
|
||||||
"sidebarSites": "Espaces",
|
"sidebarSites": "Espaces",
|
||||||
"sidebarResources": "Ressource",
|
"sidebarResources": "Ressources",
|
||||||
"sidebarAccessControl": "Contrôle d'accès",
|
"sidebarAccessControl": "Contrôle d'accès",
|
||||||
"sidebarUsers": "Utilisateurs",
|
"sidebarUsers": "Utilisateurs",
|
||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Licence",
|
"sidebarLicense": "Licence",
|
||||||
"sidebarClients": "Clients (Bêta)",
|
"sidebarClients": "Clients (Bêta)",
|
||||||
"sidebarDomains": "Domaines",
|
"sidebarDomains": "Domaines",
|
||||||
"enableDockerSocket": "Activer Docker Socket",
|
"enableDockerSocket": "Activer le Plan Docker",
|
||||||
"enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.",
|
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
||||||
"enableDockerSocketLink": "En savoir plus",
|
"enableDockerSocketLink": "En savoir plus",
|
||||||
"viewDockerContainers": "Voir les conteneurs Docker",
|
"viewDockerContainers": "Voir les conteneurs Docker",
|
||||||
"containersIn": "Conteneurs en {siteName}",
|
"containersIn": "Conteneurs en {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Mise à jour disponible",
|
"newtUpdateAvailable": "Mise à jour disponible",
|
||||||
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
|
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
|
||||||
"domainPickerEnterDomain": "Domaine",
|
"domainPickerEnterDomain": "Domaine",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp",
|
"domainPickerPlaceholder": "monapp.exemple.com",
|
||||||
"domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.",
|
"domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.",
|
||||||
"domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles",
|
"domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles",
|
||||||
"domainPickerTabAll": "Tous",
|
"domainPickerTabAll": "Tous",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocole",
|
"editInternalResourceDialogProtocol": "Protocole",
|
||||||
"editInternalResourceDialogSitePort": "Port du site",
|
"editInternalResourceDialogSitePort": "Port du site",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
"editInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
||||||
"editInternalResourceDialogDestinationIP": "IP de destination",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Port de destination",
|
|
||||||
"editInternalResourceDialogCancel": "Abandonner",
|
"editInternalResourceDialogCancel": "Abandonner",
|
||||||
"editInternalResourceDialogSaveResource": "Enregistrer la ressource",
|
"editInternalResourceDialogSaveResource": "Enregistrer la ressource",
|
||||||
"editInternalResourceDialogSuccess": "Succès",
|
"editInternalResourceDialogSuccess": "Succès",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Port du site",
|
"createInternalResourceDialogSitePort": "Port du site",
|
||||||
"createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.",
|
"createInternalResourceDialogSitePortDescription": "Utilisez ce port pour accéder à la ressource sur le site lors de la connexion avec un client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
"createInternalResourceDialogTargetConfiguration": "Configuration de la cible",
|
||||||
"createInternalResourceDialogDestinationIP": "IP de destination",
|
"createInternalResourceDialogDestinationIPDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "L'adresse IP de la ressource sur le réseau du site.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Port de destination",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.",
|
"createInternalResourceDialogDestinationPortDescription": "Le port sur l'IP de destination où la ressource est accessible.",
|
||||||
"createInternalResourceDialogCancel": "Abandonner",
|
"createInternalResourceDialogCancel": "Abandonner",
|
||||||
"createInternalResourceDialogCreateResource": "Créer une ressource",
|
"createInternalResourceDialogCreateResource": "Créer une ressource",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Convertir ce noeud en auto-hébergé géré"
|
"convertButton": "Convertir ce noeud en auto-hébergé géré"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Domaine international détecté",
|
"internationaldomaindetected": "Domaine international détecté",
|
||||||
"willbestoredas": "Sera stocké comme :"
|
"willbestoredas": "Sera stocké comme :",
|
||||||
|
"idpGoogleDescription": "Fournisseur Google OAuth2/OIDC",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "En-têtes personnalisés",
|
||||||
|
"headersValidationError": "Les entêtes doivent être au format : Header-Name: valeur.",
|
||||||
|
"domainPickerProvidedDomain": "Domaine fourni",
|
||||||
|
"domainPickerFreeProvidedDomain": "Domaine fourni gratuitement",
|
||||||
|
"domainPickerVerified": "Vérifié",
|
||||||
|
"domainPickerUnverified": "Non vérifié",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Ce sous-domaine contient des caractères ou une structure non valide. Il sera automatiquement nettoyé lorsque vous enregistrez.",
|
||||||
|
"domainPickerError": "Erreur",
|
||||||
|
"domainPickerErrorLoadDomains": "Impossible de charger les domaines de l'organisation",
|
||||||
|
"domainPickerErrorCheckAvailability": "Impossible de vérifier la disponibilité du domaine",
|
||||||
|
"domainPickerInvalidSubdomain": "Sous-domaine invalide",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "L'entrée \"{sub}\" a été supprimée car elle n'est pas valide.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "La «{sub}» n'a pas pu être validée pour {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Sous-domaine nettoyé",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" a été corrigé à \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Modifier le fichier : config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Modifier le fichier : docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Si è verificato un errore durante l'aggiunta dell'utente al ruolo.",
|
"accessRoleErrorAddDescription": "Si è verificato un errore durante l'aggiunta dell'utente al ruolo.",
|
||||||
"userSaved": "Utente salvato",
|
"userSaved": "Utente salvato",
|
||||||
"userSavedDescription": "L'utente è stato aggiornato.",
|
"userSavedDescription": "L'utente è stato aggiornato.",
|
||||||
|
"autoProvisioned": "Auto Provisioned",
|
||||||
|
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
|
||||||
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
|
||||||
"accessControlsSubmit": "Salva Controlli di Accesso",
|
"accessControlsSubmit": "Salva Controlli di Accesso",
|
||||||
"roles": "Ruoli",
|
"roles": "Ruoli",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Formato indirizzo IP non valido",
|
"ipAddressErrorInvalidFormat": "Formato indirizzo IP non valido",
|
||||||
"ipAddressErrorInvalidOctet": "Ottetto indirizzo IP non valido",
|
"ipAddressErrorInvalidOctet": "Ottetto indirizzo IP non valido",
|
||||||
"path": "Percorso",
|
"path": "Percorso",
|
||||||
|
"matchPath": "Corrispondenza Tracciato",
|
||||||
"ipAddressRange": "Intervallo IP",
|
"ipAddressRange": "Intervallo IP",
|
||||||
"rulesErrorFetch": "Impossibile recuperare le regole",
|
"rulesErrorFetch": "Impossibile recuperare le regole",
|
||||||
"rulesErrorFetchDescription": "Si è verificato un errore durante il recupero delle regole",
|
"rulesErrorFetchDescription": "Si è verificato un errore durante il recupero delle regole",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Connesso",
|
"idpConnectingToFinished": "Connesso",
|
||||||
"idpErrorConnectingTo": "Si è verificato un problema durante la connessione a {name}. Contatta il tuo amministratore.",
|
"idpErrorConnectingTo": "Si è verificato un problema durante la connessione a {name}. Contatta il tuo amministratore.",
|
||||||
"idpErrorNotFound": "IdP non trovato",
|
"idpErrorNotFound": "IdP non trovato",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Invito Non Valido",
|
"inviteInvalid": "Invito Non Valido",
|
||||||
"inviteInvalidDescription": "Il link di invito non è valido.",
|
"inviteInvalidDescription": "Il link di invito non è valido.",
|
||||||
"inviteErrorWrongUser": "L'invito non è per questo utente",
|
"inviteErrorWrongUser": "L'invito non è per questo utente",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Edizione Professional Richiesta",
|
"licenseTierProfessionalRequired": "Edizione Professional Richiesta",
|
||||||
"licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.",
|
"licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.",
|
||||||
"actionGetOrg": "Ottieni Organizzazione",
|
"actionGetOrg": "Ottieni Organizzazione",
|
||||||
|
"updateOrgUser": "Aggiorna Utente Org",
|
||||||
|
"createOrgUser": "Crea Utente Org",
|
||||||
"actionUpdateOrg": "Aggiorna Organizzazione",
|
"actionUpdateOrg": "Aggiorna Organizzazione",
|
||||||
"actionUpdateUser": "Aggiorna Utente",
|
"actionUpdateUser": "Aggiorna Utente",
|
||||||
"actionGetUser": "Ottieni Utente",
|
"actionGetUser": "Ottieni Utente",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Elimina Sito",
|
"actionDeleteSite": "Elimina Sito",
|
||||||
"actionGetSite": "Ottieni Sito",
|
"actionGetSite": "Ottieni Sito",
|
||||||
"actionListSites": "Elenca Siti",
|
"actionListSites": "Elenca Siti",
|
||||||
|
"actionApplyBlueprint": "Applica Progetto",
|
||||||
"setupToken": "Configura Token",
|
"setupToken": "Configura Token",
|
||||||
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
|
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
|
||||||
"setupTokenRequired": "Il token di configurazione è richiesto",
|
"setupTokenRequired": "Il token di configurazione è richiesto",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Licenza",
|
"sidebarLicense": "Licenza",
|
||||||
"sidebarClients": "Clienti (Beta)",
|
"sidebarClients": "Clienti (Beta)",
|
||||||
"sidebarDomains": "Domini",
|
"sidebarDomains": "Domini",
|
||||||
"enableDockerSocket": "Abilita Docker Socket",
|
"enableDockerSocket": "Abilita Progetto Docker",
|
||||||
"enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.",
|
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
|
||||||
"enableDockerSocketLink": "Scopri di più",
|
"enableDockerSocketLink": "Scopri di più",
|
||||||
"viewDockerContainers": "Visualizza Contenitori Docker",
|
"viewDockerContainers": "Visualizza Contenitori Docker",
|
||||||
"containersIn": "Contenitori in {siteName}",
|
"containersIn": "Contenitori in {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Aggiornamento Disponibile",
|
"newtUpdateAvailable": "Aggiornamento Disponibile",
|
||||||
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
|
||||||
"domainPickerEnterDomain": "Dominio",
|
"domainPickerEnterDomain": "Dominio",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.",
|
"domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.",
|
||||||
"domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili",
|
"domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili",
|
||||||
"domainPickerTabAll": "Tutti",
|
"domainPickerTabAll": "Tutti",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocollo",
|
"editInternalResourceDialogProtocol": "Protocollo",
|
||||||
"editInternalResourceDialogSitePort": "Porta del Sito",
|
"editInternalResourceDialogSitePort": "Porta del Sito",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
"editInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
||||||
"editInternalResourceDialogDestinationIP": "IP di Destinazione",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Porta di Destinazione",
|
|
||||||
"editInternalResourceDialogCancel": "Annulla",
|
"editInternalResourceDialogCancel": "Annulla",
|
||||||
"editInternalResourceDialogSaveResource": "Salva Risorsa",
|
"editInternalResourceDialogSaveResource": "Salva Risorsa",
|
||||||
"editInternalResourceDialogSuccess": "Successo",
|
"editInternalResourceDialogSuccess": "Successo",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Porta del Sito",
|
"createInternalResourceDialogSitePort": "Porta del Sito",
|
||||||
"createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.",
|
"createInternalResourceDialogSitePortDescription": "Usa questa porta per accedere alla risorsa nel sito quando sei connesso con un client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
"createInternalResourceDialogTargetConfiguration": "Configurazione Target",
|
||||||
"createInternalResourceDialogDestinationIP": "IP di Destinazione",
|
"createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP o hostname della risorsa nella rete del sito.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP della risorsa sulla rete del sito.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Porta di Destinazione",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.",
|
"createInternalResourceDialogDestinationPortDescription": "La porta sull'IP di destinazione dove la risorsa è accessibile.",
|
||||||
"createInternalResourceDialogCancel": "Annulla",
|
"createInternalResourceDialogCancel": "Annulla",
|
||||||
"createInternalResourceDialogCreateResource": "Crea Risorsa",
|
"createInternalResourceDialogCreateResource": "Crea Risorsa",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Converti questo nodo in auto-ospitato gestito"
|
"convertButton": "Converti questo nodo in auto-ospitato gestito"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Dominio Internazionale Rilevato",
|
"internationaldomaindetected": "Dominio Internazionale Rilevato",
|
||||||
"willbestoredas": "Verrà conservato come:"
|
"willbestoredas": "Verrà conservato come:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Intestazioni Personalizzate",
|
||||||
|
"headersValidationError": "Le intestazioni devono essere nel formato: Intestazione-Nome: valore.",
|
||||||
|
"domainPickerProvidedDomain": "Dominio Fornito",
|
||||||
|
"domainPickerFreeProvidedDomain": "Dominio Fornito Gratuito",
|
||||||
|
"domainPickerVerified": "Verificato",
|
||||||
|
"domainPickerUnverified": "Non Verificato",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Questo sottodominio contiene caratteri o struttura non validi. Sarà sanificato automaticamente quando si salva.",
|
||||||
|
"domainPickerError": "Errore",
|
||||||
|
"domainPickerErrorLoadDomains": "Impossibile caricare i domini dell'organizzazione",
|
||||||
|
"domainPickerErrorCheckAvailability": "Impossibile verificare la disponibilità del dominio",
|
||||||
|
"domainPickerInvalidSubdomain": "Sottodominio non valido",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "L'input \"{sub}\" è stato rimosso perché non è valido.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" non può essere reso valido per {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Sottodominio igienizzato",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" è stato corretto in \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Modifica file: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Modifica file: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
||||||
"userSaved": "사용자 저장됨",
|
"userSaved": "사용자 저장됨",
|
||||||
"userSavedDescription": "사용자가 업데이트되었습니다.",
|
"userSavedDescription": "사용자가 업데이트되었습니다.",
|
||||||
|
"autoProvisioned": "자동 프로비저닝됨",
|
||||||
|
"autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다",
|
||||||
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
|
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
|
||||||
"accessControlsSubmit": "접근 제어 저장",
|
"accessControlsSubmit": "접근 제어 저장",
|
||||||
"roles": "역할",
|
"roles": "역할",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식",
|
"ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식",
|
||||||
"ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟",
|
"ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟",
|
||||||
"path": "경로",
|
"path": "경로",
|
||||||
|
"matchPath": "경로 맞춤",
|
||||||
"ipAddressRange": "IP 범위",
|
"ipAddressRange": "IP 범위",
|
||||||
"rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.",
|
"rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.",
|
||||||
"rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다",
|
"rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "연결됨",
|
"idpConnectingToFinished": "연결됨",
|
||||||
"idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.",
|
"idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.",
|
||||||
"idpErrorNotFound": "IdP를 찾을 수 없습니다.",
|
"idpErrorNotFound": "IdP를 찾을 수 없습니다.",
|
||||||
|
"idpGoogleAlt": "구글",
|
||||||
|
"idpAzureAlt": "애저",
|
||||||
"inviteInvalid": "유효하지 않은 초대",
|
"inviteInvalid": "유효하지 않은 초대",
|
||||||
"inviteInvalidDescription": "초대 링크가 유효하지 않습니다.",
|
"inviteInvalidDescription": "초대 링크가 유효하지 않습니다.",
|
||||||
"inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다",
|
"inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
|
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
|
||||||
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
|
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
|
||||||
"actionGetOrg": "조직 가져오기",
|
"actionGetOrg": "조직 가져오기",
|
||||||
|
"updateOrgUser": "조직 사용자 업데이트",
|
||||||
|
"createOrgUser": "조직 사용자 생성",
|
||||||
"actionUpdateOrg": "조직 업데이트",
|
"actionUpdateOrg": "조직 업데이트",
|
||||||
"actionUpdateUser": "사용자 업데이트",
|
"actionUpdateUser": "사용자 업데이트",
|
||||||
"actionGetUser": "사용자 조회",
|
"actionGetUser": "사용자 조회",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "사이트 삭제",
|
"actionDeleteSite": "사이트 삭제",
|
||||||
"actionGetSite": "사이트 가져오기",
|
"actionGetSite": "사이트 가져오기",
|
||||||
"actionListSites": "사이트 목록",
|
"actionListSites": "사이트 목록",
|
||||||
|
"actionApplyBlueprint": "청사진 적용",
|
||||||
"setupToken": "설정 토큰",
|
"setupToken": "설정 토큰",
|
||||||
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||||
"setupTokenRequired": "설정 토큰이 필요합니다",
|
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "라이선스",
|
"sidebarLicense": "라이선스",
|
||||||
"sidebarClients": "클라이언트 (Beta)",
|
"sidebarClients": "클라이언트 (Beta)",
|
||||||
"sidebarDomains": "도메인",
|
"sidebarDomains": "도메인",
|
||||||
"enableDockerSocket": "Docker 소켓 활성화",
|
"enableDockerSocket": "Docker 청사진 활성화",
|
||||||
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
|
||||||
"enableDockerSocketLink": "자세히 알아보기",
|
"enableDockerSocketLink": "자세히 알아보기",
|
||||||
"viewDockerContainers": "도커 컨테이너 보기",
|
"viewDockerContainers": "도커 컨테이너 보기",
|
||||||
"containersIn": "{siteName}의 컨테이너",
|
"containersIn": "{siteName}의 컨테이너",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "업데이트 가능",
|
"newtUpdateAvailable": "업데이트 가능",
|
||||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||||
"domainPickerEnterDomain": "도메인",
|
"domainPickerEnterDomain": "도메인",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerTabAll": "모두",
|
"domainPickerTabAll": "모두",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "프로토콜",
|
"editInternalResourceDialogProtocol": "프로토콜",
|
||||||
"editInternalResourceDialogSitePort": "사이트 포트",
|
"editInternalResourceDialogSitePort": "사이트 포트",
|
||||||
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
||||||
"editInternalResourceDialogDestinationIP": "대상 IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "대상 IP의 포트",
|
|
||||||
"editInternalResourceDialogCancel": "취소",
|
"editInternalResourceDialogCancel": "취소",
|
||||||
"editInternalResourceDialogSaveResource": "리소스 저장",
|
"editInternalResourceDialogSaveResource": "리소스 저장",
|
||||||
"editInternalResourceDialogSuccess": "성공",
|
"editInternalResourceDialogSuccess": "성공",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "사이트 포트",
|
"createInternalResourceDialogSitePort": "사이트 포트",
|
||||||
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
||||||
"createInternalResourceDialogDestinationIP": "대상 IP",
|
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "대상 포트",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
||||||
"createInternalResourceDialogCancel": "취소",
|
"createInternalResourceDialogCancel": "취소",
|
||||||
"createInternalResourceDialogCreateResource": "리소스 생성",
|
"createInternalResourceDialogCreateResource": "리소스 생성",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "이 노드를 관리 자체 호스팅으로 변환"
|
"convertButton": "이 노드를 관리 자체 호스팅으로 변환"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "국제 도메인 감지됨",
|
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||||
"willbestoredas": "다음으로 저장됩니다:"
|
"willbestoredas": "다음으로 저장됩니다:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC 공급자",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 공급자",
|
||||||
|
"customHeaders": "사용자 정의 헤더",
|
||||||
|
"headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.",
|
||||||
|
"domainPickerProvidedDomain": "제공된 도메인",
|
||||||
|
"domainPickerFreeProvidedDomain": "무료 제공된 도메인",
|
||||||
|
"domainPickerVerified": "검증됨",
|
||||||
|
"domainPickerUnverified": "검증되지 않음",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "이 하위 도메인은 잘못된 문자 또는 구조를 포함하고 있습니다. 저장 시 자동으로 정리됩니다.",
|
||||||
|
"domainPickerError": "오류",
|
||||||
|
"domainPickerErrorLoadDomains": "조직 도메인 로드 실패",
|
||||||
|
"domainPickerErrorCheckAvailability": "도메인 가용성 확인 실패",
|
||||||
|
"domainPickerInvalidSubdomain": "잘못된 하위 도메인",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "입력 \"{sub}\"이(가) 유효하지 않으므로 제거되었습니다.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\"을(를) {domain}에 대해 유효하게 만들 수 없습니다.",
|
||||||
|
"domainPickerSubdomainSanitized": "하위 도메인 정리됨",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\"이(가) \"{sanitized}\"로 수정되었습니다",
|
||||||
|
"resourceAddEntrypointsEditFile": "파일 편집: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "파일 편집: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.",
|
"accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.",
|
||||||
"userSaved": "Bruker lagret",
|
"userSaved": "Bruker lagret",
|
||||||
"userSavedDescription": "Brukeren har blitt oppdatert.",
|
"userSavedDescription": "Brukeren har blitt oppdatert.",
|
||||||
|
"autoProvisioned": "Auto avlyst",
|
||||||
|
"autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør",
|
||||||
"accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen",
|
"accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen",
|
||||||
"accessControlsSubmit": "Lagre tilgangskontroller",
|
"accessControlsSubmit": "Lagre tilgangskontroller",
|
||||||
"roles": "Roller",
|
"roles": "Roller",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat",
|
"ipAddressErrorInvalidFormat": "Ugyldig IP-adresseformat",
|
||||||
"ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet",
|
"ipAddressErrorInvalidOctet": "Ugyldig IP-adresse-oktet",
|
||||||
"path": "Sti",
|
"path": "Sti",
|
||||||
|
"matchPath": "Match sti",
|
||||||
"ipAddressRange": "IP-område",
|
"ipAddressRange": "IP-område",
|
||||||
"rulesErrorFetch": "Klarte ikke å hente regler",
|
"rulesErrorFetch": "Klarte ikke å hente regler",
|
||||||
"rulesErrorFetchDescription": "Det oppsto en feil under henting av regler",
|
"rulesErrorFetchDescription": "Det oppsto en feil under henting av regler",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Tilkoblet",
|
"idpConnectingToFinished": "Tilkoblet",
|
||||||
"idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.",
|
"idpErrorConnectingTo": "Det oppstod et problem med å koble til {name}. Vennligst kontakt din administrator.",
|
||||||
"idpErrorNotFound": "IdP ikke funnet",
|
"idpErrorNotFound": "IdP ikke funnet",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Ugyldig invitasjon",
|
"inviteInvalid": "Ugyldig invitasjon",
|
||||||
"inviteInvalidDescription": "Invitasjonslenken er ugyldig.",
|
"inviteInvalidDescription": "Invitasjonslenken er ugyldig.",
|
||||||
"inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren",
|
"inviteErrorWrongUser": "Invitasjonen er ikke for denne brukeren",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Profesjonell utgave påkrevd",
|
"licenseTierProfessionalRequired": "Profesjonell utgave påkrevd",
|
||||||
"licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.",
|
"licenseTierProfessionalRequiredDescription": "Denne funksjonen er kun tilgjengelig i den profesjonelle utgaven.",
|
||||||
"actionGetOrg": "Hent organisasjon",
|
"actionGetOrg": "Hent organisasjon",
|
||||||
|
"updateOrgUser": "Oppdater org.bruker",
|
||||||
|
"createOrgUser": "Opprett Org bruker",
|
||||||
"actionUpdateOrg": "Oppdater organisasjon",
|
"actionUpdateOrg": "Oppdater organisasjon",
|
||||||
"actionUpdateUser": "Oppdater bruker",
|
"actionUpdateUser": "Oppdater bruker",
|
||||||
"actionGetUser": "Hent bruker",
|
"actionGetUser": "Hent bruker",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Slett område",
|
"actionDeleteSite": "Slett område",
|
||||||
"actionGetSite": "Hent område",
|
"actionGetSite": "Hent område",
|
||||||
"actionListSites": "List opp områder",
|
"actionListSites": "List opp områder",
|
||||||
|
"actionApplyBlueprint": "Bruk blåkopi",
|
||||||
"setupToken": "Oppsetttoken",
|
"setupToken": "Oppsetttoken",
|
||||||
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
|
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
|
||||||
"setupTokenRequired": "Oppsetttoken er nødvendig",
|
"setupTokenRequired": "Oppsetttoken er nødvendig",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Lisens",
|
"sidebarLicense": "Lisens",
|
||||||
"sidebarClients": "Klienter (Beta)",
|
"sidebarClients": "Klienter (Beta)",
|
||||||
"sidebarDomains": "Domener",
|
"sidebarDomains": "Domener",
|
||||||
"enableDockerSocket": "Aktiver Docker Socket",
|
"enableDockerSocket": "Aktiver Docker blåkopi",
|
||||||
"enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.",
|
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
||||||
"enableDockerSocketLink": "Lær mer",
|
"enableDockerSocketLink": "Lær mer",
|
||||||
"viewDockerContainers": "Vis Docker-containere",
|
"viewDockerContainers": "Vis Docker-containere",
|
||||||
"containersIn": "Containere i {siteName}",
|
"containersIn": "Containere i {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Oppdatering tilgjengelig",
|
"newtUpdateAvailable": "Oppdatering tilgjengelig",
|
||||||
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
|
"newtUpdateAvailableInfo": "En ny versjon av Newt er tilgjengelig. Vennligst oppdater til den nyeste versjonen for den beste opplevelsen.",
|
||||||
"domainPickerEnterDomain": "Domene",
|
"domainPickerEnterDomain": "Domene",
|
||||||
"domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp",
|
"domainPickerPlaceholder": "minapp.eksempel.no",
|
||||||
"domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.",
|
"domainPickerDescription": "Skriv inn hele domenet til ressursen for å se tilgjengelige alternativer.",
|
||||||
"domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer",
|
"domainPickerDescriptionSaas": "Skriv inn et fullt domene, underdomene eller bare et navn for å se tilgjengelige alternativer",
|
||||||
"domainPickerTabAll": "Alle",
|
"domainPickerTabAll": "Alle",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protokoll",
|
"editInternalResourceDialogProtocol": "Protokoll",
|
||||||
"editInternalResourceDialogSitePort": "Områdeport",
|
"editInternalResourceDialogSitePort": "Områdeport",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
"editInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
||||||
"editInternalResourceDialogDestinationIP": "Destinasjons-IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Destinasjonsport",
|
|
||||||
"editInternalResourceDialogCancel": "Avbryt",
|
"editInternalResourceDialogCancel": "Avbryt",
|
||||||
"editInternalResourceDialogSaveResource": "Lagre ressurs",
|
"editInternalResourceDialogSaveResource": "Lagre ressurs",
|
||||||
"editInternalResourceDialogSuccess": "Suksess",
|
"editInternalResourceDialogSuccess": "Suksess",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Områdeport",
|
"createInternalResourceDialogSitePort": "Områdeport",
|
||||||
"createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.",
|
"createInternalResourceDialogSitePortDescription": "Bruk denne porten for å få tilgang til ressursen på området når du er tilkoblet med en klient.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
"createInternalResourceDialogTargetConfiguration": "Målkonfigurasjon",
|
||||||
"createInternalResourceDialogDestinationIP": "Destinasjons-IP",
|
"createInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "IP-adressen til ressursen på områdets nettverk.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Destinasjonsport",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.",
|
"createInternalResourceDialogDestinationPortDescription": "Porten på destinasjons-IP-en der ressursen kan nås.",
|
||||||
"createInternalResourceDialogCancel": "Avbryt",
|
"createInternalResourceDialogCancel": "Avbryt",
|
||||||
"createInternalResourceDialogCreateResource": "Opprett ressurs",
|
"createInternalResourceDialogCreateResource": "Opprett ressurs",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Konverter denne noden til manuelt bruk"
|
"convertButton": "Konverter denne noden til manuelt bruk"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Internasjonalt domene oppdaget",
|
"internationaldomaindetected": "Internasjonalt domene oppdaget",
|
||||||
"willbestoredas": "Vil bli lagret som:"
|
"willbestoredas": "Vil bli lagret som:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC leverandør",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Egendefinerte topptekster",
|
||||||
|
"headersValidationError": "Topptekst må være i formatet: header-navn: verdi.",
|
||||||
|
"domainPickerProvidedDomain": "Gitt domene",
|
||||||
|
"domainPickerFreeProvidedDomain": "Gratis oppgitt domene",
|
||||||
|
"domainPickerVerified": "Bekreftet",
|
||||||
|
"domainPickerUnverified": "Uverifisert",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Dette underdomenet inneholder ugyldige tegn eller struktur. Det vil automatisk bli utsatt når du lagrer.",
|
||||||
|
"domainPickerError": "Feil",
|
||||||
|
"domainPickerErrorLoadDomains": "Kan ikke laste organisasjonens domener",
|
||||||
|
"domainPickerErrorCheckAvailability": "Kunne ikke kontrollere domenetilgjengelighet",
|
||||||
|
"domainPickerInvalidSubdomain": "Ugyldig underdomene",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "Inndata \"{sub}\" ble fjernet fordi det ikke er gyldig.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kunne ikke gjøres gyldig for {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Underdomenet som ble sanivert",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" var korrigert til \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Rediger fil: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Rediger fil: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,12 @@
|
||||||
"name": "naam",
|
"name": "naam",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"site": "Website",
|
"site": "Referentie",
|
||||||
"dataIn": "Gegevens in",
|
"dataIn": "Dataverbruik inkomend",
|
||||||
"dataOut": "Data Uit",
|
"dataOut": "Dataverbruik uitgaand",
|
||||||
"connectionType": "Type verbinding",
|
"connectionType": "Type verbinding",
|
||||||
"tunnelType": "Tunnel type",
|
"tunnelType": "Tunnel type",
|
||||||
"local": "lokaal",
|
"local": "Lokaal",
|
||||||
"edit": "Bewerken",
|
"edit": "Bewerken",
|
||||||
"siteConfirmDelete": "Verwijderen van site bevestigen",
|
"siteConfirmDelete": "Verwijderen van site bevestigen",
|
||||||
"siteDelete": "Site verwijderen",
|
"siteDelete": "Site verwijderen",
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
"siteCreate": "Site maken",
|
"siteCreate": "Site maken",
|
||||||
"siteCreateDescription2": "Volg de onderstaande stappen om een nieuwe site aan te maken en te verbinden",
|
"siteCreateDescription2": "Volg de onderstaande stappen om een nieuwe site aan te maken en te verbinden",
|
||||||
"siteCreateDescription": "Maak een nieuwe site aan om verbinding te maken met uw bronnen",
|
"siteCreateDescription": "Maak een nieuwe site aan om verbinding te maken met uw bronnen",
|
||||||
"close": "Afsluiten",
|
"close": "Sluiten",
|
||||||
"siteErrorCreate": "Fout bij maken site",
|
"siteErrorCreate": "Fout bij maken site",
|
||||||
"siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden",
|
"siteErrorCreateKeyPair": "Key pair of site standaard niet gevonden",
|
||||||
"siteErrorCreateDefaults": "Standaardinstellingen niet gevonden",
|
"siteErrorCreateDefaults": "Standaardinstellingen niet gevonden",
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
"siteGeneralDescription": "Algemene instellingen voor deze site configureren",
|
"siteGeneralDescription": "Algemene instellingen voor deze site configureren",
|
||||||
"siteSettingDescription": "Configureer de instellingen op uw site",
|
"siteSettingDescription": "Configureer de instellingen op uw site",
|
||||||
"siteSetting": "{siteName} instellingen",
|
"siteSetting": "{siteName} instellingen",
|
||||||
"siteNewtTunnel": "Nieuwstunnel (Aanbevolen)",
|
"siteNewtTunnel": "Newttunnel (Aanbevolen)",
|
||||||
"siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.",
|
"siteNewtTunnelDescription": "Gemakkelijkste manier om een ingangspunt in uw netwerk te maken. Geen extra opzet.",
|
||||||
"siteWg": "Basis WireGuard",
|
"siteWg": "Basis WireGuard",
|
||||||
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
"siteWgDescription": "Gebruik een WireGuard client om een tunnel te bouwen. Handmatige NAT installatie vereist.",
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
"siteCredentialsSave": "Uw referenties opslaan",
|
"siteCredentialsSave": "Uw referenties opslaan",
|
||||||
"siteCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.",
|
"siteCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.",
|
||||||
"siteInfo": "Site informatie",
|
"siteInfo": "Site informatie",
|
||||||
"status": "status",
|
"status": "Status",
|
||||||
"shareTitle": "Beheer deellinks",
|
"shareTitle": "Beheer deellinks",
|
||||||
"shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot uw bronnen te verlenen",
|
"shareDescription": "Maak deelbare links aan om tijdelijke of permanente toegang tot uw bronnen te verlenen",
|
||||||
"shareSearch": "Zoek share links...",
|
"shareSearch": "Zoek share links...",
|
||||||
|
@ -146,19 +146,19 @@
|
||||||
"never": "Nooit",
|
"never": "Nooit",
|
||||||
"shareErrorSelectResource": "Selecteer een bron",
|
"shareErrorSelectResource": "Selecteer een bron",
|
||||||
"resourceTitle": "Bronnen beheren",
|
"resourceTitle": "Bronnen beheren",
|
||||||
"resourceDescription": "Veilige proxy's voor uw privé applicaties maken",
|
"resourceDescription": "Veilige proxy's voor uw privé applicaties aanmaken",
|
||||||
"resourcesSearch": "Zoek bronnen...",
|
"resourcesSearch": "Zoek bronnen...",
|
||||||
"resourceAdd": "Bron toevoegen",
|
"resourceAdd": "Bron toevoegen",
|
||||||
"resourceErrorDelte": "Fout bij verwijderen document",
|
"resourceErrorDelte": "Fout bij verwijderen document",
|
||||||
"authentication": "Authenticatie",
|
"authentication": "Authenticatie",
|
||||||
"protected": "Beschermd",
|
"protected": "Beveiligd",
|
||||||
"notProtected": "Niet beschermd",
|
"notProtected": "Niet beveiligd",
|
||||||
"resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.",
|
"resourceMessageRemove": "Eenmaal verwijderd, zal het bestand niet langer toegankelijk zijn. Alle doelen die gekoppeld zijn aan het hulpbron, zullen ook verwijderd worden.",
|
||||||
"resourceMessageConfirm": "Om te bevestigen, typ de naam van de bron hieronder.",
|
"resourceMessageConfirm": "Om te bevestigen, typ de naam van de bron hieronder.",
|
||||||
"resourceQuestionRemove": "Weet u zeker dat u de resource {selectedResource} uit de organisatie wilt verwijderen?",
|
"resourceQuestionRemove": "Weet u zeker dat u de resource {selectedResource} uit de organisatie wilt verwijderen?",
|
||||||
"resourceHTTP": "HTTPS bron",
|
"resourceHTTP": "HTTPS bron",
|
||||||
"resourceHTTPDescription": "Proxy verzoeken aan uw app via HTTPS via een subdomein of basisdomein.",
|
"resourceHTTPDescription": "Proxy verzoeken aan uw app via HTTPS via een subdomein of basisdomein.",
|
||||||
"resourceRaw": "Ruwe TCP/UDP bron",
|
"resourceRaw": "TCP/UDP bron",
|
||||||
"resourceRawDescription": "Proxy verzoeken naar je app via TCP/UDP met behulp van een poortnummer.",
|
"resourceRawDescription": "Proxy verzoeken naar je app via TCP/UDP met behulp van een poortnummer.",
|
||||||
"resourceCreate": "Bron maken",
|
"resourceCreate": "Bron maken",
|
||||||
"resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken",
|
"resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken",
|
||||||
|
@ -183,7 +183,7 @@
|
||||||
"protocolSelect": "Selecteer een protocol",
|
"protocolSelect": "Selecteer een protocol",
|
||||||
"resourcePortNumber": "Nummer van poort",
|
"resourcePortNumber": "Nummer van poort",
|
||||||
"resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.",
|
"resourcePortNumberDescription": "Het externe poortnummer naar proxyverzoeken.",
|
||||||
"cancel": "annuleren",
|
"cancel": "Annuleren",
|
||||||
"resourceConfig": "Configuratie tekstbouwstenen",
|
"resourceConfig": "Configuratie tekstbouwstenen",
|
||||||
"resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om je TCP/UDP-bron in te stellen",
|
"resourceConfigDescription": "Kopieer en plak deze configuratie-snippets om je TCP/UDP-bron in te stellen",
|
||||||
"resourceAddEntrypoints": "Traefik: Entrypoints toevoegen",
|
"resourceAddEntrypoints": "Traefik: Entrypoints toevoegen",
|
||||||
|
@ -212,7 +212,7 @@
|
||||||
"saveGeneralSettings": "Algemene instellingen opslaan",
|
"saveGeneralSettings": "Algemene instellingen opslaan",
|
||||||
"saveSettings": "Instellingen opslaan",
|
"saveSettings": "Instellingen opslaan",
|
||||||
"orgDangerZone": "Gevaarlijke zone",
|
"orgDangerZone": "Gevaarlijke zone",
|
||||||
"orgDangerZoneDescription": "Als u deze instantie verwijdert, is er geen weg terug. Wees het alstublieft zeker.",
|
"orgDangerZoneDescription": "Deze instantie verwijderen is onomkeerbaar. Bevestig alstublieft dat u wilt doorgaan.",
|
||||||
"orgDelete": "Verwijder organisatie",
|
"orgDelete": "Verwijder organisatie",
|
||||||
"orgDeleteConfirm": "Bevestig Verwijderen Organisatie",
|
"orgDeleteConfirm": "Bevestig Verwijderen Organisatie",
|
||||||
"orgMessageRemove": "Deze actie is onomkeerbaar en zal alle bijbehorende gegevens verwijderen.",
|
"orgMessageRemove": "Deze actie is onomkeerbaar en zal alle bijbehorende gegevens verwijderen.",
|
||||||
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Er is een fout opgetreden tijdens het toevoegen van de rol.",
|
"accessRoleErrorAddDescription": "Er is een fout opgetreden tijdens het toevoegen van de rol.",
|
||||||
"userSaved": "Gebruiker opgeslagen",
|
"userSaved": "Gebruiker opgeslagen",
|
||||||
"userSavedDescription": "De gebruiker is bijgewerkt.",
|
"userSavedDescription": "De gebruiker is bijgewerkt.",
|
||||||
|
"autoProvisioned": "Automatisch bevestigen",
|
||||||
|
"autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider",
|
||||||
"accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie",
|
"accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie",
|
||||||
"accessControlsSubmit": "Bewaar Toegangsbesturing",
|
"accessControlsSubmit": "Bewaar Toegangsbesturing",
|
||||||
"roles": "Rollen",
|
"roles": "Rollen",
|
||||||
|
@ -499,8 +501,8 @@
|
||||||
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
"targetStickySessionsDescription": "Behoud verbindingen op hetzelfde backend doel voor hun hele sessie.",
|
||||||
"methodSelect": "Selecteer methode",
|
"methodSelect": "Selecteer methode",
|
||||||
"targetSubmit": "Doelwit toevoegen",
|
"targetSubmit": "Doelwit toevoegen",
|
||||||
"targetNoOne": "Geen doelwitten. Voeg een doel toe via het formulier.",
|
"targetNoOne": "Geen doel toegevoegd. Voeg deze toe via dit formulier.",
|
||||||
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal load balancering mogelijk maken.",
|
||||||
"targetsSubmit": "Doelstellingen opslaan",
|
"targetsSubmit": "Doelstellingen opslaan",
|
||||||
"proxyAdditional": "Extra Proxy-instellingen",
|
"proxyAdditional": "Extra Proxy-instellingen",
|
||||||
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
|
"proxyAdditionalDescription": "Configureer hoe de proxy-instellingen van uw bron worden afgehandeld",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Ongeldig IP-adresformaat",
|
"ipAddressErrorInvalidFormat": "Ongeldig IP-adresformaat",
|
||||||
"ipAddressErrorInvalidOctet": "Ongeldige IP adres octet",
|
"ipAddressErrorInvalidOctet": "Ongeldige IP adres octet",
|
||||||
"path": "Pad",
|
"path": "Pad",
|
||||||
|
"matchPath": "Overeenkomend pad",
|
||||||
"ipAddressRange": "IP Bereik",
|
"ipAddressRange": "IP Bereik",
|
||||||
"rulesErrorFetch": "Regels ophalen mislukt",
|
"rulesErrorFetch": "Regels ophalen mislukt",
|
||||||
"rulesErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de regels",
|
"rulesErrorFetchDescription": "Er is een fout opgetreden bij het ophalen van de regels",
|
||||||
|
@ -595,7 +598,7 @@
|
||||||
"newtId": "Newt-ID",
|
"newtId": "Newt-ID",
|
||||||
"newtSecretKey": "Nieuwe geheime sleutel",
|
"newtSecretKey": "Nieuwe geheime sleutel",
|
||||||
"architecture": "Architectuur",
|
"architecture": "Architectuur",
|
||||||
"sites": "Werkruimtes",
|
"sites": "Verbindingen",
|
||||||
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.",
|
"siteWgAnyClients": "Gebruik een willekeurige WireGuard client om verbinding te maken. Je moet je interne bronnen aanspreken met behulp van de peer IP.",
|
||||||
"siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients",
|
"siteWgCompatibleAllClients": "Compatibel met alle WireGuard clients",
|
||||||
"siteWgManualConfigurationRequired": "Handmatige configuratie vereist",
|
"siteWgManualConfigurationRequired": "Handmatige configuratie vereist",
|
||||||
|
@ -726,7 +729,7 @@
|
||||||
"idpMessageConfirm": "Om dit te bevestigen, typt u de naam van onderstaande identiteitsprovider.",
|
"idpMessageConfirm": "Om dit te bevestigen, typt u de naam van onderstaande identiteitsprovider.",
|
||||||
"idpConfirmDelete": "Bevestig verwijderen Identity Provider",
|
"idpConfirmDelete": "Bevestig verwijderen Identity Provider",
|
||||||
"idpDelete": "Identity Provider verwijderen",
|
"idpDelete": "Identity Provider verwijderen",
|
||||||
"idp": "Identiteit aanbieders",
|
"idp": "Identiteitsaanbieders",
|
||||||
"idpSearch": "Identiteitsaanbieders zoeken...",
|
"idpSearch": "Identiteitsaanbieders zoeken...",
|
||||||
"idpAdd": "Identity Provider toevoegen",
|
"idpAdd": "Identity Provider toevoegen",
|
||||||
"idpClientIdRequired": "Client-ID is vereist.",
|
"idpClientIdRequired": "Client-ID is vereist.",
|
||||||
|
@ -798,7 +801,7 @@
|
||||||
"defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.",
|
"defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.",
|
||||||
"defaultMappingsSubmit": "Standaard toewijzingen opslaan",
|
"defaultMappingsSubmit": "Standaard toewijzingen opslaan",
|
||||||
"orgPoliciesEdit": "Organisatie beleid bewerken",
|
"orgPoliciesEdit": "Organisatie beleid bewerken",
|
||||||
"org": "Rekening",
|
"org": "Organisatie",
|
||||||
"orgSelect": "Selecteer organisatie",
|
"orgSelect": "Selecteer organisatie",
|
||||||
"orgSearch": "Zoek in org",
|
"orgSearch": "Zoek in org",
|
||||||
"orgNotFound": "Geen org gevonden.",
|
"orgNotFound": "Geen org gevonden.",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Verbonden",
|
"idpConnectingToFinished": "Verbonden",
|
||||||
"idpErrorConnectingTo": "Er was een probleem bij het verbinden met {name}. Neem contact op met uw beheerder.",
|
"idpErrorConnectingTo": "Er was een probleem bij het verbinden met {name}. Neem contact op met uw beheerder.",
|
||||||
"idpErrorNotFound": "IdP niet gevonden",
|
"idpErrorNotFound": "IdP niet gevonden",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Ongeldige uitnodiging",
|
"inviteInvalid": "Ongeldige uitnodiging",
|
||||||
"inviteInvalidDescription": "Uitnodigingslink is ongeldig.",
|
"inviteInvalidDescription": "Uitnodigingslink is ongeldig.",
|
||||||
"inviteErrorWrongUser": "Uitnodiging is niet voor deze gebruiker",
|
"inviteErrorWrongUser": "Uitnodiging is niet voor deze gebruiker",
|
||||||
|
@ -971,10 +976,10 @@
|
||||||
"supportKeyEnterDescription": "Ontmoet je eigen huisdier Pangolin!",
|
"supportKeyEnterDescription": "Ontmoet je eigen huisdier Pangolin!",
|
||||||
"githubUsername": "GitHub-gebruikersnaam",
|
"githubUsername": "GitHub-gebruikersnaam",
|
||||||
"supportKeyInput": "Supporter Sleutel",
|
"supportKeyInput": "Supporter Sleutel",
|
||||||
"supportKeyBuy": "Koop Supportersleutel",
|
"supportKeyBuy": "Koop supportersleutel",
|
||||||
"logoutError": "Fout bij uitloggen",
|
"logoutError": "Fout bij uitloggen",
|
||||||
"signingAs": "Ingelogd als",
|
"signingAs": "Ingelogd als",
|
||||||
"serverAdmin": "Server Beheerder",
|
"serverAdmin": "Server beheer",
|
||||||
"managedSelfhosted": "Beheerde Self-Hosted",
|
"managedSelfhosted": "Beheerde Self-Hosted",
|
||||||
"otpEnable": "Twee-factor inschakelen",
|
"otpEnable": "Twee-factor inschakelen",
|
||||||
"otpDisable": "Tweestapsverificatie uitschakelen",
|
"otpDisable": "Tweestapsverificatie uitschakelen",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Professionele editie vereist",
|
"licenseTierProfessionalRequired": "Professionele editie vereist",
|
||||||
"licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.",
|
"licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.",
|
||||||
"actionGetOrg": "Krijg Organisatie",
|
"actionGetOrg": "Krijg Organisatie",
|
||||||
|
"updateOrgUser": "Org gebruiker bijwerken",
|
||||||
|
"createOrgUser": "Org gebruiker aanmaken",
|
||||||
"actionUpdateOrg": "Organisatie bijwerken",
|
"actionUpdateOrg": "Organisatie bijwerken",
|
||||||
"actionUpdateUser": "Gebruiker bijwerken",
|
"actionUpdateUser": "Gebruiker bijwerken",
|
||||||
"actionGetUser": "Gebruiker ophalen",
|
"actionGetUser": "Gebruiker ophalen",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Site verwijderen",
|
"actionDeleteSite": "Site verwijderen",
|
||||||
"actionGetSite": "Site ophalen",
|
"actionGetSite": "Site ophalen",
|
||||||
"actionListSites": "Sites weergeven",
|
"actionListSites": "Sites weergeven",
|
||||||
|
"actionApplyBlueprint": "Blauwdruk toepassen",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
|
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
|
||||||
"setupTokenRequired": "Setup-token is vereist",
|
"setupTokenRequired": "Setup-token is vereist",
|
||||||
|
@ -1120,7 +1128,7 @@
|
||||||
"sidebarOverview": "Overzicht.",
|
"sidebarOverview": "Overzicht.",
|
||||||
"sidebarHome": "Startpagina",
|
"sidebarHome": "Startpagina",
|
||||||
"sidebarSites": "Werkruimtes",
|
"sidebarSites": "Werkruimtes",
|
||||||
"sidebarResources": "Hulpmiddelen",
|
"sidebarResources": "Bronnen",
|
||||||
"sidebarAccessControl": "Toegangs controle",
|
"sidebarAccessControl": "Toegangs controle",
|
||||||
"sidebarUsers": "Gebruikers",
|
"sidebarUsers": "Gebruikers",
|
||||||
"sidebarInvitations": "Uitnodigingen",
|
"sidebarInvitations": "Uitnodigingen",
|
||||||
|
@ -1133,13 +1141,13 @@
|
||||||
"sidebarLicense": "Licentie",
|
"sidebarLicense": "Licentie",
|
||||||
"sidebarClients": "Clients (Bèta)",
|
"sidebarClients": "Clients (Bèta)",
|
||||||
"sidebarDomains": "Domeinen",
|
"sidebarDomains": "Domeinen",
|
||||||
"enableDockerSocket": "Docker Socket inschakelen",
|
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
||||||
"enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.",
|
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
||||||
"enableDockerSocketLink": "Meer informatie",
|
"enableDockerSocketLink": "Meer informatie",
|
||||||
"viewDockerContainers": "Bekijk Docker containers",
|
"viewDockerContainers": "Bekijk Docker containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
"selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.",
|
"selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.",
|
||||||
"containerName": "naam",
|
"containerName": "Naam",
|
||||||
"containerImage": "Afbeelding",
|
"containerImage": "Afbeelding",
|
||||||
"containerState": "Provincie",
|
"containerState": "Provincie",
|
||||||
"containerNetworks": "Netwerken",
|
"containerNetworks": "Netwerken",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Update beschikbaar",
|
"newtUpdateAvailable": "Update beschikbaar",
|
||||||
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
|
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
|
||||||
"domainPickerEnterDomain": "Domein",
|
"domainPickerEnterDomain": "Domein",
|
||||||
"domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp",
|
"domainPickerPlaceholder": "mijnapp.voorbeeld.nl",
|
||||||
"domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.",
|
"domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.",
|
||||||
"domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien",
|
"domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien",
|
||||||
"domainPickerTabAll": "Alles",
|
"domainPickerTabAll": "Alles",
|
||||||
|
@ -1341,7 +1349,7 @@
|
||||||
"olmId": "Olm ID",
|
"olmId": "Olm ID",
|
||||||
"olmSecretKey": "Olm Geheime Sleutel",
|
"olmSecretKey": "Olm Geheime Sleutel",
|
||||||
"clientCredentialsSave": "Uw referenties opslaan",
|
"clientCredentialsSave": "Uw referenties opslaan",
|
||||||
"clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde plek.",
|
"clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer deze naar een veilige plek.",
|
||||||
"generalSettingsDescription": "Configureer de algemene instellingen voor deze client",
|
"generalSettingsDescription": "Configureer de algemene instellingen voor deze client",
|
||||||
"clientUpdated": "Klant bijgewerkt ",
|
"clientUpdated": "Klant bijgewerkt ",
|
||||||
"clientUpdatedDescription": "De client is bijgewerkt.",
|
"clientUpdatedDescription": "De client is bijgewerkt.",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocol",
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
"editInternalResourceDialogSitePort": "Site Poort",
|
"editInternalResourceDialogSitePort": "Site Poort",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
"editInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
||||||
"editInternalResourceDialogDestinationIP": "Bestemming IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Bestemmingspoort",
|
|
||||||
"editInternalResourceDialogCancel": "Annuleren",
|
"editInternalResourceDialogCancel": "Annuleren",
|
||||||
"editInternalResourceDialogSaveResource": "Sla bron op",
|
"editInternalResourceDialogSaveResource": "Sla bron op",
|
||||||
"editInternalResourceDialogSuccess": "Succes",
|
"editInternalResourceDialogSuccess": "Succes",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site Poort",
|
"createInternalResourceDialogSitePort": "Site Poort",
|
||||||
"createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.",
|
"createInternalResourceDialogSitePortDescription": "Gebruik deze poort om toegang te krijgen tot de bron op de site wanneer verbonden met een client.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
"createInternalResourceDialogTargetConfiguration": "Doelconfiguratie",
|
||||||
"createInternalResourceDialogDestinationIP": "Bestemming IP",
|
"createInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Het IP-adres van de bron op het netwerk van de site.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Bestemmingspoort",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.",
|
"createInternalResourceDialogDestinationPortDescription": "De poort op het bestemmings-IP waar de bron toegankelijk is.",
|
||||||
"createInternalResourceDialogCancel": "Annuleren",
|
"createInternalResourceDialogCancel": "Annuleren",
|
||||||
"createInternalResourceDialogCreateResource": "Bron aanmaken",
|
"createInternalResourceDialogCreateResource": "Bron aanmaken",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Converteer deze node naar Beheerde Zelf-Hosted"
|
"convertButton": "Converteer deze node naar Beheerde Zelf-Hosted"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
|
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
|
||||||
"willbestoredas": "Zal worden opgeslagen als:"
|
"willbestoredas": "Zal worden opgeslagen als:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC provider",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Aangepaste headers",
|
||||||
|
"headersValidationError": "Headers moeten in het formaat zijn: Header-Naam: waarde.",
|
||||||
|
"domainPickerProvidedDomain": "Opgegeven domein",
|
||||||
|
"domainPickerFreeProvidedDomain": "Gratis verstrekt domein",
|
||||||
|
"domainPickerVerified": "Geverifieerd",
|
||||||
|
"domainPickerUnverified": "Ongeverifieerd",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Dit subdomein bevat ongeldige tekens of structuur. Het zal automatisch worden gesaneerd wanneer u opslaat.",
|
||||||
|
"domainPickerError": "Foutmelding",
|
||||||
|
"domainPickerErrorLoadDomains": "Fout bij het laden van organisatiedomeinen",
|
||||||
|
"domainPickerErrorCheckAvailability": "Kan domein beschikbaarheid niet controleren",
|
||||||
|
"domainPickerInvalidSubdomain": "Ongeldig subdomein",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "De invoer \"{sub}\" is verwijderd omdat het niet geldig is.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" kon niet geldig worden gemaakt voor {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomein gesaniseerd",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" was gecorrigeerd op \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Bestand bewerken: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Bestand bewerken: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Wystąpił błąd podczas dodawania użytkownika do roli.",
|
"accessRoleErrorAddDescription": "Wystąpił błąd podczas dodawania użytkownika do roli.",
|
||||||
"userSaved": "Użytkownik zapisany",
|
"userSaved": "Użytkownik zapisany",
|
||||||
"userSavedDescription": "Użytkownik został zaktualizowany.",
|
"userSavedDescription": "Użytkownik został zaktualizowany.",
|
||||||
|
"autoProvisioned": "Przesłane automatycznie",
|
||||||
|
"autoProvisionedDescription": "Pozwól temu użytkownikowi na automatyczne zarządzanie przez dostawcę tożsamości",
|
||||||
"accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji",
|
"accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji",
|
||||||
"accessControlsSubmit": "Zapisz kontrole dostępu",
|
"accessControlsSubmit": "Zapisz kontrole dostępu",
|
||||||
"roles": "Role",
|
"roles": "Role",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Nieprawidłowy format adresu IP",
|
"ipAddressErrorInvalidFormat": "Nieprawidłowy format adresu IP",
|
||||||
"ipAddressErrorInvalidOctet": "Nieprawidłowy oktet adresu IP",
|
"ipAddressErrorInvalidOctet": "Nieprawidłowy oktet adresu IP",
|
||||||
"path": "Ścieżka",
|
"path": "Ścieżka",
|
||||||
|
"matchPath": "Ścieżka dopasowania",
|
||||||
"ipAddressRange": "Zakres IP",
|
"ipAddressRange": "Zakres IP",
|
||||||
"rulesErrorFetch": "Nie udało się pobrać reguł",
|
"rulesErrorFetch": "Nie udało się pobrać reguł",
|
||||||
"rulesErrorFetchDescription": "Wystąpił błąd podczas pobierania reguł",
|
"rulesErrorFetchDescription": "Wystąpił błąd podczas pobierania reguł",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Połączono",
|
"idpConnectingToFinished": "Połączono",
|
||||||
"idpErrorConnectingTo": "Wystąpił problem z połączeniem z {name}. Skontaktuj się z administratorem.",
|
"idpErrorConnectingTo": "Wystąpił problem z połączeniem z {name}. Skontaktuj się z administratorem.",
|
||||||
"idpErrorNotFound": "Nie znaleziono IdP",
|
"idpErrorNotFound": "Nie znaleziono IdP",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Nieprawidłowe zaproszenie",
|
"inviteInvalid": "Nieprawidłowe zaproszenie",
|
||||||
"inviteInvalidDescription": "Link zapraszający jest nieprawidłowy.",
|
"inviteInvalidDescription": "Link zapraszający jest nieprawidłowy.",
|
||||||
"inviteErrorWrongUser": "Zaproszenie nie jest dla tego użytkownika",
|
"inviteErrorWrongUser": "Zaproszenie nie jest dla tego użytkownika",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Wymagana edycja Professional",
|
"licenseTierProfessionalRequired": "Wymagana edycja Professional",
|
||||||
"licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.",
|
"licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.",
|
||||||
"actionGetOrg": "Pobierz organizację",
|
"actionGetOrg": "Pobierz organizację",
|
||||||
|
"updateOrgUser": "Aktualizuj użytkownika Org",
|
||||||
|
"createOrgUser": "Utwórz użytkownika Org",
|
||||||
"actionUpdateOrg": "Aktualizuj organizację",
|
"actionUpdateOrg": "Aktualizuj organizację",
|
||||||
"actionUpdateUser": "Zaktualizuj użytkownika",
|
"actionUpdateUser": "Zaktualizuj użytkownika",
|
||||||
"actionGetUser": "Pobierz użytkownika",
|
"actionGetUser": "Pobierz użytkownika",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Usuń witrynę",
|
"actionDeleteSite": "Usuń witrynę",
|
||||||
"actionGetSite": "Pobierz witrynę",
|
"actionGetSite": "Pobierz witrynę",
|
||||||
"actionListSites": "Lista witryn",
|
"actionListSites": "Lista witryn",
|
||||||
|
"actionApplyBlueprint": "Zastosuj schemat",
|
||||||
"setupToken": "Skonfiguruj token",
|
"setupToken": "Skonfiguruj token",
|
||||||
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
|
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
|
||||||
"setupTokenRequired": "Wymagany jest token konfiguracji",
|
"setupTokenRequired": "Wymagany jest token konfiguracji",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Licencja",
|
"sidebarLicense": "Licencja",
|
||||||
"sidebarClients": "Klienci (Beta)",
|
"sidebarClients": "Klienci (Beta)",
|
||||||
"sidebarDomains": "Domeny",
|
"sidebarDomains": "Domeny",
|
||||||
"enableDockerSocket": "Włącz gniazdo dokera",
|
"enableDockerSocket": "Włącz schemat dokera",
|
||||||
"enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.",
|
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
|
||||||
"enableDockerSocketLink": "Dowiedz się więcej",
|
"enableDockerSocketLink": "Dowiedz się więcej",
|
||||||
"viewDockerContainers": "Zobacz kontenery dokujące",
|
"viewDockerContainers": "Zobacz kontenery dokujące",
|
||||||
"containersIn": "Pojemniki w {siteName}",
|
"containersIn": "Pojemniki w {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Dostępna aktualizacja",
|
"newtUpdateAvailable": "Dostępna aktualizacja",
|
||||||
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
|
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
|
||||||
"domainPickerEnterDomain": "Domena",
|
"domainPickerEnterDomain": "Domena",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp",
|
"domainPickerPlaceholder": "mojapp.example.com",
|
||||||
"domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.",
|
"domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.",
|
||||||
"domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje",
|
"domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje",
|
||||||
"domainPickerTabAll": "Wszystko",
|
"domainPickerTabAll": "Wszystko",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protokół",
|
"editInternalResourceDialogProtocol": "Protokół",
|
||||||
"editInternalResourceDialogSitePort": "Port witryny",
|
"editInternalResourceDialogSitePort": "Port witryny",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
"editInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
||||||
"editInternalResourceDialogDestinationIP": "IP docelowe",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Port docelowy",
|
|
||||||
"editInternalResourceDialogCancel": "Anuluj",
|
"editInternalResourceDialogCancel": "Anuluj",
|
||||||
"editInternalResourceDialogSaveResource": "Zapisz zasób",
|
"editInternalResourceDialogSaveResource": "Zapisz zasób",
|
||||||
"editInternalResourceDialogSuccess": "Sukces",
|
"editInternalResourceDialogSuccess": "Sukces",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Port witryny",
|
"createInternalResourceDialogSitePort": "Port witryny",
|
||||||
"createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.",
|
"createInternalResourceDialogSitePortDescription": "Użyj tego portu, aby uzyskać dostęp do zasobu na stronie, gdy połączony z klientem.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
"createInternalResourceDialogTargetConfiguration": "Konfiguracja celu",
|
||||||
"createInternalResourceDialogDestinationIP": "IP docelowe",
|
"createInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Adres IP zasobu w sieci strony.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Port docelowy",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.",
|
"createInternalResourceDialogDestinationPortDescription": "Port na docelowym IP, gdzie zasób jest dostępny.",
|
||||||
"createInternalResourceDialogCancel": "Anuluj",
|
"createInternalResourceDialogCancel": "Anuluj",
|
||||||
"createInternalResourceDialogCreateResource": "Utwórz zasób",
|
"createInternalResourceDialogCreateResource": "Utwórz zasób",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Konwertuj ten węzeł do zarządzanego samodzielnie"
|
"convertButton": "Konwertuj ten węzeł do zarządzanego samodzielnie"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Wykryto międzynarodową domenę",
|
"internationaldomaindetected": "Wykryto międzynarodową domenę",
|
||||||
"willbestoredas": "Będą przechowywane jako:"
|
"willbestoredas": "Będą przechowywane jako:",
|
||||||
|
"idpGoogleDescription": "Dostawca Google OAuth2/OIDC",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Niestandardowe nagłówki",
|
||||||
|
"headersValidationError": "Nagłówki muszą być w formacie: Nazwa nagłówka: wartość.",
|
||||||
|
"domainPickerProvidedDomain": "Dostarczona domena",
|
||||||
|
"domainPickerFreeProvidedDomain": "Darmowa oferowana domena",
|
||||||
|
"domainPickerVerified": "Zweryfikowano",
|
||||||
|
"domainPickerUnverified": "Niezweryfikowane",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Ta subdomena zawiera nieprawidłowe znaki lub strukturę. Zostanie ona automatycznie oczyszczona po zapisaniu.",
|
||||||
|
"domainPickerError": "Błąd",
|
||||||
|
"domainPickerErrorLoadDomains": "Nie udało się załadować domen organizacji",
|
||||||
|
"domainPickerErrorCheckAvailability": "Nie udało się sprawdzić dostępności domeny",
|
||||||
|
"domainPickerInvalidSubdomain": "Nieprawidłowa subdomena",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "Wejście \"{sub}\" zostało usunięte, ponieważ jest nieprawidłowe.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" nie może być poprawne dla {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Poddomena oczyszczona",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" został skorygowany do \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Edytuj plik: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Edytuj plik: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar usuário à função.",
|
"accessRoleErrorAddDescription": "Ocorreu um erro ao adicionar usuário à função.",
|
||||||
"userSaved": "Usuário salvo",
|
"userSaved": "Usuário salvo",
|
||||||
"userSavedDescription": "O usuário foi atualizado.",
|
"userSavedDescription": "O usuário foi atualizado.",
|
||||||
|
"autoProvisioned": "Auto provisionado",
|
||||||
|
"autoProvisionedDescription": "Permitir que este usuário seja gerenciado automaticamente pelo provedor de identidade",
|
||||||
"accessControlsDescription": "Gerencie o que este usuário pode acessar e fazer na organização",
|
"accessControlsDescription": "Gerencie o que este usuário pode acessar e fazer na organização",
|
||||||
"accessControlsSubmit": "Salvar Controles de Acesso",
|
"accessControlsSubmit": "Salvar Controles de Acesso",
|
||||||
"roles": "Funções",
|
"roles": "Funções",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Formato de endereço IP inválido",
|
"ipAddressErrorInvalidFormat": "Formato de endereço IP inválido",
|
||||||
"ipAddressErrorInvalidOctet": "Octeto de endereço IP inválido",
|
"ipAddressErrorInvalidOctet": "Octeto de endereço IP inválido",
|
||||||
"path": "Caminho",
|
"path": "Caminho",
|
||||||
|
"matchPath": "Correspondência de caminho",
|
||||||
"ipAddressRange": "Faixa de IP",
|
"ipAddressRange": "Faixa de IP",
|
||||||
"rulesErrorFetch": "Falha ao buscar regras",
|
"rulesErrorFetch": "Falha ao buscar regras",
|
||||||
"rulesErrorFetchDescription": "Ocorreu um erro ao buscar regras",
|
"rulesErrorFetchDescription": "Ocorreu um erro ao buscar regras",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Conectado",
|
"idpConnectingToFinished": "Conectado",
|
||||||
"idpErrorConnectingTo": "Ocorreu um problema ao ligar a {name}. Por favor, contacte o seu administrador.",
|
"idpErrorConnectingTo": "Ocorreu um problema ao ligar a {name}. Por favor, contacte o seu administrador.",
|
||||||
"idpErrorNotFound": "IdP não encontrado",
|
"idpErrorNotFound": "IdP não encontrado",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Convite Inválido",
|
"inviteInvalid": "Convite Inválido",
|
||||||
"inviteInvalidDescription": "O link do convite é inválido.",
|
"inviteInvalidDescription": "O link do convite é inválido.",
|
||||||
"inviteErrorWrongUser": "O convite não é para este usuário",
|
"inviteErrorWrongUser": "O convite não é para este usuário",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Edição Profissional Necessária",
|
"licenseTierProfessionalRequired": "Edição Profissional Necessária",
|
||||||
"licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.",
|
"licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.",
|
||||||
"actionGetOrg": "Obter Organização",
|
"actionGetOrg": "Obter Organização",
|
||||||
|
"updateOrgUser": "Atualizar usuário Org",
|
||||||
|
"createOrgUser": "Criar usuário Org",
|
||||||
"actionUpdateOrg": "Atualizar Organização",
|
"actionUpdateOrg": "Atualizar Organização",
|
||||||
"actionUpdateUser": "Atualizar Usuário",
|
"actionUpdateUser": "Atualizar Usuário",
|
||||||
"actionGetUser": "Obter Usuário",
|
"actionGetUser": "Obter Usuário",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Eliminar Site",
|
"actionDeleteSite": "Eliminar Site",
|
||||||
"actionGetSite": "Obter Site",
|
"actionGetSite": "Obter Site",
|
||||||
"actionListSites": "Listar Sites",
|
"actionListSites": "Listar Sites",
|
||||||
|
"actionApplyBlueprint": "Aplicar Diagrama",
|
||||||
"setupToken": "Configuração do Token",
|
"setupToken": "Configuração do Token",
|
||||||
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
|
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
|
||||||
"setupTokenRequired": "Token de configuração é necessário",
|
"setupTokenRequired": "Token de configuração é necessário",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Tipo:",
|
"sidebarLicense": "Tipo:",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clientes (Beta)",
|
||||||
"sidebarDomains": "Domínios",
|
"sidebarDomains": "Domínios",
|
||||||
"enableDockerSocket": "Habilitar Docker Socket",
|
"enableDockerSocket": "Habilitar o Diagrama Docker",
|
||||||
"enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.",
|
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
|
||||||
"enableDockerSocketLink": "Saiba mais",
|
"enableDockerSocketLink": "Saiba mais",
|
||||||
"viewDockerContainers": "Ver contêineres Docker",
|
"viewDockerContainers": "Ver contêineres Docker",
|
||||||
"containersIn": "Contêineres em {siteName}",
|
"containersIn": "Contêineres em {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Nova Atualização Disponível",
|
"newtUpdateAvailable": "Nova Atualização Disponível",
|
||||||
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
|
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
|
||||||
"domainPickerEnterDomain": "Domínio",
|
"domainPickerEnterDomain": "Domínio",
|
||||||
"domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp",
|
"domainPickerPlaceholder": "myapp.exemplo.com",
|
||||||
"domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.",
|
"domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.",
|
||||||
"domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis",
|
"domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis",
|
||||||
"domainPickerTabAll": "Todos",
|
"domainPickerTabAll": "Todos",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protocolo",
|
"editInternalResourceDialogProtocol": "Protocolo",
|
||||||
"editInternalResourceDialogSitePort": "Porta do Site",
|
"editInternalResourceDialogSitePort": "Porta do Site",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
"editInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
||||||
"editInternalResourceDialogDestinationIP": "IP de Destino",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Porta de Destino",
|
|
||||||
"editInternalResourceDialogCancel": "Cancelar",
|
"editInternalResourceDialogCancel": "Cancelar",
|
||||||
"editInternalResourceDialogSaveResource": "Salvar Recurso",
|
"editInternalResourceDialogSaveResource": "Salvar Recurso",
|
||||||
"editInternalResourceDialogSuccess": "Sucesso",
|
"editInternalResourceDialogSuccess": "Sucesso",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Porta do Site",
|
"createInternalResourceDialogSitePort": "Porta do Site",
|
||||||
"createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.",
|
"createInternalResourceDialogSitePortDescription": "Use esta porta para acessar o recurso no site quando conectado com um cliente.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
"createInternalResourceDialogTargetConfiguration": "Configuração do Alvo",
|
||||||
"createInternalResourceDialogDestinationIP": "IP de Destino",
|
"createInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "O endereço IP do recurso na rede do site.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Porta de Destino",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.",
|
"createInternalResourceDialogDestinationPortDescription": "A porta no IP de destino onde o recurso está acessível.",
|
||||||
"createInternalResourceDialogCancel": "Cancelar",
|
"createInternalResourceDialogCancel": "Cancelar",
|
||||||
"createInternalResourceDialogCreateResource": "Criar Recurso",
|
"createInternalResourceDialogCreateResource": "Criar Recurso",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Converter este nó para Auto-Hospedado Gerenciado"
|
"convertButton": "Converter este nó para Auto-Hospedado Gerenciado"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Domínio Internacional Detectado",
|
"internationaldomaindetected": "Domínio Internacional Detectado",
|
||||||
"willbestoredas": "Será armazenado como:"
|
"willbestoredas": "Será armazenado como:",
|
||||||
|
"idpGoogleDescription": "Provedor Google OAuth2/OIDC",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Cabeçalhos Personalizados",
|
||||||
|
"headersValidationError": "Cabeçalhos devem estar no formato: Nome do Cabeçalho: valor.",
|
||||||
|
"domainPickerProvidedDomain": "Domínio fornecido",
|
||||||
|
"domainPickerFreeProvidedDomain": "Domínio fornecido grátis",
|
||||||
|
"domainPickerVerified": "Verificada",
|
||||||
|
"domainPickerUnverified": "Não verificado",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Este subdomínio contém caracteres ou estrutura inválidos. Ele será eliminado automaticamente quando você salvar.",
|
||||||
|
"domainPickerError": "ERRO",
|
||||||
|
"domainPickerErrorLoadDomains": "Falha ao carregar domínios da organização",
|
||||||
|
"domainPickerErrorCheckAvailability": "Não foi possível verificar a disponibilidade do domínio",
|
||||||
|
"domainPickerInvalidSubdomain": "Subdomínio inválido",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "A entrada \"{sub}\" foi removida porque ela não é válida.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" não pôde ser válido para {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Subdomínio banalizado",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" foi corrigido para \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Editar arquivo: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Editar arquivo: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.",
|
"accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.",
|
||||||
"userSaved": "Пользователь сохранён",
|
"userSaved": "Пользователь сохранён",
|
||||||
"userSavedDescription": "Пользователь был обновлён.",
|
"userSavedDescription": "Пользователь был обновлён.",
|
||||||
|
"autoProvisioned": "Автоподбор",
|
||||||
|
"autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем",
|
||||||
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
|
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
|
||||||
"accessControlsSubmit": "Сохранить контроль доступа",
|
"accessControlsSubmit": "Сохранить контроль доступа",
|
||||||
"roles": "Роли",
|
"roles": "Роли",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Неверный формат IP адреса",
|
"ipAddressErrorInvalidFormat": "Неверный формат IP адреса",
|
||||||
"ipAddressErrorInvalidOctet": "Неверный октет IP адреса",
|
"ipAddressErrorInvalidOctet": "Неверный октет IP адреса",
|
||||||
"path": "Путь",
|
"path": "Путь",
|
||||||
|
"matchPath": "Путь матча",
|
||||||
"ipAddressRange": "Диапазон IP",
|
"ipAddressRange": "Диапазон IP",
|
||||||
"rulesErrorFetch": "Не удалось получить правила",
|
"rulesErrorFetch": "Не удалось получить правила",
|
||||||
"rulesErrorFetchDescription": "Произошла ошибка при получении правил",
|
"rulesErrorFetchDescription": "Произошла ошибка при получении правил",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Подключено",
|
"idpConnectingToFinished": "Подключено",
|
||||||
"idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.",
|
"idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.",
|
||||||
"idpErrorNotFound": "IdP не найден",
|
"idpErrorNotFound": "IdP не найден",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Недействительное приглашение",
|
"inviteInvalid": "Недействительное приглашение",
|
||||||
"inviteInvalidDescription": "Ссылка на приглашение недействительна.",
|
"inviteInvalidDescription": "Ссылка на приглашение недействительна.",
|
||||||
"inviteErrorWrongUser": "Приглашение не для этого пользователя",
|
"inviteErrorWrongUser": "Приглашение не для этого пользователя",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
|
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
|
||||||
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
||||||
"actionGetOrg": "Получить организацию",
|
"actionGetOrg": "Получить организацию",
|
||||||
|
"updateOrgUser": "Обновить пользователя Org",
|
||||||
|
"createOrgUser": "Создать пользователя Org",
|
||||||
"actionUpdateOrg": "Обновить организацию",
|
"actionUpdateOrg": "Обновить организацию",
|
||||||
"actionUpdateUser": "Обновить пользователя",
|
"actionUpdateUser": "Обновить пользователя",
|
||||||
"actionGetUser": "Получить пользователя",
|
"actionGetUser": "Получить пользователя",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Удалить сайт",
|
"actionDeleteSite": "Удалить сайт",
|
||||||
"actionGetSite": "Получить сайт",
|
"actionGetSite": "Получить сайт",
|
||||||
"actionListSites": "Список сайтов",
|
"actionListSites": "Список сайтов",
|
||||||
|
"actionApplyBlueprint": "Применить чертёж",
|
||||||
"setupToken": "Код настройки",
|
"setupToken": "Код настройки",
|
||||||
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
||||||
"setupTokenRequired": "Токен настройки обязателен",
|
"setupTokenRequired": "Токен настройки обязателен",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Лицензия",
|
"sidebarLicense": "Лицензия",
|
||||||
"sidebarClients": "Клиенты (бета)",
|
"sidebarClients": "Клиенты (бета)",
|
||||||
"sidebarDomains": "Домены",
|
"sidebarDomains": "Домены",
|
||||||
"enableDockerSocket": "Включить Docker Socket",
|
"enableDockerSocket": "Включить чертёж Docker",
|
||||||
"enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.",
|
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
||||||
"enableDockerSocketLink": "Узнать больше",
|
"enableDockerSocketLink": "Узнать больше",
|
||||||
"viewDockerContainers": "Просмотр контейнеров Docker",
|
"viewDockerContainers": "Просмотр контейнеров Docker",
|
||||||
"containersIn": "Контейнеры в {siteName}",
|
"containersIn": "Контейнеры в {siteName}",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Доступно обновление",
|
"newtUpdateAvailable": "Доступно обновление",
|
||||||
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
||||||
"domainPickerEnterDomain": "Домен",
|
"domainPickerEnterDomain": "Домен",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, или просто myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.",
|
"domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.",
|
||||||
"domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции",
|
"domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции",
|
||||||
"domainPickerTabAll": "Все",
|
"domainPickerTabAll": "Все",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Протокол",
|
"editInternalResourceDialogProtocol": "Протокол",
|
||||||
"editInternalResourceDialogSitePort": "Порт сайта",
|
"editInternalResourceDialogSitePort": "Порт сайта",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Настройка цели",
|
"editInternalResourceDialogTargetConfiguration": "Настройка цели",
|
||||||
"editInternalResourceDialogDestinationIP": "Целевая IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Целевой порт",
|
|
||||||
"editInternalResourceDialogCancel": "Отмена",
|
"editInternalResourceDialogCancel": "Отмена",
|
||||||
"editInternalResourceDialogSaveResource": "Сохранить ресурс",
|
"editInternalResourceDialogSaveResource": "Сохранить ресурс",
|
||||||
"editInternalResourceDialogSuccess": "Успешно",
|
"editInternalResourceDialogSuccess": "Успешно",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Порт сайта",
|
"createInternalResourceDialogSitePort": "Порт сайта",
|
||||||
"createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.",
|
"createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Настройка цели",
|
"createInternalResourceDialogTargetConfiguration": "Настройка цели",
|
||||||
"createInternalResourceDialogDestinationIP": "Целевая IP",
|
"createInternalResourceDialogDestinationIPDescription": "IP или адрес хоста ресурса в сети сайта.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "IP-адрес ресурса в сети сайта.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Целевой порт",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.",
|
"createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.",
|
||||||
"createInternalResourceDialogCancel": "Отмена",
|
"createInternalResourceDialogCancel": "Отмена",
|
||||||
"createInternalResourceDialogCreateResource": "Создать ресурс",
|
"createInternalResourceDialogCreateResource": "Создать ресурс",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Конвертировать этот узел в управляемый себе-хост"
|
"convertButton": "Конвертировать этот узел в управляемый себе-хост"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Обнаружен международный домен",
|
"internationaldomaindetected": "Обнаружен международный домен",
|
||||||
"willbestoredas": "Будет храниться как:"
|
"willbestoredas": "Будет храниться как:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC провайдер",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "Пользовательские заголовки",
|
||||||
|
"headersValidationError": "Заголовки должны быть в формате: Название заголовка: значение.",
|
||||||
|
"domainPickerProvidedDomain": "Домен предоставлен",
|
||||||
|
"domainPickerFreeProvidedDomain": "Бесплатный домен",
|
||||||
|
"domainPickerVerified": "Подтверждено",
|
||||||
|
"domainPickerUnverified": "Не подтверждено",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Этот поддомен содержит недопустимые символы или структуру. Он будет очищен автоматически при сохранении.",
|
||||||
|
"domainPickerError": "Ошибка",
|
||||||
|
"domainPickerErrorLoadDomains": "Не удалось загрузить домены организации",
|
||||||
|
"domainPickerErrorCheckAvailability": "Не удалось проверить доступность домена",
|
||||||
|
"domainPickerInvalidSubdomain": "Неверный поддомен",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "Ввод \"{sub}\" был удален, потому что он недействителен.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" не может быть действительным для {domain}.",
|
||||||
|
"domainPickerSubdomainSanitized": "Субдомен очищен",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" был исправлен на \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "Редактировать файл: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Редактировать файл: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.",
|
"accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.",
|
||||||
"userSaved": "Kullanıcı kaydedildi",
|
"userSaved": "Kullanıcı kaydedildi",
|
||||||
"userSavedDescription": "Kullanıcı güncellenmiştir.",
|
"userSavedDescription": "Kullanıcı güncellenmiştir.",
|
||||||
|
"autoProvisioned": "Otomatik Sağlandı",
|
||||||
|
"autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver",
|
||||||
"accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin",
|
"accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin",
|
||||||
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
|
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
|
||||||
"roles": "Roller",
|
"roles": "Roller",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "Geçersiz IP adresi formatı",
|
"ipAddressErrorInvalidFormat": "Geçersiz IP adresi formatı",
|
||||||
"ipAddressErrorInvalidOctet": "Geçersiz IP adresi okteti",
|
"ipAddressErrorInvalidOctet": "Geçersiz IP adresi okteti",
|
||||||
"path": "Yol",
|
"path": "Yol",
|
||||||
|
"matchPath": "Yol Eşleştir",
|
||||||
"ipAddressRange": "IP Aralığı",
|
"ipAddressRange": "IP Aralığı",
|
||||||
"rulesErrorFetch": "Kurallar alınamadı",
|
"rulesErrorFetch": "Kurallar alınamadı",
|
||||||
"rulesErrorFetchDescription": "Kurallar alınırken bir hata oluştu",
|
"rulesErrorFetchDescription": "Kurallar alınırken bir hata oluştu",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "Bağlandı",
|
"idpConnectingToFinished": "Bağlandı",
|
||||||
"idpErrorConnectingTo": "{name} ile bağlantı kurarken bir sorun meydana geldi. Lütfen yöneticiye danışın.",
|
"idpErrorConnectingTo": "{name} ile bağlantı kurarken bir sorun meydana geldi. Lütfen yöneticiye danışın.",
|
||||||
"idpErrorNotFound": "IdP bulunamadı",
|
"idpErrorNotFound": "IdP bulunamadı",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "Geçersiz Davet",
|
"inviteInvalid": "Geçersiz Davet",
|
||||||
"inviteInvalidDescription": "Davet bağlantısı geçersiz.",
|
"inviteInvalidDescription": "Davet bağlantısı geçersiz.",
|
||||||
"inviteErrorWrongUser": "Davet bu kullanıcı için değil",
|
"inviteErrorWrongUser": "Davet bu kullanıcı için değil",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "Profesyonel Sürüme Gereklidir",
|
"licenseTierProfessionalRequired": "Profesyonel Sürüme Gereklidir",
|
||||||
"licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.",
|
"licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.",
|
||||||
"actionGetOrg": "Kuruluşu Al",
|
"actionGetOrg": "Kuruluşu Al",
|
||||||
|
"updateOrgUser": "Organizasyon Kullanıcısını Güncelle",
|
||||||
|
"createOrgUser": "Organizasyon Kullanıcısı Oluştur",
|
||||||
"actionUpdateOrg": "Kuruluşu Güncelle",
|
"actionUpdateOrg": "Kuruluşu Güncelle",
|
||||||
"actionUpdateUser": "Kullanıcıyı Güncelle",
|
"actionUpdateUser": "Kullanıcıyı Güncelle",
|
||||||
"actionGetUser": "Kullanıcıyı Getir",
|
"actionGetUser": "Kullanıcıyı Getir",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "Siteyi Sil",
|
"actionDeleteSite": "Siteyi Sil",
|
||||||
"actionGetSite": "Siteyi Al",
|
"actionGetSite": "Siteyi Al",
|
||||||
"actionListSites": "Siteleri Listele",
|
"actionListSites": "Siteleri Listele",
|
||||||
|
"actionApplyBlueprint": "Planı Uygula",
|
||||||
"setupToken": "Kurulum Simgesi",
|
"setupToken": "Kurulum Simgesi",
|
||||||
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
|
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
|
||||||
"setupTokenRequired": "Kurulum simgesi gerekli",
|
"setupTokenRequired": "Kurulum simgesi gerekli",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "Lisans",
|
"sidebarLicense": "Lisans",
|
||||||
"sidebarClients": "Müşteriler (Beta)",
|
"sidebarClients": "Müşteriler (Beta)",
|
||||||
"sidebarDomains": "Alan Adları",
|
"sidebarDomains": "Alan Adları",
|
||||||
"enableDockerSocket": "Docker Soketi Etkinleştir",
|
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
||||||
"enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.",
|
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
||||||
"enableDockerSocketLink": "Daha fazla bilgi",
|
"enableDockerSocketLink": "Daha fazla bilgi",
|
||||||
"viewDockerContainers": "Docker Konteynerlerini Görüntüle",
|
"viewDockerContainers": "Docker Konteynerlerini Görüntüle",
|
||||||
"containersIn": "{siteName} içindeki konteynerler",
|
"containersIn": "{siteName} içindeki konteynerler",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "Güncelleme Mevcut",
|
"newtUpdateAvailable": "Güncelleme Mevcut",
|
||||||
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp",
|
"domainPickerPlaceholder": "myapp.example.com",
|
||||||
"domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.",
|
"domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.",
|
||||||
"domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin",
|
"domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin",
|
||||||
"domainPickerTabAll": "Tümü",
|
"domainPickerTabAll": "Tümü",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "Protokol",
|
"editInternalResourceDialogProtocol": "Protokol",
|
||||||
"editInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
"editInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
"editInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||||
"editInternalResourceDialogDestinationIP": "Hedef IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
|
||||||
"editInternalResourceDialogCancel": "İptal",
|
"editInternalResourceDialogCancel": "İptal",
|
||||||
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
|
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
|
||||||
"editInternalResourceDialogSuccess": "Başarı",
|
"editInternalResourceDialogSuccess": "Başarı",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
"createInternalResourceDialogSitePort": "Site Bağlantı Noktası",
|
||||||
"createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.",
|
"createInternalResourceDialogSitePortDescription": "İstemci ile bağlanıldığında site üzerindeki kaynağa erişmek için bu bağlantı noktasını kullanın.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
"createInternalResourceDialogTargetConfiguration": "Hedef Yapılandırma",
|
||||||
"createInternalResourceDialogDestinationIP": "Hedef IP",
|
"createInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Site ağındaki kaynağın IP adresi.",
|
|
||||||
"createInternalResourceDialogDestinationPort": "Hedef Bağlantı Noktası",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.",
|
"createInternalResourceDialogDestinationPortDescription": "Kaynağa erişilebilecek hedef IP üzerindeki bağlantı noktası.",
|
||||||
"createInternalResourceDialogCancel": "İptal",
|
"createInternalResourceDialogCancel": "İptal",
|
||||||
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
|
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün"
|
"convertButton": "Bu Düğümü Yönetilen Kendi Kendine Barındırma Dönüştürün"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
|
"internationaldomaindetected": "Uluslararası Alan Adı Tespit Edildi",
|
||||||
"willbestoredas": "Şu şekilde depolanacak:"
|
"willbestoredas": "Şu şekilde depolanacak:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC sağlayıcısı",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC sağlayıcısı",
|
||||||
|
"customHeaders": "Özel Başlıklar",
|
||||||
|
"headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.",
|
||||||
|
"domainPickerProvidedDomain": "Sağlanan Alan Adı",
|
||||||
|
"domainPickerFreeProvidedDomain": "Ücretsiz Sağlanan Alan Adı",
|
||||||
|
"domainPickerVerified": "Doğrulandı",
|
||||||
|
"domainPickerUnverified": "Doğrulanmadı",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "Bu alt alan adı geçersiz karakterler veya yapı içeriyor. Kaydettiğinizde otomatik olarak temizlenecektir.",
|
||||||
|
"domainPickerError": "Hata",
|
||||||
|
"domainPickerErrorLoadDomains": "Organizasyon alan adları yüklenemedi",
|
||||||
|
"domainPickerErrorCheckAvailability": "Alan adı kullanılabilirliği kontrol edilemedi",
|
||||||
|
"domainPickerInvalidSubdomain": "Geçersiz alt alan adı",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "Girdi \"{sub}\" geçersiz olduğu için kaldırıldı.",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" {domain} için geçerli yapılamadı.",
|
||||||
|
"domainPickerSubdomainSanitized": "Alt alan adı temizlendi",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" \"{sanitized}\" olarak düzeltildi",
|
||||||
|
"resourceAddEntrypointsEditFile": "Dosyayı düzenle: config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "Dosyayı düzenle: docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,8 @@
|
||||||
"accessRoleErrorAddDescription": "添加用户到角色时出错。",
|
"accessRoleErrorAddDescription": "添加用户到角色时出错。",
|
||||||
"userSaved": "用户已保存",
|
"userSaved": "用户已保存",
|
||||||
"userSavedDescription": "用户已更新。",
|
"userSavedDescription": "用户已更新。",
|
||||||
|
"autoProvisioned": "自动设置",
|
||||||
|
"autoProvisionedDescription": "允许此用户由身份提供商自动管理",
|
||||||
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
|
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
|
||||||
"accessControlsSubmit": "保存访问控制",
|
"accessControlsSubmit": "保存访问控制",
|
||||||
"roles": "角色",
|
"roles": "角色",
|
||||||
|
@ -511,6 +513,7 @@
|
||||||
"ipAddressErrorInvalidFormat": "无效的 IP 地址格式",
|
"ipAddressErrorInvalidFormat": "无效的 IP 地址格式",
|
||||||
"ipAddressErrorInvalidOctet": "无效的 IP 地址",
|
"ipAddressErrorInvalidOctet": "无效的 IP 地址",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
|
"matchPath": "匹配路径",
|
||||||
"ipAddressRange": "IP 范围",
|
"ipAddressRange": "IP 范围",
|
||||||
"rulesErrorFetch": "获取规则失败",
|
"rulesErrorFetch": "获取规则失败",
|
||||||
"rulesErrorFetchDescription": "获取规则时出错",
|
"rulesErrorFetchDescription": "获取规则时出错",
|
||||||
|
@ -911,6 +914,8 @@
|
||||||
"idpConnectingToFinished": "已连接",
|
"idpConnectingToFinished": "已连接",
|
||||||
"idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。",
|
"idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。",
|
||||||
"idpErrorNotFound": "找不到 IdP",
|
"idpErrorNotFound": "找不到 IdP",
|
||||||
|
"idpGoogleAlt": "Google",
|
||||||
|
"idpAzureAlt": "Azure",
|
||||||
"inviteInvalid": "无效邀请",
|
"inviteInvalid": "无效邀请",
|
||||||
"inviteInvalidDescription": "邀请链接无效。",
|
"inviteInvalidDescription": "邀请链接无效。",
|
||||||
"inviteErrorWrongUser": "邀请不是该用户的",
|
"inviteErrorWrongUser": "邀请不是该用户的",
|
||||||
|
@ -982,6 +987,8 @@
|
||||||
"licenseTierProfessionalRequired": "需要专业版",
|
"licenseTierProfessionalRequired": "需要专业版",
|
||||||
"licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。",
|
"licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。",
|
||||||
"actionGetOrg": "获取组织",
|
"actionGetOrg": "获取组织",
|
||||||
|
"updateOrgUser": "更新组织用户",
|
||||||
|
"createOrgUser": "创建组织用户",
|
||||||
"actionUpdateOrg": "更新组织",
|
"actionUpdateOrg": "更新组织",
|
||||||
"actionUpdateUser": "更新用户",
|
"actionUpdateUser": "更新用户",
|
||||||
"actionGetUser": "获取用户",
|
"actionGetUser": "获取用户",
|
||||||
|
@ -991,6 +998,7 @@
|
||||||
"actionDeleteSite": "删除站点",
|
"actionDeleteSite": "删除站点",
|
||||||
"actionGetSite": "获取站点",
|
"actionGetSite": "获取站点",
|
||||||
"actionListSites": "站点列表",
|
"actionListSites": "站点列表",
|
||||||
|
"actionApplyBlueprint": "应用蓝图",
|
||||||
"setupToken": "设置令牌",
|
"setupToken": "设置令牌",
|
||||||
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
||||||
"setupTokenRequired": "需要设置令牌",
|
"setupTokenRequired": "需要设置令牌",
|
||||||
|
@ -1133,8 +1141,8 @@
|
||||||
"sidebarLicense": "证书",
|
"sidebarLicense": "证书",
|
||||||
"sidebarClients": "客户端(测试版)",
|
"sidebarClients": "客户端(测试版)",
|
||||||
"sidebarDomains": "域",
|
"sidebarDomains": "域",
|
||||||
"enableDockerSocket": "启用停靠套接字",
|
"enableDockerSocket": "启用 Docker 蓝图",
|
||||||
"enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。",
|
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
||||||
"enableDockerSocketLink": "了解更多",
|
"enableDockerSocketLink": "了解更多",
|
||||||
"viewDockerContainers": "查看停靠容器",
|
"viewDockerContainers": "查看停靠容器",
|
||||||
"containersIn": "{siteName} 中的容器",
|
"containersIn": "{siteName} 中的容器",
|
||||||
|
@ -1234,7 +1242,7 @@
|
||||||
"newtUpdateAvailable": "更新可用",
|
"newtUpdateAvailable": "更新可用",
|
||||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
||||||
"domainPickerEnterDomain": "域名",
|
"domainPickerEnterDomain": "域名",
|
||||||
"domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp",
|
"domainPickerPlaceholder": "example.com",
|
||||||
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
|
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
|
||||||
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
|
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
|
||||||
"domainPickerTabAll": "所有",
|
"domainPickerTabAll": "所有",
|
||||||
|
@ -1392,8 +1400,6 @@
|
||||||
"editInternalResourceDialogProtocol": "协议",
|
"editInternalResourceDialogProtocol": "协议",
|
||||||
"editInternalResourceDialogSitePort": "站点端口",
|
"editInternalResourceDialogSitePort": "站点端口",
|
||||||
"editInternalResourceDialogTargetConfiguration": "目标配置",
|
"editInternalResourceDialogTargetConfiguration": "目标配置",
|
||||||
"editInternalResourceDialogDestinationIP": "目标IP",
|
|
||||||
"editInternalResourceDialogDestinationPort": "目标端口",
|
|
||||||
"editInternalResourceDialogCancel": "取消",
|
"editInternalResourceDialogCancel": "取消",
|
||||||
"editInternalResourceDialogSaveResource": "保存资源",
|
"editInternalResourceDialogSaveResource": "保存资源",
|
||||||
"editInternalResourceDialogSuccess": "成功",
|
"editInternalResourceDialogSuccess": "成功",
|
||||||
|
@ -1424,9 +1430,7 @@
|
||||||
"createInternalResourceDialogSitePort": "站点端口",
|
"createInternalResourceDialogSitePort": "站点端口",
|
||||||
"createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。",
|
"createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。",
|
||||||
"createInternalResourceDialogTargetConfiguration": "目标配置",
|
"createInternalResourceDialogTargetConfiguration": "目标配置",
|
||||||
"createInternalResourceDialogDestinationIP": "目标IP",
|
"createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP或主机名地址。",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP地址。",
|
|
||||||
"createInternalResourceDialogDestinationPort": "目标端口",
|
|
||||||
"createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。",
|
"createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。",
|
||||||
"createInternalResourceDialogCancel": "取消",
|
"createInternalResourceDialogCancel": "取消",
|
||||||
"createInternalResourceDialogCreateResource": "创建资源",
|
"createInternalResourceDialogCreateResource": "创建资源",
|
||||||
|
@ -1496,5 +1500,24 @@
|
||||||
"convertButton": "将此节点转换为管理自托管的"
|
"convertButton": "将此节点转换为管理自托管的"
|
||||||
},
|
},
|
||||||
"internationaldomaindetected": "检测到国际域",
|
"internationaldomaindetected": "检测到国际域",
|
||||||
"willbestoredas": "储存为:"
|
"willbestoredas": "储存为:",
|
||||||
|
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
||||||
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
|
"customHeaders": "自定义标题",
|
||||||
|
"headersValidationError": "头部必须是格式:头部名称:值。",
|
||||||
|
"domainPickerProvidedDomain": "提供的域",
|
||||||
|
"domainPickerFreeProvidedDomain": "免费提供的域",
|
||||||
|
"domainPickerVerified": "已验证",
|
||||||
|
"domainPickerUnverified": "未验证",
|
||||||
|
"domainPickerInvalidSubdomainStructure": "此子域包含无效的字符或结构。当您保存时,它将被自动清除。",
|
||||||
|
"domainPickerError": "错误",
|
||||||
|
"domainPickerErrorLoadDomains": "加载组织域名失败",
|
||||||
|
"domainPickerErrorCheckAvailability": "检查域可用性失败",
|
||||||
|
"domainPickerInvalidSubdomain": "无效的子域",
|
||||||
|
"domainPickerInvalidSubdomainRemoved": "输入 \"{sub}\" 已被移除,因为其无效。",
|
||||||
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" 无法为 {domain} 变为有效。",
|
||||||
|
"domainPickerSubdomainSanitized": "子域已净化",
|
||||||
|
"domainPickerSubdomainCorrected": "\"{sub}\" 已被更正为 \"{sanitized}\"",
|
||||||
|
"resourceAddEntrypointsEditFile": "编辑文件:config/traefik/traefik_config.yml",
|
||||||
|
"resourceExposePortsEditFile": "编辑文件:docker-compose.yml"
|
||||||
}
|
}
|
||||||
|
|
2264
package-lock.json
generated
2264
package-lock.json
generated
File diff suppressed because it is too large
Load diff
53
package.json
53
package.json
|
@ -21,13 +21,13 @@
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
"start": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||||
"email": "email dev --dir server/emails/templates --port 3005",
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "4.1.3",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
|
@ -49,15 +49,15 @@
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "0.5.0",
|
"@react-email/components": "0.5.3",
|
||||||
"@react-email/render": "^1.2.0",
|
"@react-email/render": "^1.2.0",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "1.2.2",
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.2",
|
||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"axios": "1.11.0",
|
"axios": "^1.12.2",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"canvas-confetti": "1.9.3",
|
"canvas-confetti": "1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
@ -68,11 +68,11 @@
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"drizzle-orm": "0.44.4",
|
"drizzle-orm": "0.44.5",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.35.0",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "15.5.3",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "8.0.1",
|
"express-rate-limit": "8.1.0",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
|
@ -81,30 +81,29 @@
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "0.539.0",
|
"lucide-react": "^0.544.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.4.6",
|
"next": "15.5.3",
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.9",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.5",
|
"nodemailer": "7.0.6",
|
||||||
"npm": "^11.5.2",
|
"npm": "^11.6.0",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.7.0",
|
||||||
"react-hook-form": "7.62.0",
|
"react-hook-form": "7.62.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"source-map-support": "0.5.21",
|
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
|
@ -114,9 +113,9 @@
|
||||||
"zod-validation-error": "3.5.2"
|
"zod-validation-error": "3.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.49.0",
|
"@dotenvx/dotenvx": "1.49.1",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
|
@ -126,25 +125,25 @@
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24",
|
"@types/node": "24.5.2",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "7.0.1",
|
||||||
"@types/pg": "8.15.5",
|
"@types/pg": "8.15.5",
|
||||||
"@types/react": "19.1.12",
|
"@types/react": "19.1.13",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"drizzle-kit": "0.31.4",
|
"drizzle-kit": "0.31.4",
|
||||||
"esbuild": "0.25.9",
|
"esbuild": "0.25.10",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.2.8",
|
"react-email": "4.2.11",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.20.5",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.40.0"
|
"typescript-eslint": "^8.44.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|
BIN
public/idp/azure.png
Normal file
BIN
public/idp/azure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
BIN
public/idp/google.png
Normal file
BIN
public/idp/google.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -101,7 +101,9 @@ export enum ActionsEnum {
|
||||||
getApiKey = "getApiKey",
|
getApiKey = "getApiKey",
|
||||||
createOrgDomain = "createOrgDomain",
|
createOrgDomain = "createOrgDomain",
|
||||||
deleteOrgDomain = "deleteOrgDomain",
|
deleteOrgDomain = "deleteOrgDomain",
|
||||||
restartOrgDomain = "restartOrgDomain"
|
restartOrgDomain = "restartOrgDomain",
|
||||||
|
updateOrgUser = "updateOrgUser",
|
||||||
|
applyBlueprint = "applyBlueprint"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { db } from "@server/db";
|
import { db, resources, siteResources } from "@server/db";
|
||||||
import { exitNodes, sites } from "@server/db";
|
import { exitNodes, sites } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { __DIRNAME } from "@server/lib/consts";
|
import { __DIRNAME } from "@server/lib/consts";
|
||||||
|
@ -34,6 +34,44 @@ export async function getUniqueSiteName(orgId: string): Promise<string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUniqueResourceName(orgId: string): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
const count = await db
|
||||||
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
|
.from(resources)
|
||||||
|
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId)));
|
||||||
|
if (count.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
const count = await db
|
||||||
|
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)));
|
||||||
|
if (count.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
const count = await db
|
const count = await db
|
||||||
|
|
|
@ -50,3 +50,4 @@ function createDb() {
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
|
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
|
@ -71,6 +71,7 @@ export const resources = pgTable("resources", {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subdomain: varchar("subdomain"),
|
subdomain: varchar("subdomain"),
|
||||||
fullDomain: varchar("fullDomain"),
|
fullDomain: varchar("fullDomain"),
|
||||||
|
@ -95,6 +96,7 @@ export const resources = pgTable("resources", {
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
|
@ -113,7 +115,9 @@ export const targets = pgTable("targets", {
|
||||||
method: varchar("method"),
|
method: varchar("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: boolean("enabled").notNull().default(true)
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
path: text("path"),
|
||||||
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
|
@ -127,7 +131,8 @@ export const exitNodes = pgTable("exitNodes", {
|
||||||
maxConnections: integer("maxConnections"),
|
maxConnections: integer("maxConnections"),
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
||||||
|
region: varchar("region")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteResources = pgTable("siteResources", { // this is for the clients
|
export const siteResources = pgTable("siteResources", { // this is for the clients
|
||||||
|
@ -138,6 +143,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
protocol: varchar("protocol").notNull(),
|
protocol: varchar("protocol").notNull(),
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
|
@ -212,7 +218,8 @@ export const userOrgs = pgTable("userOrgs", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: boolean("isOwner").notNull().default(false)
|
isOwner: boolean("isOwner").notNull().default(false),
|
||||||
|
autoProvisioned: boolean("autoProvisioned").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||||
|
@ -458,6 +465,7 @@ export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||||
|
variant: varchar("variant").notNull().default("oidc"),
|
||||||
clientId: varchar("clientId").notNull(),
|
clientId: varchar("clientId").notNull(),
|
||||||
clientSecret: varchar("clientSecret").notNull(),
|
clientSecret: varchar("clientSecret").notNull(),
|
||||||
authUrl: varchar("authUrl").notNull(),
|
authUrl: varchar("authUrl").notNull(),
|
||||||
|
|
|
@ -18,6 +18,7 @@ function createDb() {
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
|
export type Transaction = Parameters<Parameters<typeof db["transaction"]>[0]>[0];
|
||||||
|
|
||||||
function checkFileExists(filePath: string): boolean {
|
function checkFileExists(filePath: string): boolean {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -77,6 +77,7 @@ export const resources = sqliteTable("resources", {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subdomain: text("subdomain"),
|
subdomain: text("subdomain"),
|
||||||
fullDomain: text("fullDomain"),
|
fullDomain: text("fullDomain"),
|
||||||
|
@ -107,6 +108,7 @@ export const resources = sqliteTable("resources", {
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
|
@ -125,7 +127,9 @@ export const targets = sqliteTable("targets", {
|
||||||
method: text("method"),
|
method: text("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
path: text("path"),
|
||||||
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = sqliteTable("exitNodes", {
|
export const exitNodes = sqliteTable("exitNodes", {
|
||||||
|
@ -139,23 +143,28 @@ export const exitNodes = sqliteTable("exitNodes", {
|
||||||
maxConnections: integer("maxConnections"),
|
maxConnections: integer("maxConnections"),
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
lastPing: integer("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
type: text("type").default("gerbil"), // gerbil, remoteExitNode
|
||||||
|
region: text("region")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteResources = sqliteTable("siteResources", { // this is for the clients
|
export const siteResources = sqliteTable("siteResources", {
|
||||||
siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }),
|
// this is for the clients
|
||||||
|
siteResourceId: integer("siteResourceId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
protocol: text("protocol").notNull(),
|
protocol: text("protocol").notNull(),
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
destinationPort: integer("destinationPort").notNull(),
|
||||||
destinationIp: text("destinationIp").notNull(),
|
destinationIp: text("destinationIp").notNull(),
|
||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
|
@ -259,7 +268,9 @@ export const clientSites = sqliteTable("clientSites", {
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false),
|
isRelayed: integer("isRelayed", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
endpoint: text("endpoint")
|
endpoint: text("endpoint")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -317,7 +328,10 @@ export const userOrgs = sqliteTable("userOrgs", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false)
|
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||||
|
autoProvisioned: integer("autoProvisioned", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
|
||||||
|
@ -594,6 +608,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
|
||||||
idpOauthConfigId: integer("idpOauthConfigId").primaryKey({
|
idpOauthConfigId: integer("idpOauthConfigId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
|
variant: text("variant").notNull().default("oidc"),
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => idp.idpId, { onDelete: "cascade" }),
|
.references(() => idp.idpId, { onDelete: "cascade" }),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
import "./extendZod.ts";
|
import "./extendZod.ts";
|
||||||
import 'source-map-support/register.js'
|
|
||||||
|
|
||||||
import { runSetupFunctions } from "./setup";
|
import { runSetupFunctions } from "./setup";
|
||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
|
|
170
server/lib/blueprints/applyBlueprint.ts
Normal file
170
server/lib/blueprints/applyBlueprint.ts
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import { db, newts, Target } from "@server/db";
|
||||||
|
import { Config, ConfigSchema } from "./types";
|
||||||
|
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { resources, targets, sites } from "@server/db";
|
||||||
|
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||||
|
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
|
||||||
|
import { addTargets as addClientTargets } from "@server/routers/client/targets";
|
||||||
|
import {
|
||||||
|
ClientResourcesResults,
|
||||||
|
updateClientResources
|
||||||
|
} from "./clientResources";
|
||||||
|
|
||||||
|
export async function applyBlueprint(
|
||||||
|
orgId: string,
|
||||||
|
configData: unknown,
|
||||||
|
siteId?: number
|
||||||
|
): Promise<void> {
|
||||||
|
// Validate the input data
|
||||||
|
const validationResult = ConfigSchema.safeParse(configData);
|
||||||
|
if (!validationResult.success) {
|
||||||
|
throw new Error(fromError(validationResult.error).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = validationResult.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let proxyResourcesResults: ProxyResourcesResults = [];
|
||||||
|
let clientResourcesResults: ClientResourcesResults = [];
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
proxyResourcesResults = await updateProxyResources(
|
||||||
|
orgId,
|
||||||
|
config,
|
||||||
|
trx,
|
||||||
|
siteId
|
||||||
|
);
|
||||||
|
clientResourcesResults = await updateClientResources(
|
||||||
|
orgId,
|
||||||
|
config,
|
||||||
|
trx,
|
||||||
|
siteId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to update the targets on the newts from the successfully updated information
|
||||||
|
for (const result of proxyResourcesResults) {
|
||||||
|
for (const target of result.targetsToUpdate) {
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.siteId, target.siteId),
|
||||||
|
eq(sites.orgId, orgId),
|
||||||
|
eq(sites.type, "newt"),
|
||||||
|
isNotNull(sites.pubKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
logger.debug(
|
||||||
|
`Updating target ${target.targetId} on site ${site.sites.siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await addProxyTargets(
|
||||||
|
site.newt.newtId,
|
||||||
|
[target],
|
||||||
|
result.proxyResource.protocol,
|
||||||
|
result.proxyResource.proxyPort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to update the targets on the newts from the successfully updated information
|
||||||
|
for (const result of clientResourcesResults) {
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.siteId, result.resource.siteId),
|
||||||
|
eq(sites.orgId, orgId),
|
||||||
|
eq(sites.type, "newt"),
|
||||||
|
isNotNull(sites.pubKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
logger.debug(
|
||||||
|
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await addClientTargets(
|
||||||
|
site.newt.newtId,
|
||||||
|
result.resource.destinationIp,
|
||||||
|
result.resource.destinationPort,
|
||||||
|
result.resource.protocol,
|
||||||
|
result.resource.proxyPort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update database from config: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// await updateDatabaseFromConfig("org_i21aifypnlyxur2", {
|
||||||
|
// resources: {
|
||||||
|
// "resource-nice-id": {
|
||||||
|
// name: "this is my resource",
|
||||||
|
// protocol: "http",
|
||||||
|
// "full-domain": "level1.test.example.com",
|
||||||
|
// "host-header": "example.com",
|
||||||
|
// "tls-server-name": "example.com",
|
||||||
|
// auth: {
|
||||||
|
// pincode: 123456,
|
||||||
|
// password: "sadfasdfadsf",
|
||||||
|
// "sso-enabled": true,
|
||||||
|
// "sso-roles": ["Member"],
|
||||||
|
// "sso-users": ["owen@fossorial.io"],
|
||||||
|
// "whitelist-users": ["owen@fossorial.io"]
|
||||||
|
// },
|
||||||
|
// targets: [
|
||||||
|
// {
|
||||||
|
// site: "glossy-plains-viscacha-rat",
|
||||||
|
// hostname: "localhost",
|
||||||
|
// method: "http",
|
||||||
|
// port: 8000,
|
||||||
|
// healthcheck: {
|
||||||
|
// port: 8000,
|
||||||
|
// hostname: "localhost"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// site: "glossy-plains-viscacha-rat",
|
||||||
|
// hostname: "localhost",
|
||||||
|
// method: "http",
|
||||||
|
// port: 8001
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "resource-nice-id2": {
|
||||||
|
// name: "http server",
|
||||||
|
// protocol: "tcp",
|
||||||
|
// "proxy-port": 3000,
|
||||||
|
// targets: [
|
||||||
|
// {
|
||||||
|
// site: "glossy-plains-viscacha-rat",
|
||||||
|
// hostname: "localhost",
|
||||||
|
// port: 3000,
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
53
server/lib/blueprints/applyNewtDockerBlueprint.ts
Normal file
53
server/lib/blueprints/applyNewtDockerBlueprint.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { sendToClient } from "@server/routers/ws";
|
||||||
|
import { processContainerLabels } from "./parseDockerContainers";
|
||||||
|
import { applyBlueprint } from "./applyBlueprint";
|
||||||
|
import { db, sites } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export async function applyNewtDockerBlueprint(
|
||||||
|
siteId: number,
|
||||||
|
newtId: string,
|
||||||
|
containers: any
|
||||||
|
) {
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn("Site not found in applyNewtDockerBlueprint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// logger.debug(`Applying Docker blueprint to site: ${siteId}`);
|
||||||
|
// logger.debug(`Containers: ${JSON.stringify(containers, null, 2)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blueprint = processContainerLabels(containers);
|
||||||
|
|
||||||
|
logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
|
||||||
|
|
||||||
|
// Update the blueprint in the database
|
||||||
|
await applyBlueprint(site.orgId, blueprint, site.siteId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update database from config: ${error}`);
|
||||||
|
await sendToClient(newtId, {
|
||||||
|
type: "newt/blueprint/results",
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to update database from config: ${error}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClient(newtId, {
|
||||||
|
type: "newt/blueprint/results",
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
message: "Config updated successfully"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
117
server/lib/blueprints/clientResources.ts
Normal file
117
server/lib/blueprints/clientResources.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import {
|
||||||
|
SiteResource,
|
||||||
|
siteResources,
|
||||||
|
Transaction,
|
||||||
|
} from "@server/db";
|
||||||
|
import { sites } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
} from "./types";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export type ClientResourcesResults = {
|
||||||
|
resource: SiteResource;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export async function updateClientResources(
|
||||||
|
orgId: string,
|
||||||
|
config: Config,
|
||||||
|
trx: Transaction,
|
||||||
|
siteId?: number
|
||||||
|
): Promise<ClientResourcesResults> {
|
||||||
|
const results: ClientResourcesResults = [];
|
||||||
|
|
||||||
|
for (const [resourceNiceId, resourceData] of Object.entries(
|
||||||
|
config["client-resources"]
|
||||||
|
)) {
|
||||||
|
const [existingResource] = await trx
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.niceId, resourceNiceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const resourceSiteId = resourceData.site;
|
||||||
|
let site;
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
throw new Error(
|
||||||
|
`Site not found: ${resourceSiteId} in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingResource) {
|
||||||
|
// Update existing resource
|
||||||
|
const [updatedResource] = await trx
|
||||||
|
.update(siteResources)
|
||||||
|
.set({
|
||||||
|
name: resourceData.name || resourceNiceId,
|
||||||
|
siteId: site.siteId,
|
||||||
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
|
destinationIp: resourceData.hostname,
|
||||||
|
destinationPort: resourceData["internal-port"],
|
||||||
|
protocol: resourceData.protocol
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
existingResource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
results.push({ resource: updatedResource });
|
||||||
|
} else {
|
||||||
|
// Create new resource
|
||||||
|
const [newResource] = await trx
|
||||||
|
.insert(siteResources)
|
||||||
|
.values({
|
||||||
|
orgId: orgId,
|
||||||
|
siteId: site.siteId,
|
||||||
|
niceId: resourceNiceId,
|
||||||
|
name: resourceData.name || resourceNiceId,
|
||||||
|
proxyPort: resourceData["proxy-port"]!,
|
||||||
|
destinationIp: resourceData.hostname,
|
||||||
|
destinationPort: resourceData["internal-port"],
|
||||||
|
protocol: resourceData.protocol
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
results.push({ resource: newResource });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
301
server/lib/blueprints/parseDockerContainers.ts
Normal file
301
server/lib/blueprints/parseDockerContainers.ts
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { setNestedProperty } from "./parseDotNotation";
|
||||||
|
|
||||||
|
export type DockerLabels = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsedObject = {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContainerPort = {
|
||||||
|
privatePort: number;
|
||||||
|
publicPort: number;
|
||||||
|
type: string;
|
||||||
|
ip: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Container = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
ports: ContainerPort[] | null;
|
||||||
|
labels: DockerLabels;
|
||||||
|
created: number;
|
||||||
|
networks: { [key: string]: any };
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Target = {
|
||||||
|
hostname?: string;
|
||||||
|
port?: number;
|
||||||
|
method?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResourceConfig = {
|
||||||
|
[key: string]: any;
|
||||||
|
targets?: (Target | null)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getContainerPort(container: Container): number | null {
|
||||||
|
if (!container.ports || container.ports.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Return the first port's privatePort
|
||||||
|
return container.ports[0].privatePort;
|
||||||
|
// return container.ports[0].publicPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processContainerLabels(containers: Container[]): {
|
||||||
|
"proxy-resources": { [key: string]: ResourceConfig };
|
||||||
|
"client-resources": { [key: string]: ResourceConfig };
|
||||||
|
} {
|
||||||
|
const result = {
|
||||||
|
"proxy-resources": {} as { [key: string]: ResourceConfig },
|
||||||
|
"client-resources": {} as { [key: string]: ResourceConfig }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process each container
|
||||||
|
containers.forEach((container) => {
|
||||||
|
if (container.state !== "running") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyResourceLabels: DockerLabels = {};
|
||||||
|
const clientResourceLabels: DockerLabels = {};
|
||||||
|
|
||||||
|
// Filter and separate proxy-resources and client-resources labels
|
||||||
|
Object.entries(container.labels).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith("pangolin.proxy-resources.")) {
|
||||||
|
// remove the pangolin.proxy- prefix to get "resources.xxx"
|
||||||
|
const strippedKey = key.replace("pangolin.proxy-", "");
|
||||||
|
proxyResourceLabels[strippedKey] = value;
|
||||||
|
} else if (key.startsWith("pangolin.client-resources.")) {
|
||||||
|
// remove the pangolin.client- prefix to get "resources.xxx"
|
||||||
|
const strippedKey = key.replace("pangolin.client-", "");
|
||||||
|
clientResourceLabels[strippedKey] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process proxy resources
|
||||||
|
if (Object.keys(proxyResourceLabels).length > 0) {
|
||||||
|
processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process client resources
|
||||||
|
if (Object.keys(clientResourceLabels).length > 0) {
|
||||||
|
processResourceLabels(clientResourceLabels, container, result["client-resources"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processResourceLabels(
|
||||||
|
resourceLabels: DockerLabels,
|
||||||
|
container: Container,
|
||||||
|
targetResult: { [key: string]: ResourceConfig }
|
||||||
|
) {
|
||||||
|
// Parse the labels using the existing parseDockerLabels logic
|
||||||
|
const tempResult: ParsedObject = {};
|
||||||
|
Object.entries(resourceLabels).forEach(([key, value]) => {
|
||||||
|
setNestedProperty(tempResult, key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge into target result
|
||||||
|
if (tempResult.resources) {
|
||||||
|
Object.entries(tempResult.resources).forEach(
|
||||||
|
([resourceKey, resourceConfig]: [string, any]) => {
|
||||||
|
// Initialize resource if it doesn't exist
|
||||||
|
if (!targetResult[resourceKey]) {
|
||||||
|
targetResult[resourceKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge all properties except targets
|
||||||
|
Object.entries(resourceConfig).forEach(
|
||||||
|
([propKey, propValue]) => {
|
||||||
|
if (propKey !== "targets") {
|
||||||
|
targetResult[resourceKey][propKey] = propValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle targets specially
|
||||||
|
if (
|
||||||
|
resourceConfig.targets &&
|
||||||
|
Array.isArray(resourceConfig.targets)
|
||||||
|
) {
|
||||||
|
const resource = targetResult[resourceKey];
|
||||||
|
if (resource) {
|
||||||
|
if (!resource.targets) {
|
||||||
|
resource.targets = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceConfig.targets.forEach(
|
||||||
|
(target: any, targetIndex: number) => {
|
||||||
|
// check if the target is an empty object
|
||||||
|
if (
|
||||||
|
typeof target === "object" &&
|
||||||
|
Object.keys(target).length === 0
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping null target at index ${targetIndex} for resource ${resourceKey}`
|
||||||
|
);
|
||||||
|
resource.targets!.push(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure targets array is long enough
|
||||||
|
while (
|
||||||
|
resource.targets!.length <= targetIndex
|
||||||
|
) {
|
||||||
|
resource.targets!.push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default hostname and port if not provided
|
||||||
|
const finalTarget = { ...target };
|
||||||
|
if (!finalTarget.hostname) {
|
||||||
|
finalTarget.hostname =
|
||||||
|
container.name ||
|
||||||
|
container.hostname;
|
||||||
|
}
|
||||||
|
if (!finalTarget.port) {
|
||||||
|
const containerPort =
|
||||||
|
getContainerPort(container);
|
||||||
|
if (containerPort !== null) {
|
||||||
|
finalTarget.port = containerPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge with existing target data
|
||||||
|
resource.targets![targetIndex] = {
|
||||||
|
...resource.targets![targetIndex],
|
||||||
|
...finalTarget
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Test example
|
||||||
|
// const testContainers: Container[] = [
|
||||||
|
// {
|
||||||
|
// id: "57e056cb0e3a",
|
||||||
|
// name: "nginx1",
|
||||||
|
// image: "nginxdemos/hello",
|
||||||
|
// state: "running",
|
||||||
|
// status: "Up 4 days",
|
||||||
|
// ports: [
|
||||||
|
// {
|
||||||
|
// privatePort: 80,
|
||||||
|
// publicPort: 8000,
|
||||||
|
// type: "tcp",
|
||||||
|
// ip: "0.0.0.0"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// labels: {
|
||||||
|
// "resources.nginx.name": "nginx",
|
||||||
|
// "resources.nginx.full-domain": "nginx.example.com",
|
||||||
|
// "resources.nginx.protocol": "http",
|
||||||
|
// "resources.nginx.targets[0].enabled": "true"
|
||||||
|
// },
|
||||||
|
// created: 1756942725,
|
||||||
|
// networks: {
|
||||||
|
// owen_default: {
|
||||||
|
// networkId:
|
||||||
|
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// hostname: "57e056cb0e3a"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "58e056cb0e3b",
|
||||||
|
// name: "nginx2",
|
||||||
|
// image: "nginxdemos/hello",
|
||||||
|
// state: "running",
|
||||||
|
// status: "Up 4 days",
|
||||||
|
// ports: [
|
||||||
|
// {
|
||||||
|
// privatePort: 80,
|
||||||
|
// publicPort: 8001,
|
||||||
|
// type: "tcp",
|
||||||
|
// ip: "0.0.0.0"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// labels: {
|
||||||
|
// "resources.nginx.name": "nginx",
|
||||||
|
// "resources.nginx.full-domain": "nginx.example.com",
|
||||||
|
// "resources.nginx.protocol": "http",
|
||||||
|
// "resources.nginx.targets[1].enabled": "true"
|
||||||
|
// },
|
||||||
|
// created: 1756942726,
|
||||||
|
// networks: {
|
||||||
|
// owen_default: {
|
||||||
|
// networkId:
|
||||||
|
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// hostname: "58e056cb0e3b"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "59e056cb0e3c",
|
||||||
|
// name: "api-server",
|
||||||
|
// image: "my-api:latest",
|
||||||
|
// state: "running",
|
||||||
|
// status: "Up 2 days",
|
||||||
|
// ports: [
|
||||||
|
// {
|
||||||
|
// privatePort: 3000,
|
||||||
|
// publicPort: 3000,
|
||||||
|
// type: "tcp",
|
||||||
|
// ip: "0.0.0.0"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// labels: {
|
||||||
|
// "resources.api.name": "API Server",
|
||||||
|
// "resources.api.protocol": "http",
|
||||||
|
// "resources.api.targets[0].enabled": "true",
|
||||||
|
// "resources.api.targets[0].hostname": "custom-host",
|
||||||
|
// "resources.api.targets[0].port": "3001"
|
||||||
|
// },
|
||||||
|
// created: 1756942727,
|
||||||
|
// networks: {
|
||||||
|
// owen_default: {
|
||||||
|
// networkId:
|
||||||
|
// "cb131c0f1d5d8ef7158660e77fc370508f5a563e1f9829b53a1945ae3725b58c"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// hostname: "59e056cb0e3c"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "d0e29b08361c",
|
||||||
|
// name: "beautiful_wilson",
|
||||||
|
// image: "bolkedebruin/rdpgw:latest",
|
||||||
|
// state: "exited",
|
||||||
|
// status: "Exited (0) 4 hours ago",
|
||||||
|
// ports: null,
|
||||||
|
// labels: {},
|
||||||
|
// created: 1757359039,
|
||||||
|
// networks: {
|
||||||
|
// bridge: {
|
||||||
|
// networkId:
|
||||||
|
// "ea7f56dfc9cc476b8a3560b5b570d0fe8a6a2bc5e8343ab1ed37822086e89687"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// hostname: "d0e29b08361c"
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// // Test the function
|
||||||
|
// const result = processContainerLabels(testContainers);
|
||||||
|
// console.log("Processed result:");
|
||||||
|
// console.log(JSON.stringify(result, null, 2));
|
109
server/lib/blueprints/parseDotNotation.ts
Normal file
109
server/lib/blueprints/parseDotNotation.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
export function setNestedProperty(obj: any, path: string, value: string): void {
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
|
||||||
|
// Handle array notation like "targets[0]"
|
||||||
|
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
||||||
|
|
||||||
|
if (arrayMatch) {
|
||||||
|
const [, arrayKey, indexStr] = arrayMatch;
|
||||||
|
const index = parseInt(indexStr, 10);
|
||||||
|
|
||||||
|
// Initialize array if it doesn't exist
|
||||||
|
if (!current[arrayKey]) {
|
||||||
|
current[arrayKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure array is long enough
|
||||||
|
while (current[arrayKey].length <= index) {
|
||||||
|
current[arrayKey].push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[arrayKey][index];
|
||||||
|
} else {
|
||||||
|
// Regular object property
|
||||||
|
if (!current[key]) {
|
||||||
|
current[key] = {};
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the final value
|
||||||
|
const finalKey = keys[keys.length - 1];
|
||||||
|
const arrayMatch = finalKey.match(/^(.+)\[(\d+)\]$/);
|
||||||
|
|
||||||
|
if (arrayMatch) {
|
||||||
|
const [, arrayKey, indexStr] = arrayMatch;
|
||||||
|
const index = parseInt(indexStr, 10);
|
||||||
|
|
||||||
|
if (!current[arrayKey]) {
|
||||||
|
current[arrayKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure array is long enough
|
||||||
|
while (current[arrayKey].length <= index) {
|
||||||
|
current[arrayKey].push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
current[arrayKey][index] = convertValue(value);
|
||||||
|
} else {
|
||||||
|
current[finalKey] = convertValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert string values to appropriate types
|
||||||
|
export function convertValue(value: string): any {
|
||||||
|
// Convert boolean strings
|
||||||
|
if (value === "true") return true;
|
||||||
|
if (value === "false") return false;
|
||||||
|
|
||||||
|
// Convert numeric strings
|
||||||
|
if (/^\d+$/.test(value)) {
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d*\.\d+$/.test(value)) {
|
||||||
|
const num = parseFloat(value);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Example usage:
|
||||||
|
// const dockerLabels: DockerLabels = {
|
||||||
|
// "resources.resource-nice-id.name": "this is my resource",
|
||||||
|
// "resources.resource-nice-id.protocol": "http",
|
||||||
|
// "resources.resource-nice-id.full-domain": "level1.test3.example.com",
|
||||||
|
// "resources.resource-nice-id.host-header": "example.com",
|
||||||
|
// "resources.resource-nice-id.tls-server-name": "example.com",
|
||||||
|
// "resources.resource-nice-id.auth.pincode": "123456",
|
||||||
|
// "resources.resource-nice-id.auth.password": "sadfasdfadsf",
|
||||||
|
// "resources.resource-nice-id.auth.sso-enabled": "true",
|
||||||
|
// "resources.resource-nice-id.auth.sso-roles[0]": "Member",
|
||||||
|
// "resources.resource-nice-id.auth.sso-users[0]": "owen@fossorial.io",
|
||||||
|
// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@fossorial.io",
|
||||||
|
// "resources.resource-nice-id.targets[0].hostname": "localhost",
|
||||||
|
// "resources.resource-nice-id.targets[0].method": "http",
|
||||||
|
// "resources.resource-nice-id.targets[0].port": "8000",
|
||||||
|
// "resources.resource-nice-id.targets[0].healthcheck.port": "8000",
|
||||||
|
// "resources.resource-nice-id.targets[0].healthcheck.hostname": "localhost",
|
||||||
|
// "resources.resource-nice-id.targets[1].hostname": "localhost",
|
||||||
|
// "resources.resource-nice-id.targets[1].method": "http",
|
||||||
|
// "resources.resource-nice-id.targets[1].port": "8001",
|
||||||
|
// "resources.resource-nice-id2.name": "this is other resource",
|
||||||
|
// "resources.resource-nice-id2.protocol": "tcp",
|
||||||
|
// "resources.resource-nice-id2.proxy-port": "3000",
|
||||||
|
// "resources.resource-nice-id2.targets[0].hostname": "localhost",
|
||||||
|
// "resources.resource-nice-id2.targets[0].port": "3000"
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Parse the labels
|
||||||
|
// const parsed = parseDockerLabels(dockerLabels);
|
||||||
|
// console.log(JSON.stringify(parsed, null, 2));
|
885
server/lib/blueprints/proxyResources.ts
Normal file
885
server/lib/blueprints/proxyResources.ts
Normal file
|
@ -0,0 +1,885 @@
|
||||||
|
import {
|
||||||
|
domains,
|
||||||
|
orgDomains,
|
||||||
|
Resource,
|
||||||
|
resourcePincode,
|
||||||
|
resourceRules,
|
||||||
|
resourceWhitelist,
|
||||||
|
roleResources,
|
||||||
|
roles,
|
||||||
|
Target,
|
||||||
|
Transaction,
|
||||||
|
userOrgs,
|
||||||
|
userResources,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
|
import { resources, targets, sites } from "@server/db";
|
||||||
|
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
ConfigSchema,
|
||||||
|
isTargetsOnlyResource,
|
||||||
|
TargetData
|
||||||
|
} from "./types";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
|
import { resourcePassword } from "@server/db";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
|
|
||||||
|
export type ProxyResourcesResults = {
|
||||||
|
proxyResource: Resource;
|
||||||
|
targetsToUpdate: Target[];
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export async function updateProxyResources(
|
||||||
|
orgId: string,
|
||||||
|
config: Config,
|
||||||
|
trx: Transaction,
|
||||||
|
siteId?: number
|
||||||
|
): Promise<ProxyResourcesResults> {
|
||||||
|
const results: ProxyResourcesResults = [];
|
||||||
|
|
||||||
|
for (const [resourceNiceId, resourceData] of Object.entries(
|
||||||
|
config["proxy-resources"]
|
||||||
|
)) {
|
||||||
|
const targetsToUpdate: Target[] = [];
|
||||||
|
let resource: Resource;
|
||||||
|
|
||||||
|
async function createTarget( // reusable function to create a target
|
||||||
|
resourceId: number,
|
||||||
|
targetData: TargetData
|
||||||
|
) {
|
||||||
|
const targetSiteId = targetData.site;
|
||||||
|
let site;
|
||||||
|
|
||||||
|
if (targetSiteId) {
|
||||||
|
// Look up site by niceId
|
||||||
|
[site] = await trx
|
||||||
|
.select({ siteId: sites.siteId })
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.niceId, targetSiteId),
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
throw new Error(
|
||||||
|
`Site not found: ${targetSiteId} in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let internalPortToCreate;
|
||||||
|
if (!targetData["internal-port"]) {
|
||||||
|
const { internalPort, targetIps } = await pickPort(
|
||||||
|
site.siteId!,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
internalPortToCreate = internalPort;
|
||||||
|
} else {
|
||||||
|
internalPortToCreate = targetData["internal-port"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create target
|
||||||
|
const [newTarget] = await trx
|
||||||
|
.insert(targets)
|
||||||
|
.values({
|
||||||
|
resourceId: resourceId,
|
||||||
|
siteId: site.siteId,
|
||||||
|
ip: targetData.hostname,
|
||||||
|
method: targetData.method,
|
||||||
|
port: targetData.port,
|
||||||
|
enabled: targetData.enabled,
|
||||||
|
internalPort: internalPortToCreate,
|
||||||
|
path: targetData.path,
|
||||||
|
pathMatchType: targetData["path-match"]
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
targetsToUpdate.push(newTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing resource by niceId and orgId
|
||||||
|
const [existingResource] = await trx
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.niceId, resourceNiceId),
|
||||||
|
eq(resources.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const http = resourceData.protocol == "http";
|
||||||
|
const protocol =
|
||||||
|
resourceData.protocol == "http" ? "tcp" : resourceData.protocol;
|
||||||
|
const resourceEnabled =
|
||||||
|
resourceData.enabled == undefined || resourceData.enabled == null
|
||||||
|
? true
|
||||||
|
: resourceData.enabled;
|
||||||
|
const resourceSsl =
|
||||||
|
resourceData.ssl == undefined || resourceData.ssl == null
|
||||||
|
? true
|
||||||
|
: resourceData.ssl;
|
||||||
|
let headers = "";
|
||||||
|
for (const header of resourceData.headers || []) {
|
||||||
|
headers += `${header.name}: ${header.value},`;
|
||||||
|
}
|
||||||
|
// if there are headers, remove the trailing comma
|
||||||
|
if (headers.endsWith(",")) {
|
||||||
|
headers = headers.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingResource) {
|
||||||
|
let domain;
|
||||||
|
if (http) {
|
||||||
|
domain = await getDomain(
|
||||||
|
existingResource.resourceId,
|
||||||
|
resourceData["full-domain"]!,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the only key in the resource is targets, if so, skip the update
|
||||||
|
if (isTargetsOnlyResource(resourceData)) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping update for resource ${existingResource.resourceId} as only targets are provided`
|
||||||
|
);
|
||||||
|
resource = existingResource;
|
||||||
|
} else {
|
||||||
|
// Update existing resource
|
||||||
|
[resource] = await trx
|
||||||
|
.update(resources)
|
||||||
|
.set({
|
||||||
|
name: resourceData.name || "Unnamed Resource",
|
||||||
|
protocol: protocol || "http",
|
||||||
|
http: http,
|
||||||
|
proxyPort: http ? null : resourceData["proxy-port"],
|
||||||
|
fullDomain: http ? resourceData["full-domain"] : null,
|
||||||
|
subdomain: domain ? domain.subdomain : null,
|
||||||
|
domainId: domain ? domain.domainId : null,
|
||||||
|
enabled: resourceEnabled,
|
||||||
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
|
ssl: resourceSsl,
|
||||||
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
|
emailWhitelistEnabled: resourceData.auth?.[
|
||||||
|
"whitelist-users"
|
||||||
|
]
|
||||||
|
? resourceData.auth["whitelist-users"].length > 0
|
||||||
|
: false,
|
||||||
|
headers: headers || null,
|
||||||
|
applyRules:
|
||||||
|
resourceData.rules && resourceData.rules.length > 0
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(resources.resourceId, existingResource.resourceId)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(resourcePassword)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePassword.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (resourceData.auth?.password) {
|
||||||
|
const passwordHash = await hashPassword(
|
||||||
|
resourceData.auth.password
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePassword).values({
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(resourcePincode)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePincode.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (resourceData.auth?.pincode) {
|
||||||
|
const pincodeHash = await hashPassword(
|
||||||
|
resourceData.auth.pincode.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePincode).values({
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
pincodeHash,
|
||||||
|
digitLength: 6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["sso-roles"]) {
|
||||||
|
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||||
|
await syncRoleResources(
|
||||||
|
existingResource.resourceId,
|
||||||
|
ssoRoles,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["sso-users"]) {
|
||||||
|
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||||
|
await syncUserResources(
|
||||||
|
existingResource.resourceId,
|
||||||
|
ssoUsers,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["whitelist-users"]) {
|
||||||
|
const whitelistUsers =
|
||||||
|
resourceData.auth?.["whitelist-users"];
|
||||||
|
await syncWhitelistUsers(
|
||||||
|
existingResource.resourceId,
|
||||||
|
whitelistUsers,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingResourceTargets = await trx
|
||||||
|
.select()
|
||||||
|
.from(targets)
|
||||||
|
.where(eq(targets.resourceId, existingResource.resourceId))
|
||||||
|
.orderBy(asc(targets.targetId));
|
||||||
|
|
||||||
|
// Create new targets
|
||||||
|
for (const [index, targetData] of resourceData.targets.entries()) {
|
||||||
|
if (
|
||||||
|
!targetData ||
|
||||||
|
(typeof targetData === "object" &&
|
||||||
|
Object.keys(targetData).length === 0)
|
||||||
|
) {
|
||||||
|
// If targetData is null or an empty object, we can skip it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const existingTarget = existingResourceTargets[index];
|
||||||
|
|
||||||
|
if (existingTarget) {
|
||||||
|
const targetSiteId = targetData.site;
|
||||||
|
let site;
|
||||||
|
|
||||||
|
if (targetSiteId) {
|
||||||
|
// Look up site by niceId
|
||||||
|
[site] = await trx
|
||||||
|
.select({ siteId: sites.siteId })
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.niceId, targetSiteId),
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
throw new Error(
|
||||||
|
`Site not found: ${targetSiteId} in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update this target
|
||||||
|
const [updatedTarget] = await trx
|
||||||
|
.update(targets)
|
||||||
|
.set({
|
||||||
|
siteId: site.siteId,
|
||||||
|
ip: targetData.hostname,
|
||||||
|
method: http ? targetData.method : null,
|
||||||
|
port: targetData.port,
|
||||||
|
enabled: targetData.enabled,
|
||||||
|
path: targetData.path,
|
||||||
|
pathMatchType: targetData["path-match"]
|
||||||
|
})
|
||||||
|
.where(eq(targets.targetId, existingTarget.targetId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
||||||
|
let internalPortToUpdate;
|
||||||
|
if (!targetData["internal-port"]) {
|
||||||
|
const { internalPort, targetIps } = await pickPort(
|
||||||
|
site.siteId!,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
internalPortToUpdate = internalPort;
|
||||||
|
} else {
|
||||||
|
internalPortToUpdate = targetData["internal-port"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [finalUpdatedTarget] = await trx // this double is so we can check the whole target before and after
|
||||||
|
.update(targets)
|
||||||
|
.set({
|
||||||
|
internalPort: internalPortToUpdate
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(targets.targetId, existingTarget.targetId)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
targetsToUpdate.push(finalUpdatedTarget);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await createTarget(existingResource.resourceId, targetData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingResourceTargets.length > resourceData.targets.length) {
|
||||||
|
const targetsToDelete = existingResourceTargets.slice(
|
||||||
|
resourceData.targets.length
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Targets to delete: ${JSON.stringify(targetsToDelete)}`
|
||||||
|
);
|
||||||
|
for (const target of targetsToDelete) {
|
||||||
|
if (!target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (siteId && target.siteId !== siteId) {
|
||||||
|
logger.debug(
|
||||||
|
`Skipping target ${target.targetId} for deletion. Site ID does not match filter.`
|
||||||
|
);
|
||||||
|
continue; // only delete targets for the specified siteId
|
||||||
|
}
|
||||||
|
logger.debug(`Deleting target ${target.targetId}`);
|
||||||
|
await trx
|
||||||
|
.delete(targets)
|
||||||
|
.where(eq(targets.targetId, target.targetId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRules = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourceRules)
|
||||||
|
.where(
|
||||||
|
eq(resourceRules.resourceId, existingResource.resourceId)
|
||||||
|
)
|
||||||
|
.orderBy(resourceRules.priority);
|
||||||
|
|
||||||
|
// Sync rules
|
||||||
|
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||||
|
const existingRule = existingRules[index];
|
||||||
|
if (existingRule) {
|
||||||
|
if (
|
||||||
|
existingRule.action !== getRuleAction(rule.action) ||
|
||||||
|
existingRule.match !== rule.match.toUpperCase() ||
|
||||||
|
existingRule.value !== rule.value
|
||||||
|
) {
|
||||||
|
validateRule(rule);
|
||||||
|
await trx
|
||||||
|
.update(resourceRules)
|
||||||
|
.set({
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: rule.match.toUpperCase(),
|
||||||
|
value: rule.value
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(resourceRules.ruleId, existingRule.ruleId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validateRule(rule);
|
||||||
|
await trx.insert(resourceRules).values({
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: rule.match.toUpperCase(),
|
||||||
|
value: rule.value,
|
||||||
|
priority: index + 1 // start priorities at 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingRules.length > (resourceData.rules?.length || 0)) {
|
||||||
|
const rulesToDelete = existingRules.slice(
|
||||||
|
resourceData.rules?.length || 0
|
||||||
|
);
|
||||||
|
for (const rule of rulesToDelete) {
|
||||||
|
await trx
|
||||||
|
.delete(resourceRules)
|
||||||
|
.where(eq(resourceRules.ruleId, rule.ruleId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Updated resource ${existingResource.resourceId}`);
|
||||||
|
} else {
|
||||||
|
// create a brand new resource
|
||||||
|
let domain;
|
||||||
|
if (http) {
|
||||||
|
domain = await getDomain(
|
||||||
|
undefined,
|
||||||
|
resourceData["full-domain"]!,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new resource
|
||||||
|
const [newResource] = await trx
|
||||||
|
.insert(resources)
|
||||||
|
.values({
|
||||||
|
orgId,
|
||||||
|
niceId: resourceNiceId,
|
||||||
|
name: resourceData.name || "Unnamed Resource",
|
||||||
|
protocol: resourceData.protocol || "http",
|
||||||
|
http: http,
|
||||||
|
proxyPort: http ? null : resourceData["proxy-port"],
|
||||||
|
fullDomain: http ? resourceData["full-domain"] : null,
|
||||||
|
subdomain: domain ? domain.subdomain : null,
|
||||||
|
domainId: domain ? domain.domainId : null,
|
||||||
|
enabled: resourceEnabled,
|
||||||
|
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||||
|
setHostHeader: resourceData["host-header"] || null,
|
||||||
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
|
ssl: resourceSsl,
|
||||||
|
headers: headers || null,
|
||||||
|
applyRules:
|
||||||
|
resourceData.rules && resourceData.rules.length > 0
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (resourceData.auth?.password) {
|
||||||
|
const passwordHash = await hashPassword(
|
||||||
|
resourceData.auth.password
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePassword).values({
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.pincode) {
|
||||||
|
const pincodeHash = await hashPassword(
|
||||||
|
resourceData.auth.pincode.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePincode).values({
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
pincodeHash,
|
||||||
|
digitLength: 6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resource = newResource;
|
||||||
|
|
||||||
|
const [adminRole] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
throw new Error(`Admin role not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(roleResources).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
resourceId: newResource.resourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resourceData.auth?.["sso-roles"]) {
|
||||||
|
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||||
|
await syncRoleResources(
|
||||||
|
newResource.resourceId,
|
||||||
|
ssoRoles,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["sso-users"]) {
|
||||||
|
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||||
|
await syncUserResources(
|
||||||
|
newResource.resourceId,
|
||||||
|
ssoUsers,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceData.auth?.["whitelist-users"]) {
|
||||||
|
const whitelistUsers = resourceData.auth?.["whitelist-users"];
|
||||||
|
await syncWhitelistUsers(
|
||||||
|
newResource.resourceId,
|
||||||
|
whitelistUsers,
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new targets
|
||||||
|
for (const targetData of resourceData.targets) {
|
||||||
|
if (!targetData) {
|
||||||
|
// If targetData is null or an empty object, we can skip it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await createTarget(newResource.resourceId, targetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||||
|
validateRule(rule);
|
||||||
|
await trx.insert(resourceRules).values({
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: rule.match.toUpperCase(),
|
||||||
|
value: rule.value,
|
||||||
|
priority: index + 1 // start priorities at 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
proxyResource: resource,
|
||||||
|
targetsToUpdate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuleAction(input: string) {
|
||||||
|
let action = "DROP";
|
||||||
|
if (input == "allow") {
|
||||||
|
action = "ACCEPT";
|
||||||
|
} else if (input == "deny") {
|
||||||
|
action = "DROP";
|
||||||
|
} else if (input == "pass") {
|
||||||
|
action = "PASS";
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRule(rule: any) {
|
||||||
|
if (rule.match === "cidr") {
|
||||||
|
if (!isValidCIDR(rule.value)) {
|
||||||
|
throw new Error(`Invalid CIDR provided: ${rule.value}`);
|
||||||
|
}
|
||||||
|
} else if (rule.match === "ip") {
|
||||||
|
if (!isValidIP(rule.value)) {
|
||||||
|
throw new Error(`Invalid IP provided: ${rule.value}`);
|
||||||
|
}
|
||||||
|
} else if (rule.match === "path") {
|
||||||
|
if (!isValidUrlGlobPattern(rule.value)) {
|
||||||
|
throw new Error(`Invalid URL glob pattern: ${rule.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncRoleResources(
|
||||||
|
resourceId: number,
|
||||||
|
ssoRoles: string[],
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingRoleResources = await trx
|
||||||
|
.select()
|
||||||
|
.from(roleResources)
|
||||||
|
.where(eq(roleResources.resourceId, resourceId));
|
||||||
|
|
||||||
|
for (const roleName of ssoRoles) {
|
||||||
|
if (roleName === "Admin") {
|
||||||
|
continue; // never add admin access
|
||||||
|
}
|
||||||
|
|
||||||
|
const [role] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.name, roleName), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRoleResource = existingRoleResources.find(
|
||||||
|
(rr) => rr.roleId === role.roleId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingRoleResource) {
|
||||||
|
await trx.insert(roleResources).values({
|
||||||
|
roleId: role.roleId,
|
||||||
|
resourceId: resourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existingRoleResource of existingRoleResources) {
|
||||||
|
const [role] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(eq(roles.roleId, existingRoleResource.roleId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (role.isAdmin) {
|
||||||
|
continue; // never remove admin access
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role && !ssoRoles.includes(role.name)) {
|
||||||
|
await trx
|
||||||
|
.delete(roleResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleResources.roleId, existingRoleResource.roleId),
|
||||||
|
eq(roleResources.resourceId, resourceId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncUserResources(
|
||||||
|
resourceId: number,
|
||||||
|
ssoUsers: string[],
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingUserResources = await trx
|
||||||
|
.select()
|
||||||
|
.from(userResources)
|
||||||
|
.where(eq(userResources.resourceId, resourceId));
|
||||||
|
|
||||||
|
for (const email of ssoUsers) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`User not found: ${email} in org ${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUserResource = existingUserResources.find(
|
||||||
|
(rr) => rr.userId === user.user.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingUserResource) {
|
||||||
|
await trx.insert(userResources).values({
|
||||||
|
userId: user.user.userId,
|
||||||
|
resourceId: resourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existingUserResource of existingUserResources) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(users.userId, existingUserResource.userId),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (user && user.user.email && !ssoUsers.includes(user.user.email)) {
|
||||||
|
await trx
|
||||||
|
.delete(userResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userResources.userId, existingUserResource.userId),
|
||||||
|
eq(userResources.resourceId, resourceId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncWhitelistUsers(
|
||||||
|
resourceId: number,
|
||||||
|
whitelistUsers: string[],
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingWhitelist = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourceWhitelist)
|
||||||
|
.where(eq(resourceWhitelist.resourceId, resourceId));
|
||||||
|
|
||||||
|
for (const email of whitelistUsers) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(and(eq(users.email, email), eq(userOrgs.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`User not found: ${email} in org ${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingWhitelistEntry = existingWhitelist.find(
|
||||||
|
(w) => w.email === email
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingWhitelistEntry) {
|
||||||
|
await trx.insert(resourceWhitelist).values({
|
||||||
|
email,
|
||||||
|
resourceId: resourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existingWhitelistEntry of existingWhitelist) {
|
||||||
|
if (!whitelistUsers.includes(existingWhitelistEntry.email)) {
|
||||||
|
await trx
|
||||||
|
.delete(resourceWhitelist)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceWhitelist.resourceId, resourceId),
|
||||||
|
eq(
|
||||||
|
resourceWhitelist.email,
|
||||||
|
existingWhitelistEntry.email
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfTargetChanged(
|
||||||
|
existing: Target | undefined,
|
||||||
|
incoming: Target | undefined
|
||||||
|
): boolean {
|
||||||
|
if (!existing && incoming) return true;
|
||||||
|
if (existing && !incoming) return true;
|
||||||
|
if (!existing || !incoming) return false;
|
||||||
|
|
||||||
|
if (existing.ip !== incoming.ip) return true;
|
||||||
|
if (existing.port !== incoming.port) return true;
|
||||||
|
if (existing.siteId !== incoming.siteId) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDomain(
|
||||||
|
resourceId: number | undefined,
|
||||||
|
fullDomain: string,
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const [fullDomainExists] = await trx
|
||||||
|
.select({ resourceId: resources.resourceId })
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.fullDomain, fullDomain),
|
||||||
|
eq(resources.orgId, orgId),
|
||||||
|
resourceId
|
||||||
|
? ne(resources.resourceId, resourceId)
|
||||||
|
: isNotNull(resources.resourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (fullDomainExists) {
|
||||||
|
throw new Error(
|
||||||
|
`Resource already exists: ${fullDomain} in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = await getDomainId(orgId, fullDomain, trx);
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(
|
||||||
|
`Domain not found for full-domain: ${fullDomain} in org ${orgId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDomainId(
|
||||||
|
orgId: string,
|
||||||
|
fullDomain: string,
|
||||||
|
trx: Transaction
|
||||||
|
): Promise<{ subdomain: string | null; domainId: string } | null> {
|
||||||
|
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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domainSelection = validDomains[0].domains;
|
||||||
|
const baseDomain = domainSelection.baseDomain;
|
||||||
|
|
||||||
|
// remove the base domain of the domain
|
||||||
|
let subdomain = null;
|
||||||
|
if (domainSelection.type == "ns") {
|
||||||
|
if (fullDomain != baseDomain) {
|
||||||
|
subdomain = fullDomain.replace(`.${baseDomain}`, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first valid domain
|
||||||
|
return {
|
||||||
|
subdomain: subdomain,
|
||||||
|
domainId: domainSelection.domainId
|
||||||
|
};
|
||||||
|
}
|
366
server/lib/blueprints/types.ts
Normal file
366
server/lib/blueprints/types.ts
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SiteSchema = z.object({
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
"docker-socket-enabled": z.boolean().optional().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for individual target within a resource
|
||||||
|
export const TargetSchema = z.object({
|
||||||
|
site: z.string().optional(),
|
||||||
|
method: z.enum(["http", "https", "h2c"]).optional(),
|
||||||
|
hostname: z.string(),
|
||||||
|
port: z.number().int().min(1).max(65535),
|
||||||
|
enabled: z.boolean().optional().default(true),
|
||||||
|
"internal-port": z.number().int().min(1).max(65535).optional(),
|
||||||
|
path: z.string().optional(),
|
||||||
|
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
|
});
|
||||||
|
export type TargetData = z.infer<typeof TargetSchema>;
|
||||||
|
|
||||||
|
export const AuthSchema = z.object({
|
||||||
|
// pincode has to have 6 digits
|
||||||
|
pincode: z.number().min(100000).max(999999).optional(),
|
||||||
|
password: z.string().min(1).optional(),
|
||||||
|
"sso-enabled": z.boolean().optional().default(false),
|
||||||
|
"sso-roles": z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.default([])
|
||||||
|
.refine((roles) => !roles.includes("Admin"), {
|
||||||
|
message: "Admin role cannot be included in sso-roles"
|
||||||
|
}),
|
||||||
|
"sso-users": z.array(z.string().email()).optional().default([]),
|
||||||
|
"whitelist-users": z.array(z.string().email()).optional().default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RuleSchema = z.object({
|
||||||
|
action: z.enum(["allow", "deny", "pass"]),
|
||||||
|
match: z.enum(["cidr", "path", "ip", "country"]),
|
||||||
|
value: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HeaderSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
value: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for individual resource
|
||||||
|
export const ResourceSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||||
|
ssl: z.boolean().optional(),
|
||||||
|
"full-domain": z.string().optional(),
|
||||||
|
"proxy-port": z.number().int().min(1).max(65535).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
targets: z.array(TargetSchema.nullable()).optional().default([]),
|
||||||
|
auth: AuthSchema.optional(),
|
||||||
|
"host-header": z.string().optional(),
|
||||||
|
"tls-server-name": z.string().optional(),
|
||||||
|
headers: z.array(HeaderSchema).optional(),
|
||||||
|
rules: z.array(RuleSchema).optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, require name and protocol for full resource definition
|
||||||
|
return (
|
||||||
|
resource.name !== undefined && resource.protocol !== undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Resource must either be targets-only (only 'targets' field) or have both 'name' and 'protocol' fields at a minimum",
|
||||||
|
path: ["name", "protocol"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If protocol is http, all targets must have method field
|
||||||
|
if (resource.protocol === "http") {
|
||||||
|
return resource.targets.every(
|
||||||
|
(target) => target == null || target.method !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If protocol is tcp or udp, no target should have method field
|
||||||
|
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
||||||
|
return resource.targets.every(
|
||||||
|
(target) => target == null || target.method === undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(resource) => {
|
||||||
|
if (resource.protocol === "http") {
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"When protocol is 'http', all targets must have a 'method' field",
|
||||||
|
path: ["targets"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"When protocol is 'tcp' or 'udp', targets must not have a 'method' field",
|
||||||
|
path: ["targets"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If protocol is http, it must have a full-domain
|
||||||
|
if (resource.protocol === "http") {
|
||||||
|
return (
|
||||||
|
resource["full-domain"] !== undefined &&
|
||||||
|
resource["full-domain"].length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"When protocol is 'http', a 'full-domain' must be provided",
|
||||||
|
path: ["full-domain"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If protocol is tcp or udp, it must have both proxy-port
|
||||||
|
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
||||||
|
return resource["proxy-port"] !== undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"When protocol is 'tcp' or 'udp', 'proxy-port' must be provided",
|
||||||
|
path: ["proxy-port", "exit-node"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
// Skip validation for targets-only resources
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If protocol is tcp or udp, it must not have auth
|
||||||
|
if (resource.protocol === "tcp" || resource.protocol === "udp") {
|
||||||
|
return resource.auth === undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"When protocol is 'tcp' or 'udp', 'auth' must not be provided",
|
||||||
|
path: ["auth"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function isTargetsOnlyResource(resource: any): boolean {
|
||||||
|
return Object.keys(resource).length === 1 && resource.targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientResourceSchema = z.object({
|
||||||
|
name: z.string().min(2).max(100),
|
||||||
|
site: z.string().min(2).max(100).optional(),
|
||||||
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
|
"proxy-port": z.number().min(1).max(65535),
|
||||||
|
"hostname": z.string().min(1).max(255),
|
||||||
|
"internal-port": z.number().min(1).max(65535),
|
||||||
|
enabled: z.boolean().optional().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for the entire configuration object
|
||||||
|
export const ConfigSchema = z
|
||||||
|
.object({
|
||||||
|
"proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}),
|
||||||
|
"client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}),
|
||||||
|
sites: z.record(z.string(), SiteSchema).optional().default({})
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
// Enforce the full-domain uniqueness across resources in the same stack
|
||||||
|
(config) => {
|
||||||
|
// Extract all full-domain values with their resource keys
|
||||||
|
const fullDomainMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["proxy-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const fullDomain = resource["full-domain"];
|
||||||
|
if (fullDomain) {
|
||||||
|
// Only process if full-domain is defined
|
||||||
|
if (!fullDomainMap.has(fullDomain)) {
|
||||||
|
fullDomainMap.set(fullDomain, []);
|
||||||
|
}
|
||||||
|
fullDomainMap.get(fullDomain)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find duplicates
|
||||||
|
const duplicates = Array.from(fullDomainMap.entries()).filter(
|
||||||
|
([_, resourceKeys]) => resourceKeys.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return duplicates.length === 0;
|
||||||
|
},
|
||||||
|
(config) => {
|
||||||
|
// Extract duplicates for error message
|
||||||
|
const fullDomainMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["proxy-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const fullDomain = resource["full-domain"];
|
||||||
|
if (fullDomain) {
|
||||||
|
// Only process if full-domain is defined
|
||||||
|
if (!fullDomainMap.has(fullDomain)) {
|
||||||
|
fullDomainMap.set(fullDomain, []);
|
||||||
|
}
|
||||||
|
fullDomainMap.get(fullDomain)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicates = Array.from(fullDomainMap.entries())
|
||||||
|
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
|
||||||
|
.map(
|
||||||
|
([fullDomain, resourceKeys]) =>
|
||||||
|
`'${fullDomain}' used by resources: ${resourceKeys.join(", ")}`
|
||||||
|
)
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Duplicate 'full-domain' values found: ${duplicates}`,
|
||||||
|
path: ["resources"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
// Enforce proxy-port uniqueness within proxy-resources
|
||||||
|
(config) => {
|
||||||
|
const proxyPortMap = new Map<number, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["proxy-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const proxyPort = resource["proxy-port"];
|
||||||
|
if (proxyPort !== undefined) {
|
||||||
|
if (!proxyPortMap.has(proxyPort)) {
|
||||||
|
proxyPortMap.set(proxyPort, []);
|
||||||
|
}
|
||||||
|
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find duplicates
|
||||||
|
const duplicates = Array.from(proxyPortMap.entries()).filter(
|
||||||
|
([_, resourceKeys]) => resourceKeys.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return duplicates.length === 0;
|
||||||
|
},
|
||||||
|
(config) => {
|
||||||
|
// Extract duplicates for error message
|
||||||
|
const proxyPortMap = new Map<number, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["proxy-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const proxyPort = resource["proxy-port"];
|
||||||
|
if (proxyPort !== undefined) {
|
||||||
|
if (!proxyPortMap.has(proxyPort)) {
|
||||||
|
proxyPortMap.set(proxyPort, []);
|
||||||
|
}
|
||||||
|
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicates = Array.from(proxyPortMap.entries())
|
||||||
|
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
|
||||||
|
.map(
|
||||||
|
([proxyPort, resourceKeys]) =>
|
||||||
|
`port ${proxyPort} used by proxy-resources: ${resourceKeys.join(", ")}`
|
||||||
|
)
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`,
|
||||||
|
path: ["proxy-resources"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
// Enforce proxy-port uniqueness within client-resources
|
||||||
|
(config) => {
|
||||||
|
const proxyPortMap = new Map<number, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["client-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const proxyPort = resource["proxy-port"];
|
||||||
|
if (proxyPort !== undefined) {
|
||||||
|
if (!proxyPortMap.has(proxyPort)) {
|
||||||
|
proxyPortMap.set(proxyPort, []);
|
||||||
|
}
|
||||||
|
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find duplicates
|
||||||
|
const duplicates = Array.from(proxyPortMap.entries()).filter(
|
||||||
|
([_, resourceKeys]) => resourceKeys.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return duplicates.length === 0;
|
||||||
|
},
|
||||||
|
(config) => {
|
||||||
|
// Extract duplicates for error message
|
||||||
|
const proxyPortMap = new Map<number, string[]>();
|
||||||
|
|
||||||
|
Object.entries(config["client-resources"]).forEach(
|
||||||
|
([resourceKey, resource]) => {
|
||||||
|
const proxyPort = resource["proxy-port"];
|
||||||
|
if (proxyPort !== undefined) {
|
||||||
|
if (!proxyPortMap.has(proxyPort)) {
|
||||||
|
proxyPortMap.set(proxyPort, []);
|
||||||
|
}
|
||||||
|
proxyPortMap.get(proxyPort)!.push(resourceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicates = Array.from(proxyPortMap.entries())
|
||||||
|
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
|
||||||
|
.map(
|
||||||
|
([proxyPort, resourceKeys]) =>
|
||||||
|
`port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}`
|
||||||
|
)
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`,
|
||||||
|
path: ["client-resources"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Type inference from the schema
|
||||||
|
export type Site = z.infer<typeof SiteSchema>;
|
||||||
|
export type Target = z.infer<typeof TargetSchema>;
|
||||||
|
export type Resource = z.infer<typeof ResourceSchema>;
|
||||||
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
@ -2,7 +2,7 @@ import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.9.0";
|
export const APP_VERSION = "1.10.1";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|
112
server/lib/domainUtils.ts
Normal file
112
server/lib/domainUtils.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { domains, orgDomains } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { subdomainSchema } from "@server/lib/schemas";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
export type DomainValidationResult = {
|
||||||
|
success: true;
|
||||||
|
fullDomain: string;
|
||||||
|
subdomain: string | null;
|
||||||
|
} | {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a domain and constructs the full domain based on domain type and subdomain.
|
||||||
|
*
|
||||||
|
* @param domainId - The ID of the domain to validate
|
||||||
|
* @param orgId - The organization ID to check domain access
|
||||||
|
* @param subdomain - Optional subdomain to append (for ns and wildcard domains)
|
||||||
|
* @returns DomainValidationResult with success status and either fullDomain/subdomain or error message
|
||||||
|
*/
|
||||||
|
export async function validateAndConstructDomain(
|
||||||
|
domainId: string,
|
||||||
|
orgId: string,
|
||||||
|
subdomain?: string | null
|
||||||
|
): Promise<DomainValidationResult> {
|
||||||
|
try {
|
||||||
|
// Query domain with organization access check
|
||||||
|
const [domainRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.where(eq(domains.domainId, domainId))
|
||||||
|
.leftJoin(
|
||||||
|
orgDomains,
|
||||||
|
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if domain exists
|
||||||
|
if (!domainRes || !domainRes.domains) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Domain with ID ${domainId} not found`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if organization has access to domain
|
||||||
|
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Organization does not have access to domain with ID ${domainId}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain is verified
|
||||||
|
if (!domainRes.domains.verified) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Domain with ID ${domainId} is not verified`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct full domain based on domain type
|
||||||
|
let fullDomain = "";
|
||||||
|
let finalSubdomain = subdomain;
|
||||||
|
|
||||||
|
if (domainRes.domains.type === "ns") {
|
||||||
|
if (subdomain) {
|
||||||
|
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
|
} else {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
}
|
||||||
|
} else if (domainRes.domains.type === "cname") {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
finalSubdomain = null; // CNAME domains don't use subdomains
|
||||||
|
} else if (domainRes.domains.type === "wildcard") {
|
||||||
|
if (subdomain !== undefined && subdomain !== null) {
|
||||||
|
// Validate subdomain format for wildcard domains
|
||||||
|
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
|
||||||
|
if (!parsedSubdomain.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: fromError(parsedSubdomain.error).toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
|
} else {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the full domain equals the base domain, set subdomain to null
|
||||||
|
if (fullDomain === domainRes.domains.baseDomain) {
|
||||||
|
finalSubdomain = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase
|
||||||
|
fullDomain = fullDomain.toLowerCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fullDomain,
|
||||||
|
subdomain: finalSubdomain ?? null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import logger from "@server/logger";
|
||||||
import { ExitNode } from "@server/db";
|
import { ExitNode } from "@server/db";
|
||||||
|
|
||||||
interface ExitNodeRequest {
|
interface ExitNodeRequest {
|
||||||
remoteType: string;
|
remoteType?: string;
|
||||||
localPath: string;
|
localPath: string;
|
||||||
method?: "POST" | "DELETE" | "GET" | "PUT";
|
method?: "POST" | "DELETE" | "GET" | "PUT";
|
||||||
data?: any;
|
data?: any;
|
||||||
|
|
|
@ -30,7 +30,8 @@ export async function listExitNodes(orgId: string, filterOnline = false) {
|
||||||
maxConnections: exitNodes.maxConnections,
|
maxConnections: exitNodes.maxConnections,
|
||||||
online: exitNodes.online,
|
online: exitNodes.online,
|
||||||
lastPing: exitNodes.lastPing,
|
lastPing: exitNodes.lastPing,
|
||||||
type: exitNodes.type
|
type: exitNodes.type,
|
||||||
|
region: exitNodes.region
|
||||||
})
|
})
|
||||||
.from(exitNodes);
|
.from(exitNodes);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
getValidCertificatesForDomains,
|
getValidCertificatesForDomains,
|
||||||
getValidCertificatesForDomainsHybrid
|
getValidCertificatesForDomainsHybrid
|
||||||
} from "./remoteCertificates";
|
} from "./remoteCertificates";
|
||||||
|
import { sendToExitNode } from "./exitNodeComms";
|
||||||
|
|
||||||
export class TraefikConfigManager {
|
export class TraefikConfigManager {
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
@ -403,27 +404,11 @@ export class TraefikConfigManager {
|
||||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||||
}
|
}
|
||||||
if (exitNode) {
|
if (exitNode) {
|
||||||
try {
|
await sendToExitNode(exitNode, {
|
||||||
await axios.post(
|
localPath: "/update-local-snis",
|
||||||
`${exitNode.reachableAt}/update-local-snis`,
|
method: "POST",
|
||||||
{ fullDomains: Array.from(domains) },
|
data: { fullDomains: Array.from(domains) }
|
||||||
{ headers: { "Content-Type": "application/json" } }
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating local SNI:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error updating local SNI:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error(
|
||||||
"No exit node found. Has gerbil registered yet?"
|
"No exit node found. Has gerbil registered yet?"
|
||||||
|
|
|
@ -129,6 +129,40 @@ export function isValidDomain(domain: string): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateHeaders(headers: string): boolean {
|
||||||
|
// Validate comma-separated headers in format "Header-Name: value"
|
||||||
|
const headerPairs = headers.split(",").map((pair) => pair.trim());
|
||||||
|
return headerPairs.every((pair) => {
|
||||||
|
// Check if the pair contains exactly one colon
|
||||||
|
const colonCount = (pair.match(/:/g) || []).length;
|
||||||
|
if (colonCount !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colonIndex = pair.indexOf(":");
|
||||||
|
if (colonIndex === 0 || colonIndex === pair.length - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerName = pair.substring(0, colonIndex).trim();
|
||||||
|
const headerValue = pair.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
// Header name should not be empty and should contain valid characters
|
||||||
|
// Header names are case-insensitive and can contain alphanumeric, hyphens
|
||||||
|
const headerNameRegex = /^[a-zA-Z0-9\-_]+$/;
|
||||||
|
if (!headerName || !headerNameRegex.test(headerName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header value should not be empty and should not contain colons
|
||||||
|
if (!headerValue || headerValue.includes(":")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const validTlds = [
|
const validTlds = [
|
||||||
"AAA",
|
"AAA",
|
||||||
"AARP",
|
"AARP",
|
||||||
|
|
|
@ -19,6 +19,11 @@ export async function verifyApiKeySetResourceUsers(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
if (!req.apiKeyOrg) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
@ -32,11 +37,6 @@ export async function verifyApiKeySetResourceUsers(
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey.isRoot) {
|
|
||||||
// Root keys can access any key in any org
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userIds.length === 0) {
|
if (userIds.length === 0) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,6 +343,12 @@ authenticated.get(
|
||||||
verifyUserHasAction(ActionsEnum.getResource),
|
verifyUserHasAction(ActionsEnum.getResource),
|
||||||
resource.getResource
|
resource.getResource
|
||||||
);
|
);
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/resource/:niceId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getResource),
|
||||||
|
resource.getResource
|
||||||
|
);
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
|
@ -582,6 +588,14 @@ authenticated.put(
|
||||||
user.createOrgUser
|
user.createOrgUser
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/user/:userId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateOrgUser),
|
||||||
|
user.updateOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
|
@ -932,7 +946,7 @@ authRouter.post(
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
`requestEmailVerificationCode:${req.body.email || ipKeyGenerator(req.ip || "")}`,
|
`requestEmailVerificationCode:${req.user?.email || ipKeyGenerator(req.ip || "")}`,
|
||||||
handler: (req, res, next) => {
|
handler: (req, res, next) => {
|
||||||
const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`;
|
const message = `You can only request an email verification code ${15} times every ${15} minutes. Please try again later.`;
|
||||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idpOidcConfig } from "@server/db";
|
||||||
import { domains, idp, orgDomains, users, idpOrg } from "@server/db";
|
import { domains, idp, orgDomains, users, idpOrg } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
@ -33,23 +33,21 @@ async function query(limit: number, offset: number) {
|
||||||
idpId: idp.idpId,
|
idpId: idp.idpId,
|
||||||
name: idp.name,
|
name: idp.name,
|
||||||
type: idp.type,
|
type: idp.type,
|
||||||
orgCount: sql<number>`count(${idpOrg.orgId})`
|
variant: idpOidcConfig.variant,
|
||||||
|
orgCount: sql<number>`count(${idpOrg.orgId})`,
|
||||||
|
autoProvision: idp.autoProvision
|
||||||
})
|
})
|
||||||
.from(idp)
|
.from(idp)
|
||||||
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)
|
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)
|
||||||
.groupBy(idp.idpId)
|
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
||||||
|
.groupBy(idp.idpId, idpOidcConfig.variant)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListIdpsResponse = {
|
export type ListIdpsResponse = {
|
||||||
idps: Array<{
|
idps: Awaited<ReturnType<typeof query>>;
|
||||||
idpId: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
orgCount: number;
|
|
||||||
}>;
|
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|
|
@ -24,7 +24,8 @@ import {
|
||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyClientsEnabled,
|
verifyClientsEnabled,
|
||||||
verifyApiKeySiteResourceAccess
|
verifyApiKeySiteResourceAccess,
|
||||||
|
verifyOrgAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
@ -469,6 +470,21 @@ authenticated.get(
|
||||||
user.listUsers
|
user.listUsers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/user",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
|
||||||
|
user.createOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/user/:userId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyUserAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
|
||||||
|
user.updateOrgUser
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/user/:userId",
|
"/org/:orgId/user/:userId",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
@ -628,3 +644,10 @@ authenticated.post(
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||||
client.updateClient
|
client.updateClient
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/blueprint",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
|
||||||
|
org.applyBlueprint
|
||||||
|
);
|
73
server/routers/newt/handleApplyBlueprintMessage.ts
Normal file
73
server/routers/newt/handleApplyBlueprintMessage.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { db, newts } from "@server/db";
|
||||||
|
import { MessageHandler } from "../ws";
|
||||||
|
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
|
||||||
|
import { eq, and, sql, inArray } from "drizzle-orm";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { applyBlueprint } from "@server/lib/blueprints/applyBlueprint";
|
||||||
|
|
||||||
|
export const handleApplyBlueprintMessage: MessageHandler = async (context) => {
|
||||||
|
const { message, client, sendToClient } = context;
|
||||||
|
const newt = client as Newt;
|
||||||
|
|
||||||
|
logger.debug("Handling apply blueprint message!");
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
logger.warn("Newt not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newt.siteId) {
|
||||||
|
logger.warn("Newt has no site!"); // TODO: Maybe we create the site here?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, newt.siteId));
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn("Site not found for newt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { blueprint } = message.data;
|
||||||
|
if (!blueprint) {
|
||||||
|
logger.warn("No blueprint provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Received blueprint: ${blueprint}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blueprintParsed = JSON.parse(blueprint);
|
||||||
|
// Update the blueprint in the database
|
||||||
|
await applyBlueprint(site.orgId, blueprintParsed, site.siteId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update database from config: ${error}`);
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "newt/blueprint/results",
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to update database from config: ${error}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false, // Send to all clients
|
||||||
|
excludeSender: false // Include sender in broadcast
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "newt/blueprint/results",
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
message: "Config updated successfully"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false, // Send to all clients
|
||||||
|
excludeSender: false // Include sender in broadcast
|
||||||
|
};
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getNextAvailableClientSubnet
|
getNextAvailableClientSubnet
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes";
|
import { selectBestExitNode, verifyExitNodeOrgAccess } from "@server/lib/exitNodes";
|
||||||
|
import { fetchContainers } from "./dockerSocket";
|
||||||
|
|
||||||
export type ExitNodePingResult = {
|
export type ExitNodePingResult = {
|
||||||
exitNodeId: number;
|
exitNodeId: number;
|
||||||
|
@ -76,6 +77,15 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`Docker socket enabled: ${oldSite.dockerSocketEnabled}`);
|
||||||
|
|
||||||
|
if (oldSite.dockerSocketEnabled) {
|
||||||
|
logger.debug(
|
||||||
|
"Site has docker socket enabled - requesting docker containers"
|
||||||
|
);
|
||||||
|
fetchContainers(newt.newtId);
|
||||||
|
}
|
||||||
|
|
||||||
let siteSubnet = oldSite.subnet;
|
let siteSubnet = oldSite.subnet;
|
||||||
let exitNodeIdToQuery = oldSite.exitNodeId;
|
let exitNodeIdToQuery = oldSite.exitNodeId;
|
||||||
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
|
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { MessageHandler } from "../ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { dockerSocketCache } from "./dockerSocket";
|
import { dockerSocketCache } from "./dockerSocket";
|
||||||
import { Newt } from "@server/db";
|
import { Newt } from "@server/db";
|
||||||
|
import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint";
|
||||||
|
|
||||||
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||||
const { message, client, sendToClient } = context;
|
const { message, client, sendToClient } = context;
|
||||||
|
@ -57,4 +58,15 @@ export const handleDockerContainersMessage: MessageHandler = async (
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newt.siteId) {
|
||||||
|
logger.warn("Newt has no site!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyNewtDockerBlueprint(
|
||||||
|
newt.siteId,
|
||||||
|
newt.newtId,
|
||||||
|
containers
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,4 +4,5 @@ export * from "./handleNewtRegisterMessage";
|
||||||
export * from "./handleReceiveBandwidthMessage";
|
export * from "./handleReceiveBandwidthMessage";
|
||||||
export * from "./handleGetConfigMessage";
|
export * from "./handleGetConfigMessage";
|
||||||
export * from "./handleSocketMessages";
|
export * from "./handleSocketMessages";
|
||||||
export * from "./handleNewtPingRequestMessage";
|
export * from "./handleNewtPingRequestMessage";
|
||||||
|
export * from "./handleApplyBlueprintMessage";
|
127
server/routers/org/applyBlueprint.ts
Normal file
127
server/routers/org/applyBlueprint.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
apiKeyOrg,
|
||||||
|
apiKeys,
|
||||||
|
domains,
|
||||||
|
Org,
|
||||||
|
orgDomains,
|
||||||
|
orgs,
|
||||||
|
roleActions,
|
||||||
|
roles,
|
||||||
|
userOrgs,
|
||||||
|
users,
|
||||||
|
actions
|
||||||
|
} from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { defaultRoleAllowedActions } from "../role";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { isValidCIDR } from "@server/lib/validators";
|
||||||
|
import { applyBlueprint as applyBlueprintFunc } from "@server/lib/blueprints/applyBlueprint";
|
||||||
|
|
||||||
|
const applyBlueprintSchema = z
|
||||||
|
.object({
|
||||||
|
blueprint: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const applyBlueprintParamsSchema = z
|
||||||
|
.object({
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/org/{orgId}/blueprint",
|
||||||
|
description: "Apply a base64 encoded blueprint to an organization",
|
||||||
|
tags: [OpenAPITags.Org],
|
||||||
|
request: {
|
||||||
|
params: applyBlueprintParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: applyBlueprintSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function applyBlueprint(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = applyBlueprintParamsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
const parsedBody = applyBlueprintSchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { blueprint } = parsedBody.data;
|
||||||
|
|
||||||
|
if (!blueprint) {
|
||||||
|
logger.warn("No blueprint provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Received blueprint: ${blueprint}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// first base64 decode the blueprint
|
||||||
|
const decoded = Buffer.from(blueprint, "base64").toString("utf-8");
|
||||||
|
// then parse the json
|
||||||
|
const blueprintParsed = JSON.parse(decoded);
|
||||||
|
|
||||||
|
// Update the blueprint in the database
|
||||||
|
await applyBlueprintFunc(orgId, blueprintParsed);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update database from config: ${error}`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Failed to update database from config: ${error}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Blueprint applied successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export * from "./checkId";
|
||||||
export * from "./getOrgOverview";
|
export * from "./getOrgOverview";
|
||||||
export * from "./listOrgs";
|
export * from "./listOrgs";
|
||||||
export * from "./pickOrgDefaults";
|
export * from "./pickOrgDefaults";
|
||||||
|
export * from "./applyBlueprint";
|
|
@ -21,6 +21,8 @@ import { subdomainSchema } from "@server/lib/schemas";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { getUniqueResourceName } from "@server/db/names";
|
||||||
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
|
||||||
const createResourceParamsSchema = z
|
const createResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -193,76 +195,21 @@ async function createHttpResource(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, domainId } = parsedBody.data;
|
const { name, domainId } = parsedBody.data;
|
||||||
let subdomain = parsedBody.data.subdomain;
|
const subdomain = parsedBody.data.subdomain;
|
||||||
|
|
||||||
const [domainRes] = await db
|
// Validate domain and construct full domain
|
||||||
.select()
|
const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain);
|
||||||
.from(domains)
|
|
||||||
.where(eq(domains.domainId, domainId))
|
|
||||||
.leftJoin(
|
|
||||||
orgDomains,
|
|
||||||
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!domainRes || !domainRes.domains) {
|
if (!domainResult.success) {
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Domain with ID ${domainId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`Organization does not have access to domain with ID ${domainId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainRes.domains.verified) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
`Domain with ID ${domainRes.domains.domainId} is not verified`
|
domainResult.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullDomain = "";
|
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
||||||
if (domainRes.domains.type == "ns") {
|
|
||||||
if (subdomain) {
|
|
||||||
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
} else if (domainRes.domains.type == "cname") {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
} else if (domainRes.domains.type == "wildcard") {
|
|
||||||
if (subdomain) {
|
|
||||||
// the subdomain cant have a dot in it
|
|
||||||
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
|
|
||||||
if (!parsedSubdomain.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedSubdomain.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullDomain === domainRes.domains.baseDomain) {
|
|
||||||
subdomain = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fullDomain = fullDomain.toLowerCase();
|
|
||||||
|
|
||||||
logger.debug(`Full domain: ${fullDomain}`);
|
logger.debug(`Full domain: ${fullDomain}`);
|
||||||
|
|
||||||
|
@ -283,15 +230,18 @@ async function createHttpResource(
|
||||||
|
|
||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const newResource = await trx
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
|
niceId,
|
||||||
fullDomain,
|
fullDomain,
|
||||||
domainId,
|
domainId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain: finalSubdomain,
|
||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
ssl: true
|
ssl: true
|
||||||
|
@ -391,10 +341,13 @@ async function createRawResource(
|
||||||
|
|
||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const newResource = await trx
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
|
niceId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
http,
|
http,
|
||||||
|
|
|
@ -2,32 +2,72 @@ import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { Resource, resources, sites } from "@server/db";
|
import { Resource, resources, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const getResourceSchema = z
|
const getResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
resourceId: z
|
resourceId: z
|
||||||
.string()
|
.string()
|
||||||
.transform(Number)
|
.optional()
|
||||||
.pipe(z.number().int().positive())
|
.transform(stoi)
|
||||||
|
.pipe(z.number().int().positive().optional())
|
||||||
|
.optional(),
|
||||||
|
niceId: z.string().optional(),
|
||||||
|
orgId: z.string().optional()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetResourceResponse = Resource;
|
async function query(resourceId?: number, niceId?: string, orgId?: string) {
|
||||||
|
if (resourceId) {
|
||||||
|
const [res] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
return res;
|
||||||
|
} else if (niceId && orgId) {
|
||||||
|
const [res] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(and(eq(resources.niceId, niceId), eq(resources.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetResourceResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/resource/{niceId}",
|
||||||
|
description:
|
||||||
|
"Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
orgId: z.string(),
|
||||||
|
niceId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/resource/{resourceId}",
|
path: "/resource/{resourceId}",
|
||||||
description: "Get a resource.",
|
description: "Get a resource by resourceId.",
|
||||||
tags: [OpenAPITags.Resource],
|
tags: [OpenAPITags.Resource],
|
||||||
request: {
|
request: {
|
||||||
params: getResourceSchema
|
params: z.object({
|
||||||
|
resourceId: z.number()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
responses: {}
|
responses: {}
|
||||||
});
|
});
|
||||||
|
@ -48,29 +88,18 @@ export async function getResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resourceId } = parsedParams.data;
|
const { resourceId, niceId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
const [resp] = await db
|
const resource = await query(resourceId, niceId, orgId);
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const resource = resp;
|
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response<GetResourceResponse>(res, {
|
||||||
data: {
|
data: resource,
|
||||||
...resource
|
|
||||||
},
|
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Resource retrieved successfully",
|
message: "Resource retrieved successfully",
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type GetResourceAuthInfoResponse = {
|
||||||
url: string;
|
url: string;
|
||||||
whitelist: boolean;
|
whitelist: boolean;
|
||||||
skipToIdpId: number | null;
|
skipToIdpId: number | null;
|
||||||
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getResourceAuthInfo(
|
export async function getResourceAuthInfo(
|
||||||
|
@ -88,7 +89,8 @@ export async function getResourceAuthInfo(
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
url,
|
url,
|
||||||
whitelist: resource.emailWhitelistEnabled,
|
whitelist: resource.emailWhitelistEnabled,
|
||||||
skipToIdpId: resource.skipToIdpId
|
skipToIdpId: resource.skipToIdpId,
|
||||||
|
orgId: resource.orgId
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import logger from "@server/logger";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { warn } from "console";
|
||||||
|
|
||||||
const listResourcesParamsSchema = z
|
const listResourcesParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -54,7 +55,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
enabled: resources.enabled,
|
enabled: resources.enabled,
|
||||||
domainId: resources.domainId
|
domainId: resources.domainId,
|
||||||
|
niceId: resources.niceId
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { tlsNameSchema } from "@server/lib/schemas";
|
||||||
import { subdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema } from "@server/lib/schemas";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { OpenAPITags } from "@server/openApi";
|
import { OpenAPITags } from "@server/openApi";
|
||||||
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
import { validateHeaders } from "@server/lib/validators";
|
||||||
|
|
||||||
const updateResourceParamsSchema = z
|
const updateResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -44,7 +46,8 @@ const updateHttpResourceBodySchema = z
|
||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
tlsServerName: z.string().nullable().optional(),
|
tlsServerName: z.string().nullable().optional(),
|
||||||
setHostHeader: z.string().nullable().optional(),
|
setHostHeader: z.string().nullable().optional(),
|
||||||
skipToIdpId: z.number().int().positive().nullable().optional()
|
skipToIdpId: z.number().int().positive().nullable().optional(),
|
||||||
|
headers: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
@ -82,6 +85,18 @@ const updateHttpResourceBodySchema = z
|
||||||
message:
|
message:
|
||||||
"Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header."
|
"Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header."
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.headers) {
|
||||||
|
return validateHeaders(data.headers);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Invalid headers format. Use comma-separated format: 'Header-Name: value, Another-Header: another-value'. Header values cannot contain colons."
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type UpdateResourceResponse = Resource;
|
export type UpdateResourceResponse = Resource;
|
||||||
|
@ -230,78 +245,19 @@ async function updateHttpResource(
|
||||||
if (updateData.domainId) {
|
if (updateData.domainId) {
|
||||||
const domainId = updateData.domainId;
|
const domainId = updateData.domainId;
|
||||||
|
|
||||||
const [domainRes] = await db
|
// Validate domain and construct full domain
|
||||||
.select()
|
const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain);
|
||||||
.from(domains)
|
|
||||||
.where(eq(domains.domainId, domainId))
|
if (!domainResult.success) {
|
||||||
.leftJoin(
|
|
||||||
orgDomains,
|
|
||||||
and(
|
|
||||||
eq(orgDomains.orgId, resource.orgId),
|
|
||||||
eq(orgDomains.domainId, domainId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!domainRes || !domainRes.domains) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Domain with ID ${updateData.domainId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
domainRes.orgDomains &&
|
|
||||||
domainRes.orgDomains.orgId !== resource.orgId
|
|
||||||
) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`You do not have permission to use domain with ID ${updateData.domainId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainRes.domains.verified) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
`Domain with ID ${updateData.domainId} is not verified`
|
domainResult.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullDomain = "";
|
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
||||||
if (domainRes.domains.type == "ns") {
|
|
||||||
if (updateData.subdomain) {
|
|
||||||
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
} else if (domainRes.domains.type == "cname") {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
} else if (domainRes.domains.type == "wildcard") {
|
|
||||||
if (updateData.subdomain !== undefined) {
|
|
||||||
// the subdomain cant have a dot in it
|
|
||||||
const parsedSubdomain = subdomainSchema.safeParse(
|
|
||||||
updateData.subdomain
|
|
||||||
);
|
|
||||||
if (!parsedSubdomain.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedSubdomain.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fullDomain = fullDomain.toLowerCase();
|
|
||||||
|
|
||||||
logger.debug(`Full domain: ${fullDomain}`);
|
logger.debug(`Full domain: ${fullDomain}`);
|
||||||
|
|
||||||
|
@ -332,9 +288,8 @@ async function updateHttpResource(
|
||||||
.where(eq(resources.resourceId, resource.resourceId));
|
.where(eq(resources.resourceId, resource.resourceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullDomain === domainRes.domains.baseDomain) {
|
// Update the subdomain in the update data
|
||||||
updateData.subdomain = null;
|
updateData.subdomain = finalSubdomain;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedResource = await db
|
const updatedResource = await db
|
||||||
|
|
|
@ -139,7 +139,7 @@ export async function pickSiteDefaults(
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Organization retrieved successfully",
|
message: "Site defaults chosen successfully",
|
||||||
status: HttpCode.OK
|
status: HttpCode.OK
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { addTargets } from "../client/targets";
|
import { addTargets } from "../client/targets";
|
||||||
|
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||||
|
|
||||||
const createSiteResourceParamsSchema = z
|
const createSiteResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -121,11 +122,14 @@ export async function createSiteResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const niceId = await getUniqueSiteResourceName(orgId);
|
||||||
|
|
||||||
// Create the site resource
|
// Create the site resource
|
||||||
const [newSiteResource] = await db
|
const [newSiteResource] = await db
|
||||||
.insert(siteResources)
|
.insert(siteResources)
|
||||||
.values({
|
.values({
|
||||||
siteId,
|
siteId,
|
||||||
|
niceId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
protocol,
|
protocol,
|
||||||
|
|
|
@ -12,21 +12,72 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const getSiteResourceParamsSchema = z
|
const getSiteResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
siteResourceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val ? Number(val) : undefined)
|
||||||
|
.pipe(z.number().int().positive().optional())
|
||||||
|
.optional(),
|
||||||
siteId: z.string().transform(Number).pipe(z.number().int().positive()),
|
siteId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||||
|
niceId: z.string().optional(),
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetSiteResourceResponse = SiteResource;
|
async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) {
|
||||||
|
if (siteResourceId && siteId && orgId) {
|
||||||
|
const [siteResource] = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(
|
||||||
|
eq(siteResources.siteResourceId, siteResourceId),
|
||||||
|
eq(siteResources.siteId, siteId),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
return siteResource;
|
||||||
|
} else if (niceId && siteId && orgId) {
|
||||||
|
const [siteResource] = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(
|
||||||
|
eq(siteResources.niceId, niceId),
|
||||||
|
eq(siteResources.siteId, siteId),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
return siteResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetSiteResourceResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
|
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
|
||||||
description: "Get a specific site resource.",
|
description: "Get a specific site resource by siteResourceId.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
params: getSiteResourceParamsSchema
|
params: z.object({
|
||||||
|
siteResourceId: z.number(),
|
||||||
|
siteId: z.number(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/site/{siteId}/resource/nice/{niceId}",
|
||||||
|
description: "Get a specific site resource by niceId.",
|
||||||
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
niceId: z.string(),
|
||||||
|
siteId: z.number(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
responses: {}
|
responses: {}
|
||||||
});
|
});
|
||||||
|
@ -47,18 +98,10 @@ export async function getSiteResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteResourceId, siteId, orgId } = parsedParams.data;
|
const { siteResourceId, siteId, niceId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
// Get the site resource
|
// Get the site resource
|
||||||
const [siteResource] = await db
|
const siteResource = await query(siteResourceId, siteId, niceId, orgId);
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.where(and(
|
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!siteResource) {
|
if (!siteResource) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -28,7 +28,7 @@ const updateSiteResourceSchema = z
|
||||||
protocol: z.enum(["tcp", "udp"]).optional(),
|
protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
proxyPort: z.number().int().positive().optional(),
|
proxyPort: z.number().int().positive().optional(),
|
||||||
destinationPort: z.number().int().positive().optional(),
|
destinationPort: z.number().int().positive().optional(),
|
||||||
destinationIp: z.string().ip().optional(),
|
destinationIp: z.string().optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
|
@ -30,7 +30,9 @@ const createTargetSchema = z
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().optional().nullable(),
|
method: z.string().optional().nullable(),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.number().int().min(1).max(65535),
|
||||||
enabled: z.boolean().default(true)
|
enabled: z.boolean().default(true),
|
||||||
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
@ -161,7 +163,7 @@ export async function createTarget(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(site.siteId!);
|
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
||||||
|
|
||||||
if (!internalPort) {
|
if (!internalPort) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { db } from "@server/db";
|
import { db, Transaction } from "@server/db";
|
||||||
import { resources, targets } from "@server/db";
|
import { resources, targets } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
const currentBannedPorts: number[] = [];
|
const currentBannedPorts: number[] = [];
|
||||||
|
|
||||||
export async function pickPort(siteId: number): Promise<{
|
export async function pickPort(siteId: number, trx: Transaction | typeof db): Promise<{
|
||||||
internalPort: number;
|
internalPort: number;
|
||||||
targetIps: string[];
|
targetIps: string[];
|
||||||
}> {
|
}> {
|
||||||
|
@ -12,7 +12,7 @@ export async function pickPort(siteId: number): Promise<{
|
||||||
const targetIps: string[] = [];
|
const targetIps: string[] = [];
|
||||||
const targetInternalPorts: number[] = [];
|
const targetInternalPorts: number[] = [];
|
||||||
|
|
||||||
const targetsRes = await db
|
const targetsRes = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.where(eq(targets.siteId, siteId));
|
.where(eq(targets.siteId, siteId));
|
||||||
|
|
|
@ -44,7 +44,9 @@ function queryTargets(resourceId: number) {
|
||||||
enabled: targets.enabled,
|
enabled: targets.enabled,
|
||||||
resourceId: targets.resourceId,
|
resourceId: targets.resourceId,
|
||||||
siteId: targets.siteId,
|
siteId: targets.siteId,
|
||||||
siteType: sites.type
|
siteType: sites.type,
|
||||||
|
path: targets.path,
|
||||||
|
pathMatchType: targets.pathMatchType
|
||||||
})
|
})
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.leftJoin(sites, eq(sites.siteId, targets.siteId))
|
.leftJoin(sites, eq(sites.siteId, targets.siteId))
|
||||||
|
|
|
@ -26,7 +26,9 @@ const updateTargetBodySchema = z
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
method: z.string().min(1).max(10).optional().nullable(),
|
method: z.string().min(1).max(10).optional().nullable(),
|
||||||
port: z.number().int().min(1).max(65535).optional(),
|
port: z.number().int().min(1).max(65535).optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional(),
|
||||||
|
path: z.string().optional().nullable(),
|
||||||
|
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
@ -153,7 +155,7 @@ export async function updateTarget(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(site.siteId!);
|
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
||||||
|
|
||||||
if (!internalPort) {
|
if (!internalPort) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -54,7 +54,8 @@ export async function traefikConfigProvider(
|
||||||
config.getRawConfig().traefik.site_types
|
config.getRawConfig().traefik.site_types
|
||||||
);
|
);
|
||||||
|
|
||||||
if (traefikConfig?.http?.middlewares) { // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING
|
if (traefikConfig?.http?.middlewares) {
|
||||||
|
// BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING
|
||||||
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
||||||
plugin: {
|
plugin: {
|
||||||
[badgerMiddlewareName]: {
|
[badgerMiddlewareName]: {
|
||||||
|
@ -104,106 +105,112 @@ export async function getTraefikConfig(
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all resources with related data
|
// Get resources with their targets and sites in a single optimized query
|
||||||
const allResources = await db.transaction(async (tx) => {
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
// Get resources with their targets and sites in a single optimized query
|
const resourcesWithTargetsAndSites = await db
|
||||||
// Start from sites on this exit node, then join to targets and resources
|
.select({
|
||||||
const resourcesWithTargetsAndSites = await tx
|
// Resource fields
|
||||||
.select({
|
resourceId: resources.resourceId,
|
||||||
// Resource fields
|
fullDomain: resources.fullDomain,
|
||||||
resourceId: resources.resourceId,
|
ssl: resources.ssl,
|
||||||
fullDomain: resources.fullDomain,
|
http: resources.http,
|
||||||
ssl: resources.ssl,
|
proxyPort: resources.proxyPort,
|
||||||
http: resources.http,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
subdomain: resources.subdomain,
|
||||||
protocol: resources.protocol,
|
domainId: resources.domainId,
|
||||||
subdomain: resources.subdomain,
|
enabled: resources.enabled,
|
||||||
domainId: resources.domainId,
|
stickySession: resources.stickySession,
|
||||||
enabled: resources.enabled,
|
tlsServerName: resources.tlsServerName,
|
||||||
stickySession: resources.stickySession,
|
setHostHeader: resources.setHostHeader,
|
||||||
tlsServerName: resources.tlsServerName,
|
enableProxy: resources.enableProxy,
|
||||||
setHostHeader: resources.setHostHeader,
|
headers: resources.headers,
|
||||||
enableProxy: resources.enableProxy,
|
// Target fields
|
||||||
// Target fields
|
targetId: targets.targetId,
|
||||||
targetId: targets.targetId,
|
targetEnabled: targets.enabled,
|
||||||
targetEnabled: targets.enabled,
|
ip: targets.ip,
|
||||||
ip: targets.ip,
|
method: targets.method,
|
||||||
method: targets.method,
|
port: targets.port,
|
||||||
port: targets.port,
|
internalPort: targets.internalPort,
|
||||||
internalPort: targets.internalPort,
|
path: targets.path,
|
||||||
// Site fields
|
pathMatchType: targets.pathMatchType,
|
||||||
siteId: sites.siteId,
|
|
||||||
siteType: sites.type,
|
|
||||||
siteOnline: sites.online,
|
|
||||||
subnet: sites.subnet,
|
|
||||||
exitNodeId: sites.exitNodeId
|
|
||||||
})
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(targets, eq(targets.siteId, sites.siteId))
|
|
||||||
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(targets.enabled, true),
|
|
||||||
eq(resources.enabled, true),
|
|
||||||
or(
|
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
|
||||||
isNull(sites.exitNodeId)
|
|
||||||
),
|
|
||||||
inArray(sites.type, siteTypes),
|
|
||||||
config.getRawConfig().traefik.allow_raw_resources
|
|
||||||
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
|
||||||
: eq(resources.http, true),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Group by resource and include targets with their unique site data
|
// Site fields
|
||||||
const resourcesMap = new Map();
|
siteId: sites.siteId,
|
||||||
|
siteType: sites.type,
|
||||||
|
siteOnline: sites.online,
|
||||||
|
subnet: sites.subnet,
|
||||||
|
exitNodeId: sites.exitNodeId
|
||||||
|
})
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(targets, eq(targets.siteId, sites.siteId))
|
||||||
|
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targets.enabled, true),
|
||||||
|
eq(resources.enabled, true),
|
||||||
|
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)),
|
||||||
|
inArray(sites.type, siteTypes),
|
||||||
|
config.getRawConfig().traefik.allow_raw_resources
|
||||||
|
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
||||||
|
: eq(resources.http, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
resourcesWithTargetsAndSites.forEach((row) => {
|
// Group by resource and include targets with their unique site data
|
||||||
const resourceId = row.resourceId;
|
const resourcesMap = new Map();
|
||||||
|
|
||||||
if (!resourcesMap.has(resourceId)) {
|
resourcesWithTargetsAndSites.forEach((row) => {
|
||||||
resourcesMap.set(resourceId, {
|
const resourceId = row.resourceId;
|
||||||
resourceId: row.resourceId,
|
const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths
|
||||||
fullDomain: row.fullDomain,
|
const pathMatchType = row.pathMatchType || "";
|
||||||
ssl: row.ssl,
|
|
||||||
http: row.http,
|
|
||||||
proxyPort: row.proxyPort,
|
|
||||||
protocol: row.protocol,
|
|
||||||
subdomain: row.subdomain,
|
|
||||||
domainId: row.domainId,
|
|
||||||
enabled: row.enabled,
|
|
||||||
stickySession: row.stickySession,
|
|
||||||
tlsServerName: row.tlsServerName,
|
|
||||||
setHostHeader: row.setHostHeader,
|
|
||||||
enableProxy: row.enableProxy,
|
|
||||||
targets: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add target with its associated site data
|
// Create a unique key combining resourceId and path+pathMatchType
|
||||||
resourcesMap.get(resourceId).targets.push({
|
const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-");
|
||||||
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||||
|
|
||||||
|
if (!resourcesMap.has(mapKey)) {
|
||||||
|
resourcesMap.set(mapKey, {
|
||||||
resourceId: row.resourceId,
|
resourceId: row.resourceId,
|
||||||
targetId: row.targetId,
|
fullDomain: row.fullDomain,
|
||||||
ip: row.ip,
|
ssl: row.ssl,
|
||||||
method: row.method,
|
http: row.http,
|
||||||
port: row.port,
|
proxyPort: row.proxyPort,
|
||||||
internalPort: row.internalPort,
|
protocol: row.protocol,
|
||||||
enabled: row.targetEnabled,
|
subdomain: row.subdomain,
|
||||||
site: {
|
domainId: row.domainId,
|
||||||
siteId: row.siteId,
|
enabled: row.enabled,
|
||||||
type: row.siteType,
|
stickySession: row.stickySession,
|
||||||
subnet: row.subnet,
|
tlsServerName: row.tlsServerName,
|
||||||
exitNodeId: row.exitNodeId,
|
setHostHeader: row.setHostHeader,
|
||||||
online: row.siteOnline
|
enableProxy: row.enableProxy,
|
||||||
}
|
targets: [],
|
||||||
|
headers: row.headers,
|
||||||
|
path: row.path, // the targets will all have the same path
|
||||||
|
pathMatchType: row.pathMatchType // the targets will all have the same pathMatchType
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return Array.from(resourcesMap.values());
|
// Add target with its associated site data
|
||||||
|
resourcesMap.get(mapKey).targets.push({
|
||||||
|
resourceId: row.resourceId,
|
||||||
|
targetId: row.targetId,
|
||||||
|
ip: row.ip,
|
||||||
|
method: row.method,
|
||||||
|
port: row.port,
|
||||||
|
internalPort: row.internalPort,
|
||||||
|
enabled: row.targetEnabled,
|
||||||
|
site: {
|
||||||
|
siteId: row.siteId,
|
||||||
|
type: row.siteType,
|
||||||
|
subnet: row.subnet,
|
||||||
|
exitNodeId: row.exitNodeId,
|
||||||
|
online: row.siteOnline
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!allResources.length) {
|
// make sure we have at least one resource
|
||||||
|
if (resourcesMap.size === 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,14 +226,15 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const resource of allResources) {
|
// get the key and the resource
|
||||||
|
for (const [key, resource] of resourcesMap.entries()) {
|
||||||
const targets = resource.targets;
|
const targets = resource.targets;
|
||||||
|
|
||||||
const routerName = `${resource.resourceId}-router`;
|
const routerName = `${key}-router`;
|
||||||
const serviceName = `${resource.resourceId}-service`;
|
const serviceName = `${key}-service`;
|
||||||
const fullDomain = `${resource.fullDomain}`;
|
const fullDomain = `${resource.fullDomain}`;
|
||||||
const transportName = `${resource.resourceId}-transport`;
|
const transportName = `${key}-transport`;
|
||||||
const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`;
|
const headersMiddlewareName = `${key}-headers-middleware`;
|
||||||
|
|
||||||
if (!resource.enabled) {
|
if (!resource.enabled) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -238,9 +246,6 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resource.fullDomain) {
|
if (!resource.fullDomain) {
|
||||||
logger.error(
|
|
||||||
`Resource ${resource.resourceId} has no fullDomain`
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,16 +301,68 @@ export async function getTraefikConfig(
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
|
||||||
|
const routerMiddlewares = [
|
||||||
|
badgerMiddlewareName,
|
||||||
|
...additionalMiddlewares
|
||||||
|
];
|
||||||
|
|
||||||
|
if (resource.headers && resource.headers.length > 0) {
|
||||||
|
// if there are headers, parse them into an object
|
||||||
|
const headersObj: { [key: string]: string } = {};
|
||||||
|
const headersArr = resource.headers.split(",");
|
||||||
|
for (const header of headersArr) {
|
||||||
|
const [key, value] = header
|
||||||
|
.split(":")
|
||||||
|
.map((s: string) => s.trim());
|
||||||
|
if (key && value) {
|
||||||
|
headersObj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource.setHostHeader) {
|
||||||
|
headersObj["Host"] = resource.setHostHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the object is not empty
|
||||||
|
if (Object.keys(headersObj).length > 0) {
|
||||||
|
// Add the headers middleware
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
config_output.http.middlewares[headersMiddlewareName] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: headersObj
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
routerMiddlewares.push(headersMiddlewareName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rule = `Host(\`${fullDomain}\`)`;
|
||||||
|
let priority = 100;
|
||||||
|
if (resource.path && resource.pathMatchType) {
|
||||||
|
priority += 1;
|
||||||
|
// add path to rule based on match type
|
||||||
|
if (resource.pathMatchType === "exact") {
|
||||||
|
rule += ` && Path(\`${resource.path}\`)`;
|
||||||
|
} else if (resource.pathMatchType === "prefix") {
|
||||||
|
rule += ` && PathPrefix(\`${resource.path}\`)`;
|
||||||
|
} else if (resource.pathMatchType === "regex") {
|
||||||
|
rule += ` && PathRegexp(\`${resource.path}\`)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config_output.http.routers![routerName] = {
|
config_output.http.routers![routerName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
resource.ssl
|
resource.ssl
|
||||||
? config.getRawConfig().traefik.https_entrypoint
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
: config.getRawConfig().traefik.http_entrypoint
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
],
|
],
|
||||||
middlewares: [badgerMiddlewareName, ...additionalMiddlewares],
|
middlewares: routerMiddlewares,
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: rule,
|
||||||
priority: 100,
|
priority: priority,
|
||||||
...(resource.ssl ? { tls } : {})
|
...(resource.ssl ? { tls } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -316,8 +373,8 @@ export async function getTraefikConfig(
|
||||||
],
|
],
|
||||||
middlewares: [redirectHttpsMiddlewareName],
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: rule,
|
||||||
priority: 100
|
priority: priority
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,55 +391,64 @@ export async function getTraefikConfig(
|
||||||
targets as TargetWithSite[]
|
targets as TargetWithSite[]
|
||||||
).some((target: TargetWithSite) => target.site.online);
|
).some((target: TargetWithSite) => target.site.online);
|
||||||
|
|
||||||
return (targets as TargetWithSite[])
|
return (
|
||||||
.filter((target: TargetWithSite) => {
|
(targets as TargetWithSite[])
|
||||||
if (!target.enabled) {
|
.filter((target: TargetWithSite) => {
|
||||||
return false;
|
if (!target.enabled) {
|
||||||
}
|
|
||||||
|
|
||||||
// If any sites are online, exclude offline sites
|
|
||||||
if (anySitesOnline && !target.site.online) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!target.ip ||
|
|
||||||
!target.port ||
|
|
||||||
!target.method
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (target.site.type === "newt") {
|
|
||||||
if (
|
// If any sites are online, exclude offline sites
|
||||||
!target.internalPort ||
|
if (anySitesOnline && !target.site.online) {
|
||||||
!target.method ||
|
|
||||||
!target.site.subnet
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
if (
|
||||||
})
|
target.site.type === "local" ||
|
||||||
.map((target: TargetWithSite) => {
|
target.site.type === "wireguard"
|
||||||
if (
|
) {
|
||||||
target.site.type === "local" ||
|
if (
|
||||||
target.site.type === "wireguard"
|
!target.ip ||
|
||||||
) {
|
!target.port ||
|
||||||
return {
|
!target.method
|
||||||
url: `${target.method}://${target.ip}:${target.port}`
|
) {
|
||||||
};
|
return false;
|
||||||
} else if (target.site.type === "newt") {
|
}
|
||||||
const ip =
|
} else if (target.site.type === "newt") {
|
||||||
target.site.subnet!.split("/")[0];
|
if (
|
||||||
return {
|
!target.internalPort ||
|
||||||
url: `${target.method}://${ip}:${target.internalPort}`
|
!target.method ||
|
||||||
};
|
!target.site.subnet
|
||||||
}
|
) {
|
||||||
});
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((target: TargetWithSite) => {
|
||||||
|
if (
|
||||||
|
target.site.type === "local" ||
|
||||||
|
target.site.type === "wireguard"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
url: `${target.method}://${target.ip}:${target.port}`
|
||||||
|
};
|
||||||
|
} else if (target.site.type === "newt") {
|
||||||
|
const ip =
|
||||||
|
target.site.subnet!.split("/")[0];
|
||||||
|
return {
|
||||||
|
url: `${target.method}://${ip}:${target.internalPort}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// filter out duplicates
|
||||||
|
.filter(
|
||||||
|
(v, i, a) =>
|
||||||
|
a.findIndex(
|
||||||
|
(t) => t && v && t.url === v.url
|
||||||
|
) === i
|
||||||
|
)
|
||||||
|
);
|
||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
|
@ -413,27 +479,6 @@ export async function getTraefikConfig(
|
||||||
serviceName
|
serviceName
|
||||||
].loadBalancer.serversTransport = transportName;
|
].loadBalancer.serversTransport = transportName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the host header middleware
|
|
||||||
if (resource.setHostHeader) {
|
|
||||||
if (!config_output.http.middlewares) {
|
|
||||||
config_output.http.middlewares = {};
|
|
||||||
}
|
|
||||||
config_output.http.middlewares[hostHeaderMiddlewareName] = {
|
|
||||||
headers: {
|
|
||||||
customRequestHeaders: {
|
|
||||||
Host: resource.setHostHeader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (!config_output.http.routers![routerName].middlewares) {
|
|
||||||
config_output.http.routers![routerName].middlewares = [];
|
|
||||||
}
|
|
||||||
config_output.http.routers![routerName].middlewares = [
|
|
||||||
...config_output.http.routers![routerName].middlewares,
|
|
||||||
hostHeaderMiddlewareName
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Non-HTTP (TCP/UDP) configuration
|
// Non-HTTP (TCP/UDP) configuration
|
||||||
if (!resource.enableProxy) {
|
if (!resource.enableProxy) {
|
||||||
|
@ -529,3 +574,13 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
return config_output;
|
return config_output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizePath(path: string | null | undefined): string | undefined {
|
||||||
|
if (!path) return undefined;
|
||||||
|
// clean any non alphanumeric characters from the path and replace with dashes
|
||||||
|
// the path cant be too long either, so limit to 50 characters
|
||||||
|
if (path.length > 50) {
|
||||||
|
path = path.substring(0, 50);
|
||||||
|
}
|
||||||
|
return path.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
}
|
||||||
|
|
|
@ -84,7 +84,14 @@ export async function createOrgUser(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { username, email, name, type, idpId, roleId } = parsedBody.data;
|
const {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
idpId,
|
||||||
|
roleId
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
const [role] = await db
|
const [role] = await db
|
||||||
.select()
|
.select()
|
||||||
|
@ -141,7 +148,12 @@ export async function createOrgUser(
|
||||||
const [existingUser] = await trx
|
const [existingUser] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.username, username));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(users.username, username),
|
||||||
|
eq(users.idpId, idpId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
const [existingOrgUser] = await trx
|
const [existingOrgUser] = await trx
|
||||||
|
@ -168,7 +180,8 @@ export async function createOrgUser(
|
||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
userId: existingUser.userId,
|
userId: existingUser.userId,
|
||||||
roleId: role.roleId
|
roleId: role.roleId,
|
||||||
|
autoProvisioned: false
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else {
|
} else {
|
||||||
|
@ -184,7 +197,7 @@ export async function createOrgUser(
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
idpId,
|
idpId,
|
||||||
dateCreated: new Date().toISOString(),
|
dateCreated: new Date().toISOString(),
|
||||||
emailVerified: true
|
emailVerified: true,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
@ -193,7 +206,8 @@ export async function createOrgUser(
|
||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
userId: newUser.userId,
|
userId: newUser.userId,
|
||||||
roleId: role.roleId
|
roleId: role.roleId,
|
||||||
|
autoProvisioned: false
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
@ -204,7 +218,6 @@ export async function createOrgUser(
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(eq(userOrgs.orgId, orgId));
|
.where(eq(userOrgs.orgId, orgId));
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idp, idpOidcConfig } from "@server/db";
|
||||||
import { roles, userOrgs, users } from "@server/db";
|
import { roles, userOrgs, users } from "@server/db";
|
||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
|
@ -25,10 +25,18 @@ async function queryUser(orgId: string, userId: string) {
|
||||||
isOwner: userOrgs.isOwner,
|
isOwner: userOrgs.isOwner,
|
||||||
isAdmin: roles.isAdmin,
|
isAdmin: roles.isAdmin,
|
||||||
twoFactorEnabled: users.twoFactorEnabled,
|
twoFactorEnabled: users.twoFactorEnabled,
|
||||||
|
autoProvisioned: userOrgs.autoProvisioned,
|
||||||
|
idpId: users.idpId,
|
||||||
|
idpName: idp.name,
|
||||||
|
idpType: idp.type,
|
||||||
|
idpVariant: idpOidcConfig.variant,
|
||||||
|
idpAutoProvision: idp.autoProvision
|
||||||
})
|
})
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
.leftJoin(users, eq(userOrgs.userId, users.userId))
|
||||||
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId))
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (typeof user.roles === "string") {
|
if (typeof user.roles === "string") {
|
||||||
|
|
|
@ -14,3 +14,4 @@ export * from "./removeInvitation";
|
||||||
export * from "./createOrgUser";
|
export * from "./createOrgUser";
|
||||||
export * from "./adminUpdateUser2FA";
|
export * from "./adminUpdateUser2FA";
|
||||||
export * from "./adminGetUser";
|
export * from "./adminGetUser";
|
||||||
|
export * from "./updateOrgUser";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, idpOidcConfig } from "@server/db";
|
||||||
import { idp, roles, userOrgs, users } from "@server/db";
|
import { idp, roles, userOrgs, users } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
@ -61,12 +61,15 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
|
||||||
isOwner: userOrgs.isOwner,
|
isOwner: userOrgs.isOwner,
|
||||||
idpName: idp.name,
|
idpName: idp.name,
|
||||||
idpId: users.idpId,
|
idpId: users.idpId,
|
||||||
|
idpType: idp.type,
|
||||||
|
idpVariant: idpOidcConfig.variant,
|
||||||
twoFactorEnabled: users.twoFactorEnabled,
|
twoFactorEnabled: users.twoFactorEnabled,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
||||||
.where(eq(userOrgs.orgId, orgId))
|
.where(eq(userOrgs.orgId, orgId))
|
||||||
.groupBy(users.userId)
|
.groupBy(users.userId)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|
112
server/routers/user/updateOrgUser.ts
Normal file
112
server/routers/user/updateOrgUser.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const paramsSchema = z
|
||||||
|
.object({
|
||||||
|
userId: z.string(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const bodySchema = z
|
||||||
|
.object({
|
||||||
|
autoProvisioned: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
message: "At least one field must be provided for update"
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/user/{userId}",
|
||||||
|
description: "Update a user in an org.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: paramsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: bodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateOrgUser(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"User not found in this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
const [updatedUser] = await db
|
||||||
|
.update(userOrgs)
|
||||||
|
.set({
|
||||||
|
...updateData
|
||||||
|
})
|
||||||
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: updatedUser,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Org user updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,8 @@ import {
|
||||||
handleGetConfigMessage,
|
handleGetConfigMessage,
|
||||||
handleDockerStatusMessage,
|
handleDockerStatusMessage,
|
||||||
handleDockerContainersMessage,
|
handleDockerContainersMessage,
|
||||||
handleNewtPingRequestMessage
|
handleNewtPingRequestMessage,
|
||||||
|
handleApplyBlueprintMessage
|
||||||
} from "../newt";
|
} from "../newt";
|
||||||
import {
|
import {
|
||||||
handleOlmRegisterMessage,
|
handleOlmRegisterMessage,
|
||||||
|
@ -23,7 +24,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
||||||
"olm/ping": handleOlmPingMessage,
|
"olm/ping": handleOlmPingMessage,
|
||||||
"newt/socket/status": handleDockerStatusMessage,
|
"newt/socket/status": handleDockerStatusMessage,
|
||||||
"newt/socket/containers": handleDockerContainersMessage,
|
"newt/socket/containers": handleDockerContainersMessage,
|
||||||
"newt/ping/request": handleNewtPingRequestMessage
|
"newt/ping/request": handleNewtPingRequestMessage,
|
||||||
|
"newt/blueprint/apply": handleApplyBlueprintMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
startOlmOfflineChecker(); // this is to handle the offline check for olms
|
||||||
|
|
|
@ -9,6 +9,7 @@ import m1 from "./scriptsPg/1.6.0";
|
||||||
import m2 from "./scriptsPg/1.7.0";
|
import m2 from "./scriptsPg/1.7.0";
|
||||||
import m3 from "./scriptsPg/1.8.0";
|
import m3 from "./scriptsPg/1.8.0";
|
||||||
import m4 from "./scriptsPg/1.9.0";
|
import m4 from "./scriptsPg/1.9.0";
|
||||||
|
import m5 from "./scriptsPg/1.10.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
|
@ -18,7 +19,8 @@ const migrations = [
|
||||||
{ version: "1.6.0", run: m1 },
|
{ version: "1.6.0", run: m1 },
|
||||||
{ version: "1.7.0", run: m2 },
|
{ version: "1.7.0", run: m2 },
|
||||||
{ version: "1.8.0", run: m3 },
|
{ version: "1.8.0", run: m3 },
|
||||||
{ version: "1.9.0", run: m4 }
|
{ version: "1.9.0", run: m4 },
|
||||||
|
{ version: "1.10.0", run: m5 },
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
|
@ -26,6 +26,8 @@ import m21 from "./scriptsSqlite/1.6.0";
|
||||||
import m22 from "./scriptsSqlite/1.7.0";
|
import m22 from "./scriptsSqlite/1.7.0";
|
||||||
import m23 from "./scriptsSqlite/1.8.0";
|
import m23 from "./scriptsSqlite/1.8.0";
|
||||||
import m24 from "./scriptsSqlite/1.9.0";
|
import m24 from "./scriptsSqlite/1.9.0";
|
||||||
|
import m25 from "./scriptsSqlite/1.10.0";
|
||||||
|
import m26 from "./scriptsSqlite/1.10.1";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
|
@ -51,6 +53,8 @@ const migrations = [
|
||||||
{ version: "1.7.0", run: m22 },
|
{ version: "1.7.0", run: m22 },
|
||||||
{ version: "1.8.0", run: m23 },
|
{ version: "1.8.0", run: m23 },
|
||||||
{ version: "1.9.0", run: m24 },
|
{ version: "1.9.0", run: m24 },
|
||||||
|
{ version: "1.10.0", run: m25 },
|
||||||
|
{ version: "1.10.1", run: m26 },
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
147
server/setup/scriptsPg/1.10.0.ts
Normal file
147
server/setup/scriptsPg/1.10.0.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path, { join } from "path";
|
||||||
|
|
||||||
|
const version = "1.10.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resources = await db.execute(sql`
|
||||||
|
SELECT "resourceId" FROM "resources"
|
||||||
|
`);
|
||||||
|
|
||||||
|
const siteResources = await db.execute(sql`
|
||||||
|
SELECT "siteResourceId" FROM "siteResources"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "exitNodes" ADD COLUMN "region" text;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "idpOidcConfig" ADD COLUMN "variant" text DEFAULT 'oidc' NOT NULL;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "resources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "siteResources" ADD COLUMN "niceId" text DEFAULT '' NOT NULL;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "userOrgs" ADD COLUMN "autoProvisioned" boolean DEFAULT false;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "targets" ADD COLUMN "pathMatchType" text;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(sql`ALTER TABLE "targets" ADD COLUMN "path" text;`);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "resources" ADD COLUMN "headers" text;`
|
||||||
|
);
|
||||||
|
|
||||||
|
const usedNiceIds: string[] = [];
|
||||||
|
|
||||||
|
for (const resource of resources.rows) {
|
||||||
|
// Generate a unique name and ensure it's unique
|
||||||
|
let niceId = "";
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
niceId = generateName();
|
||||||
|
if (!usedNiceIds.includes(niceId)) {
|
||||||
|
usedNiceIds.push(niceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "resources" SET "niceId" = ${niceId} WHERE "resourceId" = ${resource.resourceId}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const resource of siteResources.rows) {
|
||||||
|
// Generate a unique name and ensure it's unique
|
||||||
|
let niceId = "";
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
niceId = generateName();
|
||||||
|
if (!usedNiceIds.includes(niceId)) {
|
||||||
|
usedNiceIds.push(niceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "siteResources" SET "niceId" = ${niceId} WHERE "siteResourceId" = ${resource.siteResourceId}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle auto-provisioned users for identity providers
|
||||||
|
const autoProvisionIdps = await db.execute(sql`
|
||||||
|
SELECT "idpId" FROM "idp" WHERE "autoProvision" = true
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (const idp of autoProvisionIdps.rows) {
|
||||||
|
// Get all users with this identity provider
|
||||||
|
const usersWithIdp = await db.execute(sql`
|
||||||
|
SELECT "id" FROM "user" WHERE "idpId" = ${idp.idpId}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Update userOrgs to set autoProvisioned to true for these users
|
||||||
|
for (const user of usersWithIdp.rows) {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "userOrgs" SET "autoProvisioned" = true WHERE "userId" = ${user.id}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
|
let file;
|
||||||
|
if (!dev) {
|
||||||
|
file = join(__DIRNAME, "names.json");
|
||||||
|
} else {
|
||||||
|
file = join("server/db/names.json");
|
||||||
|
}
|
||||||
|
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||||
|
|
||||||
|
export function generateName(): string {
|
||||||
|
const name = (
|
||||||
|
names.descriptors[
|
||||||
|
Math.floor(Math.random() * names.descriptors.length)
|
||||||
|
] +
|
||||||
|
"-" +
|
||||||
|
names.animals[Math.floor(Math.random() * names.animals.length)]
|
||||||
|
)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, "-");
|
||||||
|
|
||||||
|
// clean out any non-alphanumeric characters except for dashes
|
||||||
|
return name.replace(/[^a-z0-9-]/g, "");
|
||||||
|
}
|
136
server/setup/scriptsSqlite/1.10.0.ts
Normal file
136
server/setup/scriptsSqlite/1.10.0.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path, { join } from "path";
|
||||||
|
|
||||||
|
const version = "1.10.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resources = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT resourceId FROM resources"
|
||||||
|
)
|
||||||
|
.all() as Array<{ resourceId: number }>;
|
||||||
|
|
||||||
|
const siteResources = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT siteResourceId FROM siteResources"
|
||||||
|
)
|
||||||
|
.all() as Array<{ siteResourceId: number }>;
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE 'exitNodes' ADD 'region' text;
|
||||||
|
ALTER TABLE 'idpOidcConfig' ADD 'variant' text DEFAULT 'oidc' NOT NULL;
|
||||||
|
ALTER TABLE 'resources' ADD 'niceId' text DEFAULT '' NOT NULL;
|
||||||
|
ALTER TABLE 'siteResources' ADD 'niceId' text DEFAULT '' NOT NULL;
|
||||||
|
ALTER TABLE 'userOrgs' ADD 'autoProvisioned' integer DEFAULT false;
|
||||||
|
ALTER TABLE 'targets' ADD 'pathMatchType' text;
|
||||||
|
ALTER TABLE 'targets' ADD 'path' text;
|
||||||
|
ALTER TABLE 'resources' ADD 'headers' text;
|
||||||
|
`); // this diverges from the schema a bit because the schema does not have a default on niceId but was required for the migration and I dont think it will effect much down the line...
|
||||||
|
|
||||||
|
const usedNiceIds: string[] = [];
|
||||||
|
|
||||||
|
for (const resourceId of resources) {
|
||||||
|
// Generate a unique name and ensure it's unique
|
||||||
|
let niceId = "";
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
niceId = generateName();
|
||||||
|
if (!usedNiceIds.includes(niceId)) {
|
||||||
|
usedNiceIds.push(niceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE resources SET niceId = ? WHERE resourceId = ?`
|
||||||
|
).run(niceId, resourceId.resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const resourceId of siteResources) {
|
||||||
|
// Generate a unique name and ensure it's unique
|
||||||
|
let niceId = "";
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
niceId = generateName();
|
||||||
|
if (!usedNiceIds.includes(niceId)) {
|
||||||
|
usedNiceIds.push(niceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE siteResources SET niceId = ? WHERE siteResourceId = ?`
|
||||||
|
).run(niceId, resourceId.siteResourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle auto-provisioned users for identity providers
|
||||||
|
const autoProvisionIdps = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT idpId FROM idp WHERE autoProvision = 1"
|
||||||
|
)
|
||||||
|
.all() as Array<{ idpId: number }>;
|
||||||
|
|
||||||
|
for (const idp of autoProvisionIdps) {
|
||||||
|
// Get all users with this identity provider
|
||||||
|
const usersWithIdp = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT id FROM user WHERE idpId = ?"
|
||||||
|
)
|
||||||
|
.all(idp.idpId) as Array<{ id: string }>;
|
||||||
|
|
||||||
|
// Update userOrgs to set autoProvisioned to true for these users
|
||||||
|
for (const user of usersWithIdp) {
|
||||||
|
db.prepare(
|
||||||
|
"UPDATE userOrgs SET autoProvisioned = 1 WHERE userId = ?"
|
||||||
|
).run(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
|
let file;
|
||||||
|
if (!dev) {
|
||||||
|
file = join(__DIRNAME, "names.json");
|
||||||
|
} else {
|
||||||
|
file = join("server/db/names.json");
|
||||||
|
}
|
||||||
|
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||||
|
|
||||||
|
export function generateName(): string {
|
||||||
|
const name = (
|
||||||
|
names.descriptors[
|
||||||
|
Math.floor(Math.random() * names.descriptors.length)
|
||||||
|
] +
|
||||||
|
"-" +
|
||||||
|
names.animals[Math.floor(Math.random() * names.animals.length)]
|
||||||
|
)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, "-");
|
||||||
|
|
||||||
|
// clean out any non-alphanumeric characters except for dashes
|
||||||
|
return name.replace(/[^a-z0-9-]/g, "");
|
||||||
|
}
|
69
server/setup/scriptsSqlite/1.10.1.ts
Normal file
69
server/setup/scriptsSqlite/1.10.1.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.10.1";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(`ALTER TABLE "targets" RENAME TO "targets_old";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "targets" (
|
||||||
|
"targetId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"resourceId" INTEGER NOT NULL,
|
||||||
|
"siteId" INTEGER NOT NULL,
|
||||||
|
"ip" TEXT NOT NULL,
|
||||||
|
"method" TEXT,
|
||||||
|
"port" INTEGER NOT NULL,
|
||||||
|
"internalPort" INTEGER,
|
||||||
|
"enabled" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"path" TEXT,
|
||||||
|
"pathMatchType" TEXT,
|
||||||
|
FOREIGN KEY ("resourceId") REFERENCES "resources"("resourceId") ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ("siteId") REFERENCES "sites"("siteId") ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO "targets" (
|
||||||
|
"targetId",
|
||||||
|
"resourceId",
|
||||||
|
"siteId",
|
||||||
|
"ip",
|
||||||
|
"method",
|
||||||
|
"port",
|
||||||
|
"internalPort",
|
||||||
|
"enabled",
|
||||||
|
"path",
|
||||||
|
"pathMatchType"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
targetId,
|
||||||
|
resourceId,
|
||||||
|
siteId,
|
||||||
|
ip,
|
||||||
|
method,
|
||||||
|
port,
|
||||||
|
internalPort,
|
||||||
|
enabled,
|
||||||
|
path,
|
||||||
|
pathMatchType
|
||||||
|
FROM "targets_old";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "targets_old";`);
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrganizationLandingCard from "./OrganizationLandingCard";
|
import OrganizationLandingCard from "../../components/OrganizationLandingCard";
|
||||||
import MemberResourcesPortal from "./MemberResourcesPortal";
|
import MemberResourcesPortal from "../../components/MemberResourcesPortal";
|
||||||
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
|
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import InvitationsTable, { InvitationRow } from "./InvitationsTable";
|
import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable";
|
||||||
import { GetOrgResponse } from "@server/routers/org";
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
interface AccessLayoutProps {
|
interface AccessLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
resourceId: number | string;
|
|
||||||
orgId: string;
|
orgId: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import RolesTable, { RoleRow } from "./RolesTable";
|
import RolesTable, { RoleRow } from "../../../../../components/RolesTable";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { SetUserRolesResponse } from "@server/routers/user";
|
import { SetUserRolesResponse } from "@server/routers/user";
|
||||||
|
@ -34,6 +35,8 @@ import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
export default function AccessControlsPage() {
|
export default function AccessControlsPage() {
|
||||||
const { orgUser: user } = userOrgUserContext();
|
const { orgUser: user } = userOrgUserContext();
|
||||||
|
@ -61,14 +64,16 @@ export default function AccessControlsPage() {
|
||||||
text: z.string()
|
text: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1, { message: t('accessRoleSelectPlease') })
|
.min(1, { message: t('accessRoleSelectPlease') }),
|
||||||
|
autoProvisioned: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: user.username!,
|
username: user.username!,
|
||||||
roles: []
|
roles: [],
|
||||||
|
autoProvisioned: user.autoProvisioned || false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -80,10 +85,10 @@ export default function AccessControlsPage() {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('accessRoleErrorFetch'),
|
title: t("accessRoleErrorFetch"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('accessRoleErrorFetchDescription')
|
t("accessRoleErrorFetchDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -107,31 +112,38 @@ export default function AccessControlsPage() {
|
||||||
text: i.name
|
text: i.name
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api
|
try {
|
||||||
.post<
|
// Execute both API calls simultaneously
|
||||||
AxiosResponse<SetUserRolesResponse>
|
const [roleRes, userRes] = await Promise.all([
|
||||||
>(`/org/${user.orgId}/user/${user.userId}/roles`, { roleIds: values.roles.map((r) => parseInt(r.id)) })
|
api.post<AxiosResponse<SetUserRolesResponse>>(`/org/${user.orgId}/user/${user.userId}/roles`, {
|
||||||
.catch((e) => {
|
roleIds: values.roles.map((r) => parseInt(r.id)) }
|
||||||
toast({
|
),
|
||||||
variant: "destructive",
|
api.post(`/org/${orgId}/user/${user.userId}`, {
|
||||||
title: t('accessRoleErrorAdd'),
|
autoProvisioned: values.autoProvisioned
|
||||||
description: formatAxiosError(
|
})
|
||||||
e,
|
]);
|
||||||
t('accessRoleErrorAddDescription')
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
if (roleRes.status === 200 && userRes.status === 200) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: t("userSaved"),
|
||||||
|
description: t("userSavedDescription")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "destructive",
|
||||||
title: t('userSaved'),
|
title: t("accessRoleErrorAdd"),
|
||||||
description: t('userSavedDescription')
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("accessRoleErrorAddDescription")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,9 +154,11 @@ export default function AccessControlsPage() {
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>{t('accessControls')}</SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
|
{t("accessControls")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t('accessControlsDescription')}
|
{t("accessControlsDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
|
@ -156,12 +170,29 @@ export default function AccessControlsPage() {
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
|
{/* IDP Type Display */}
|
||||||
|
{user.type !== UserType.Internal &&
|
||||||
|
user.idpType && (
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("idp")}:
|
||||||
|
</span>
|
||||||
|
<IdpTypeBadge
|
||||||
|
type={user.idpType}
|
||||||
|
variant={
|
||||||
|
user.idpVariant || undefined
|
||||||
|
}
|
||||||
|
name={user.idpName || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="roles"
|
name="roles"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>Roles</FormLabel>
|
<FormLabel>{t('roles')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<TagInput
|
||||||
{...field}
|
{...field}
|
||||||
|
@ -184,6 +215,10 @@ export default function AccessControlsPage() {
|
||||||
...Tag[]
|
...Tag[]
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
// If auto provision is enabled, set it to false when role changes
|
||||||
|
if (user.idpAutoProvision) {
|
||||||
|
form.setValue("autoProvisioned", false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
enableAutocomplete={true}
|
enableAutocomplete={true}
|
||||||
autocompleteOptions={
|
autocompleteOptions={
|
||||||
|
@ -200,6 +235,35 @@ export default function AccessControlsPage() {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{user.idpAutoProvision && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="autoProvisioned"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-2 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>
|
||||||
|
{t("autoProvisioned")}
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"autoProvisionedDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
@ -212,7 +276,7 @@ export default function AccessControlsPage() {
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
form="access-controls-form"
|
form="access-controls-form"
|
||||||
>
|
>
|
||||||
{t('accessControlsSubmit')}
|
{t("accessControlsSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { ListIdpsResponse } from "@server/routers/idp";
|
import { ListIdpsResponse } from "@server/routers/idp";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
type UserType = "internal" | "oidc";
|
type UserType = "internal" | "oidc";
|
||||||
|
|
||||||
|
@ -53,6 +54,17 @@ interface IdpOption {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
variant: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserOption {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
disabled: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
idpId?: number;
|
||||||
|
variant?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
@ -62,14 +74,14 @@ export default function Page() {
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [userType, setUserType] = useState<UserType | null>("internal");
|
const [selectedOption, setSelectedOption] = useState<string | null>("internal");
|
||||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||||
const [idps, setIdps] = useState<IdpOption[]>([]);
|
const [idps, setIdps] = useState<IdpOption[]>([]);
|
||||||
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
|
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
|
||||||
const [selectedIdp, setSelectedIdp] = useState<IdpOption | null>(null);
|
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
|
|
||||||
const internalFormSchema = z.object({
|
const internalFormSchema = z.object({
|
||||||
|
@ -80,7 +92,13 @@ export default function Page() {
|
||||||
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalFormSchema = z.object({
|
const googleAzureFormSchema = z.object({
|
||||||
|
email: z.string().email({ message: t("emailInvalid") }),
|
||||||
|
name: z.string().optional(),
|
||||||
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
|
});
|
||||||
|
|
||||||
|
const genericOidcFormSchema = z.object({
|
||||||
username: z.string().min(1, { message: t("usernameRequired") }),
|
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
|
@ -88,19 +106,51 @@ export default function Page() {
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }),
|
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||||
idpId: z.string().min(1, { message: t("idpSelectPlease") })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatIdpType = (type: string) => {
|
const formatIdpType = (type: string) => {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case "oidc":
|
case "oidc":
|
||||||
return t("idpGenericOidc");
|
return t("idpGenericOidc");
|
||||||
|
case "google":
|
||||||
|
return t("idpGoogleDescription");
|
||||||
|
case "azure":
|
||||||
|
return t("idpAzureDescription");
|
||||||
default:
|
default:
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getIdpIcon = (variant: string | null) => {
|
||||||
|
if (!variant) return null;
|
||||||
|
|
||||||
|
switch (variant.toLowerCase()) {
|
||||||
|
case "google":
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/idp/google.png"
|
||||||
|
alt={t("idpGoogleAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "azure":
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src="/idp/azure.png"
|
||||||
|
alt={t("idpAzureAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validFor = [
|
const validFor = [
|
||||||
{ hours: 24, name: t("day", { count: 1 }) },
|
{ hours: 24, name: t("day", { count: 1 }) },
|
||||||
{ hours: 48, name: t("day", { count: 2 }) },
|
{ hours: 48, name: t("day", { count: 2 }) },
|
||||||
|
@ -120,45 +170,39 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalForm = useForm<z.infer<typeof externalFormSchema>>({
|
const googleAzureForm = useForm<z.infer<typeof googleAzureFormSchema>>({
|
||||||
resolver: zodResolver(externalFormSchema),
|
resolver: zodResolver(googleAzureFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
name: "",
|
||||||
|
roleId: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const genericOidcForm = useForm<z.infer<typeof genericOidcFormSchema>>({
|
||||||
|
resolver: zodResolver(genericOidcFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
name: "",
|
name: "",
|
||||||
roleId: "",
|
roleId: ""
|
||||||
idpId: ""
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userType === "internal") {
|
if (selectedOption === "internal") {
|
||||||
setSendEmail(env.email.emailEnabled);
|
setSendEmail(env.email.emailEnabled);
|
||||||
internalForm.reset();
|
internalForm.reset();
|
||||||
setInviteLink(null);
|
setInviteLink(null);
|
||||||
setExpiresInDays(1);
|
setExpiresInDays(1);
|
||||||
} else if (userType === "oidc") {
|
} else if (selectedOption && selectedOption !== "internal") {
|
||||||
externalForm.reset();
|
googleAzureForm.reset();
|
||||||
|
genericOidcForm.reset();
|
||||||
}
|
}
|
||||||
}, [userType, env.email.emailEnabled, internalForm, externalForm]);
|
}, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]);
|
||||||
|
|
||||||
const [userTypes, setUserTypes] = useState<StrategyOption<string>[]>([
|
|
||||||
{
|
|
||||||
id: "internal",
|
|
||||||
title: t("userTypeInternal"),
|
|
||||||
description: t("userTypeInternalDescription"),
|
|
||||||
disabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "oidc",
|
|
||||||
title: t("userTypeExternal"),
|
|
||||||
description: t("userTypeExternalDescription"),
|
|
||||||
disabled: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userType) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,20 +243,6 @@ export default function Page() {
|
||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
setIdps(res.data.data.idps);
|
setIdps(res.data.data.idps);
|
||||||
|
|
||||||
if (res.data.data.idps.length) {
|
|
||||||
setUserTypes((prev) =>
|
|
||||||
prev.map((type) => {
|
|
||||||
if (type.id === "oidc") {
|
|
||||||
return {
|
|
||||||
...type,
|
|
||||||
disabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return type;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +256,33 @@ export default function Page() {
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Build user options when IDPs are loaded
|
||||||
|
useEffect(() => {
|
||||||
|
const options: UserOption[] = [
|
||||||
|
{
|
||||||
|
id: "internal",
|
||||||
|
title: t("userTypeInternal"),
|
||||||
|
description: t("userTypeInternalDescription"),
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add IDP options
|
||||||
|
idps.forEach((idp) => {
|
||||||
|
options.push({
|
||||||
|
id: `idp-${idp.idpId}`,
|
||||||
|
title: idp.name,
|
||||||
|
description: formatIdpType(idp.variant || idp.type),
|
||||||
|
disabled: false,
|
||||||
|
icon: getIdpIcon(idp.variant),
|
||||||
|
idpId: idp.idpId,
|
||||||
|
variant: idp.variant
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setUserOptions(options);
|
||||||
|
}, [idps, t]);
|
||||||
|
|
||||||
async function onSubmitInternal(
|
async function onSubmitInternal(
|
||||||
values: z.infer<typeof internalFormSchema>
|
values: z.infer<typeof internalFormSchema>
|
||||||
) {
|
) {
|
||||||
|
@ -274,9 +331,52 @@ export default function Page() {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitExternal(
|
async function onSubmitGoogleAzure(
|
||||||
values: z.infer<typeof externalFormSchema>
|
values: z.infer<typeof googleAzureFormSchema>
|
||||||
) {
|
) {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.put(`/org/${orgId}/user`, {
|
||||||
|
username: values.email, // Use email as username for Google/Azure
|
||||||
|
email: values.email,
|
||||||
|
name: values.name,
|
||||||
|
type: "oidc",
|
||||||
|
idpId: selectedUserOption.idpId,
|
||||||
|
roleId: parseInt(values.roleId)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("userErrorCreate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("userErrorCreateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 201) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: t("userCreated"),
|
||||||
|
description: t("userCreatedDescription")
|
||||||
|
});
|
||||||
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmitGenericOidc(
|
||||||
|
values: z.infer<typeof genericOidcFormSchema>
|
||||||
|
) {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
|
@ -285,7 +385,7 @@ export default function Page() {
|
||||||
email: values.email,
|
email: values.email,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
type: "oidc",
|
type: "oidc",
|
||||||
idpId: parseInt(values.idpId),
|
idpId: selectedUserOption.idpId,
|
||||||
roleId: parseInt(values.roleId)
|
roleId: parseInt(values.roleId)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -330,7 +430,7 @@ export default function Page() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{!inviteLink && build !== "saas" ? (
|
{!inviteLink && build !== "saas" && dataLoaded ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
|
@ -342,15 +442,15 @@ export default function Page() {
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<StrategySelect
|
<StrategySelect
|
||||||
options={userTypes}
|
options={userOptions}
|
||||||
defaultValue={userType || undefined}
|
defaultValue={selectedOption || undefined}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setUserType(value as UserType);
|
setSelectedOption(value);
|
||||||
if (value === "internal") {
|
if (value === "internal") {
|
||||||
internalForm.reset();
|
internalForm.reset();
|
||||||
} else if (value === "oidc") {
|
} else {
|
||||||
externalForm.reset();
|
googleAzureForm.reset();
|
||||||
setSelectedIdp(null);
|
genericOidcForm.reset();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
cols={2}
|
cols={2}
|
||||||
|
@ -359,7 +459,7 @@ export default function Page() {
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{userType === "internal" && dataLoaded && (
|
{selectedOption === "internal" && dataLoaded && (
|
||||||
<>
|
<>
|
||||||
{!inviteLink ? (
|
{!inviteLink ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
|
@ -564,71 +664,7 @@ export default function Page() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{userType !== "internal" && dataLoaded && (
|
{selectedOption && selectedOption !== "internal" && dataLoaded && (
|
||||||
<>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("idpTitle")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("idpSelect")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
{idps.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{t("idpNotConfigured")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Form {...externalForm}>
|
|
||||||
<FormField
|
|
||||||
control={externalForm.control}
|
|
||||||
name="idpId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<StrategySelect
|
|
||||||
options={idps.map(
|
|
||||||
(idp) => ({
|
|
||||||
id: idp.idpId.toString(),
|
|
||||||
title: idp.name,
|
|
||||||
description:
|
|
||||||
formatIdpType(
|
|
||||||
idp.type
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
onChange={(
|
|
||||||
value
|
|
||||||
) => {
|
|
||||||
field.onChange(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
const idp =
|
|
||||||
idps.find(
|
|
||||||
(idp) =>
|
|
||||||
idp.idpId.toString() ===
|
|
||||||
value
|
|
||||||
);
|
|
||||||
setSelectedIdp(
|
|
||||||
idp || null
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
cols={2}
|
|
||||||
/>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
{idps.length > 0 && (
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
|
@ -640,144 +676,206 @@ export default function Page() {
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...externalForm}>
|
{/* Google/Azure Form */}
|
||||||
<form
|
{(() => {
|
||||||
onSubmit={externalForm.handleSubmit(
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
onSubmitExternal
|
return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure";
|
||||||
)}
|
})() && (
|
||||||
className="space-y-4"
|
<Form {...googleAzureForm}>
|
||||||
id="create-user-form"
|
<form
|
||||||
>
|
onSubmit={googleAzureForm.handleSubmit(
|
||||||
<FormField
|
onSubmitGoogleAzure
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"username"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{t(
|
|
||||||
"usernameUniq"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
|
id="create-user-form"
|
||||||
<FormField
|
>
|
||||||
control={
|
<FormField
|
||||||
externalForm.control
|
control={googleAzureForm.control}
|
||||||
}
|
name="email"
|
||||||
name="email"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>
|
||||||
<FormLabel>
|
{t("email")}
|
||||||
{t(
|
</FormLabel>
|
||||||
"emailOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"nameOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
externalForm.control
|
|
||||||
}
|
|
||||||
name="roleId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("role")}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<Input
|
||||||
<SelectValue
|
{...field}
|
||||||
placeholder={t(
|
/>
|
||||||
"accessRoleSelect"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
{roles.map(
|
</FormItem>
|
||||||
(
|
)}
|
||||||
role
|
/>
|
||||||
) => (
|
|
||||||
|
<FormField
|
||||||
|
control={googleAzureForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("nameOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={googleAzureForm.control}
|
||||||
|
name="roleId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("role")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("accessRoleSelect")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{roles.map((role) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={
|
key={role.roleId}
|
||||||
role.roleId
|
|
||||||
}
|
|
||||||
value={role.roleId.toString()}
|
value={role.roleId.toString()}
|
||||||
>
|
>
|
||||||
{
|
{role.name}
|
||||||
role.name
|
|
||||||
}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)
|
))}
|
||||||
)}
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Generic OIDC Form */}
|
||||||
|
{(() => {
|
||||||
|
const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
|
||||||
|
return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure";
|
||||||
|
})() && (
|
||||||
|
<Form {...genericOidcForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={genericOidcForm.handleSubmit(
|
||||||
|
onSubmitGenericOidc
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
</form>
|
id="create-user-form"
|
||||||
</Form>
|
>
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("username")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t("usernameUniq")}
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("emailOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("nameOptional")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={genericOidcForm.control}
|
||||||
|
name="roleId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("role")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("accessRoleSelect")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{roles.map((role) => (
|
||||||
|
<SelectItem
|
||||||
|
key={role.roleId}
|
||||||
|
value={role.roleId.toString()}
|
||||||
|
>
|
||||||
|
{role.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
<div className="flex justify-end space-x-2 mt-8">
|
||||||
{userType && dataLoaded && (
|
{selectedOption && dataLoaded && (
|
||||||
<Button
|
<Button
|
||||||
type={inviteLink ? "button" : "submit"}
|
type={inviteLink ? "button" : "submit"}
|
||||||
form={inviteLink ? undefined : "create-user-form"}
|
form={inviteLink ? undefined : "create-user-form"}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { ListUsersResponse } from "@server/routers/user";
|
import { ListUsersResponse } from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import UsersTable, { UserRow } from "./UsersTable";
|
import UsersTable, { UserRow } from "../../../../../components/UsersTable";
|
||||||
import { GetOrgResponse } from "@server/routers/org";
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
@ -77,6 +77,7 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
type: user.type,
|
type: user.type,
|
||||||
|
idpVariant: user.idpVariant,
|
||||||
idpId: user.idpId,
|
idpId: user.idpId,
|
||||||
idpName: user.idpName || t('idpNameInternal'),
|
idpName: user.idpName || t('idpNameInternal'),
|
||||||
status: t('userConfirmed'),
|
status: t('userConfirmed'),
|
||||||
|
|
|
@ -210,6 +210,11 @@ export default function Page() {
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault(); // block default enter refresh
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-site-form"
|
id="create-site-form"
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable";
|
import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable";
|
||||||
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export const dynamic = "force-dynamic";
|
||||||
export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
let apiKeys: ListOrgApiKeysResponse["apiKeys"] = [];
|
let apiKeys: ListOrgApiKeysResponse["apiKeys"] = [];
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<ListOrgApiKeysResponse>>(
|
const res = await internal.get<AxiosResponse<ListOrgApiKeysResponse>>(
|
||||||
|
|
|
@ -3,14 +3,14 @@ import { AxiosResponse } from "axios";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { GetClientResponse } from "@server/routers/client";
|
import { GetClientResponse } from "@server/routers/client";
|
||||||
import ClientInfoCard from "./ClientInfoCard";
|
import ClientInfoCard from "../../../../../components/ClientInfoCard";
|
||||||
import ClientProvider from "@app/providers/ClientProvider";
|
import ClientProvider from "@app/providers/ClientProvider";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
|
|
||||||
type SettingsLayoutProps = {
|
type SettingsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{ clientId: number; orgId: string }>;
|
params: Promise<{ clientId: number | string; orgId: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function ClientPage(props: {
|
export default async function ClientPage(props: {
|
||||||
params: Promise<{ orgId: string; clientId: number }>;
|
params: Promise<{ orgId: string; clientId: number | string }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
redirect(`/${params.orgId}/settings/clients/${params.clientId}/general`);
|
redirect(`/${params.orgId}/settings/clients/${params.clientId}/general`);
|
||||||
|
|
|
@ -42,6 +42,10 @@ import {
|
||||||
FaFreebsd,
|
FaFreebsd,
|
||||||
FaWindows
|
FaWindows
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
|
import {
|
||||||
|
SiNixos,
|
||||||
|
SiKubernetes
|
||||||
|
} from "react-icons/si";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
@ -248,10 +252,14 @@ export default function Page() {
|
||||||
return <FaApple className="h-4 w-4 mr-2" />;
|
return <FaApple className="h-4 w-4 mr-2" />;
|
||||||
case "docker":
|
case "docker":
|
||||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||||
|
case "kubernetes":
|
||||||
|
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||||
case "podman":
|
case "podman":
|
||||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
return <FaFreebsd className="h-4 w-4 mr-2" />;
|
return <FaFreebsd className="h-4 w-4 mr-2" />;
|
||||||
|
case "nixos":
|
||||||
|
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||||
default:
|
default:
|
||||||
return <Terminal className="h-4 w-4 mr-2" />;
|
return <Terminal className="h-4 w-4 mr-2" />;
|
||||||
}
|
}
|
||||||
|
@ -440,6 +448,11 @@ export default function Page() {
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault(); // block default enter refresh
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-client-form"
|
id="create-client-form"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { ClientRow } from "./ClientsTable";
|
import { ClientRow } from "../../../../components/ClientsTable";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { ListClientsResponse } from "@server/routers/client";
|
import { ListClientsResponse } from "@server/routers/client";
|
||||||
import ClientsTable from "./ClientsTable";
|
import ClientsTable from "../../../../components/ClientsTable";
|
||||||
|
|
||||||
type ClientsPageProps = {
|
type ClientsPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue