Compare commits
No commits in common. "oss" and "v1.9.4" have entirely different histories.
191 changed files with 1658 additions and 8633 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@v6
|
uses: actions/setup-go@v5
|
||||||
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@v5
|
uses: actions/setup-node@v4
|
||||||
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@v10
|
- uses: actions/stale@v9
|
||||||
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@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|
15
README.md
15
README.md
|
@ -20,24 +20,15 @@ _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-managed">
|
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
||||||
Quick Install Guide
|
Install Guide
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="mailto:contact@fossorial.io">
|
<a href="mailto:numbat@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
72
blueprint.py
|
@ -1,72 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
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,9 +16,8 @@ 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}}`)"
|
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||||
service: next-service
|
service: next-service
|
||||||
priority: 10
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
tls:
|
tls:
|
||||||
|
@ -28,7 +27,15 @@ http:
|
||||||
api-router:
|
api-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
service: api-service
|
service: api-service
|
||||||
priority: 100
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
# WebSocket router
|
||||||
|
ws-router:
|
||||||
|
rule: "Host(`{{.DashboardDomain}}`)"
|
||||||
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
tls:
|
tls:
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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: false,
|
minify: true,
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,7 @@ esbuild
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
sourcemap: "inline",
|
sourcemap: "external",
|
||||||
target: "node22",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module installer
|
module installer
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.35.0
|
golang.org/x/term v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.36.0 // indirect
|
require golang.org/x/sys v0.35.0 // indirect
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
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,8 +8,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -17,13 +15,14 @@ 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 = "1.9.4"
|
config.PangolinVersion = "replaceme"
|
||||||
config.GerbilVersion = "1.2.1"
|
config.GerbilVersion = "replaceme"
|
||||||
config.BadgerVersion = "1.2.0"
|
config.BadgerVersion = "replaceme"
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed config/*
|
//go:embed config/*
|
||||||
|
@ -75,7 +74,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. If you already have the Pangolin stack running, shut them down before proceeding.\n")
|
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,17 +204,8 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
err := installCrowdsec(config)
|
installCrowdsec(config)
|
||||||
if (err != nil) {
|
|
||||||
fmt.Printf("Error installing CrowdSec: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("CrowdSec installed successfully!")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,12 +328,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
config.HybridSecret = readString(reader, "Enter your secret", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get public IP as default
|
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
||||||
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)", "")
|
||||||
|
@ -599,32 +584,6 @@ 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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Socket",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. 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}",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
"createInternalResourceDialogDestinationIP": "Destination IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Socket",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. 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}",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
"createInternalResourceDialogDestinationIP": "Destination IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Lizenz",
|
"sidebarLicense": "Lizenz",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Docker Blaupause aktivieren",
|
"enableDockerSocket": "Docker Socket aktivieren",
|
||||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
"enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.",
|
||||||
"enableDockerSocketLink": "Mehr erfahren",
|
"enableDockerSocketLink": "Mehr erfahren",
|
||||||
"viewDockerContainers": "Docker Container anzeigen",
|
"viewDockerContainers": "Docker Container anzeigen",
|
||||||
"containersIn": "Container in {siteName}",
|
"containersIn": "Container in {siteName}",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Die IP-Adresse oder Hostname Adresse der Ressource im Netzwerk der Website.",
|
"createInternalResourceDialogDestinationIP": "Ziel-IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Clients (Beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Socket",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. 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}",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
|
"createInternalResourceDialogDestinationIP": "Destination IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Licencia",
|
"sidebarLicense": "Licencia",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clientes (Beta)",
|
||||||
"sidebarDomains": "Dominios",
|
"sidebarDomains": "Dominios",
|
||||||
"enableDockerSocket": "Habilitar Plano Docker",
|
"enableDockerSocket": "Habilitar conector Docker",
|
||||||
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
|
"enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. 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}",
|
||||||
|
@ -1242,7 +1234,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": "miapp.ejemplo.com",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "La dirección IP o nombre de host del recurso en la red del sitio.",
|
"createInternalResourceDialogDestinationIP": "IP de destino",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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 sur Pangolin",
|
"welcome": "Bienvenue à 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": "Supprimer",
|
"delete": "Supprimez",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"online": "En ligne",
|
"online": "En ligne",
|
||||||
"offline": "Hors ligne",
|
"offline": "Hors ligne",
|
||||||
"site": "Site",
|
"site": "Site",
|
||||||
"dataIn": "Données reçues",
|
"dataIn": "Données dans",
|
||||||
"dataOut": "Données envoyées",
|
"dataOut": "Données épuisé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 racine",
|
"baseDomain": "Domaine de base",
|
||||||
"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",
|
"type": "Type de texte",
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -598,7 +595,7 @@
|
||||||
"newtId": "ID Newt",
|
"newtId": "ID Newt",
|
||||||
"newtSecretKey": "Clé secrète Newt",
|
"newtSecretKey": "Clé secrète Newt",
|
||||||
"architecture": "Architecture",
|
"architecture": "Architecture",
|
||||||
"sites": "Sites",
|
"sites": "Espaces",
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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.",
|
||||||
|
@ -1128,7 +1120,7 @@
|
||||||
"sidebarOverview": "Aperçu",
|
"sidebarOverview": "Aperçu",
|
||||||
"sidebarHome": "Domicile",
|
"sidebarHome": "Domicile",
|
||||||
"sidebarSites": "Espaces",
|
"sidebarSites": "Espaces",
|
||||||
"sidebarResources": "Ressources",
|
"sidebarResources": "Ressource",
|
||||||
"sidebarAccessControl": "Contrôle d'accès",
|
"sidebarAccessControl": "Contrôle d'accès",
|
||||||
"sidebarUsers": "Utilisateurs",
|
"sidebarUsers": "Utilisateurs",
|
||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Licence",
|
"sidebarLicense": "Licence",
|
||||||
"sidebarClients": "Clients (Bêta)",
|
"sidebarClients": "Clients (Bêta)",
|
||||||
"sidebarDomains": "Domaines",
|
"sidebarDomains": "Domaines",
|
||||||
"enableDockerSocket": "Activer le Plan Docker",
|
"enableDockerSocket": "Activer Docker Socket",
|
||||||
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
|
"enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du 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}",
|
||||||
|
@ -1242,7 +1234,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": "monapp.exemple.com",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "L'adresse IP ou le nom d'hôte de la ressource sur le réseau du site.",
|
"createInternalResourceDialogDestinationIP": "IP de destination",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Licenza",
|
"sidebarLicense": "Licenza",
|
||||||
"sidebarClients": "Clienti (Beta)",
|
"sidebarClients": "Clienti (Beta)",
|
||||||
"sidebarDomains": "Domini",
|
"sidebarDomains": "Domini",
|
||||||
"enableDockerSocket": "Abilita Progetto Docker",
|
"enableDockerSocket": "Abilita Docker Socket",
|
||||||
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
|
"enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. 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}",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "L'indirizzo IP o hostname della risorsa nella rete del sito.",
|
"createInternalResourceDialogDestinationIP": "IP di Destinazione",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
|
||||||
"userSaved": "사용자 저장됨",
|
"userSaved": "사용자 저장됨",
|
||||||
"userSavedDescription": "사용자가 업데이트되었습니다.",
|
"userSavedDescription": "사용자가 업데이트되었습니다.",
|
||||||
"autoProvisioned": "자동 프로비저닝됨",
|
|
||||||
"autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다",
|
|
||||||
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
|
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
|
||||||
"accessControlsSubmit": "접근 제어 저장",
|
"accessControlsSubmit": "접근 제어 저장",
|
||||||
"roles": "역할",
|
"roles": "역할",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식",
|
"ipAddressErrorInvalidFormat": "잘못된 IP 주소 형식",
|
||||||
"ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟",
|
"ipAddressErrorInvalidOctet": "유효하지 않은 IP 주소 옥텟",
|
||||||
"path": "경로",
|
"path": "경로",
|
||||||
"matchPath": "경로 맞춤",
|
|
||||||
"ipAddressRange": "IP 범위",
|
"ipAddressRange": "IP 범위",
|
||||||
"rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.",
|
"rulesErrorFetch": "규칙을 가져오는 데 실패했습니다.",
|
||||||
"rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다",
|
"rulesErrorFetchDescription": "규칙을 가져오는 중 오류가 발생했습니다",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"idpConnectingToFinished": "연결됨",
|
"idpConnectingToFinished": "연결됨",
|
||||||
"idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.",
|
"idpErrorConnectingTo": "{name}에 연결하는 데 문제가 발생했습니다. 관리자에게 문의하십시오.",
|
||||||
"idpErrorNotFound": "IdP를 찾을 수 없습니다.",
|
"idpErrorNotFound": "IdP를 찾을 수 없습니다.",
|
||||||
"idpGoogleAlt": "구글",
|
|
||||||
"idpAzureAlt": "애저",
|
|
||||||
"inviteInvalid": "유효하지 않은 초대",
|
"inviteInvalid": "유효하지 않은 초대",
|
||||||
"inviteInvalidDescription": "초대 링크가 유효하지 않습니다.",
|
"inviteInvalidDescription": "초대 링크가 유효하지 않습니다.",
|
||||||
"inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다",
|
"inviteErrorWrongUser": "이 초대는 이 사용자에게 해당되지 않습니다",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
|
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
|
||||||
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
|
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
|
||||||
"actionGetOrg": "조직 가져오기",
|
"actionGetOrg": "조직 가져오기",
|
||||||
"updateOrgUser": "조직 사용자 업데이트",
|
|
||||||
"createOrgUser": "조직 사용자 생성",
|
|
||||||
"actionUpdateOrg": "조직 업데이트",
|
"actionUpdateOrg": "조직 업데이트",
|
||||||
"actionUpdateUser": "사용자 업데이트",
|
"actionUpdateUser": "사용자 업데이트",
|
||||||
"actionGetUser": "사용자 조회",
|
"actionGetUser": "사용자 조회",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"actionDeleteSite": "사이트 삭제",
|
"actionDeleteSite": "사이트 삭제",
|
||||||
"actionGetSite": "사이트 가져오기",
|
"actionGetSite": "사이트 가져오기",
|
||||||
"actionListSites": "사이트 목록",
|
"actionListSites": "사이트 목록",
|
||||||
"actionApplyBlueprint": "청사진 적용",
|
|
||||||
"setupToken": "설정 토큰",
|
"setupToken": "설정 토큰",
|
||||||
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
|
||||||
"setupTokenRequired": "설정 토큰이 필요합니다",
|
"setupTokenRequired": "설정 토큰이 필요합니다",
|
||||||
|
@ -1141,8 +1133,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}의 컨테이너",
|
||||||
|
@ -1242,7 +1234,7 @@
|
||||||
"newtUpdateAvailable": "업데이트 가능",
|
"newtUpdateAvailable": "업데이트 가능",
|
||||||
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
|
||||||
"domainPickerEnterDomain": "도메인",
|
"domainPickerEnterDomain": "도메인",
|
||||||
"domainPickerPlaceholder": "myapp.example.com",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
|
||||||
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
"domainPickerDescription": "리소스의 전체 도메인을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
"domainPickerDescriptionSaas": "전체 도메인, 서브도메인 또는 이름을 입력하여 사용 가능한 옵션을 확인하십시오.",
|
||||||
"domainPickerTabAll": "모두",
|
"domainPickerTabAll": "모두",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"editInternalResourceDialogProtocol": "프로토콜",
|
"editInternalResourceDialogProtocol": "프로토콜",
|
||||||
"editInternalResourceDialogSitePort": "사이트 포트",
|
"editInternalResourceDialogSitePort": "사이트 포트",
|
||||||
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
"editInternalResourceDialogTargetConfiguration": "대상 구성",
|
||||||
|
"editInternalResourceDialogDestinationIP": "대상 IP",
|
||||||
|
"editInternalResourceDialogDestinationPort": "대상 IP의 포트",
|
||||||
"editInternalResourceDialogCancel": "취소",
|
"editInternalResourceDialogCancel": "취소",
|
||||||
"editInternalResourceDialogSaveResource": "리소스 저장",
|
"editInternalResourceDialogSaveResource": "리소스 저장",
|
||||||
"editInternalResourceDialogSuccess": "성공",
|
"editInternalResourceDialogSuccess": "성공",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"createInternalResourceDialogSitePort": "사이트 포트",
|
"createInternalResourceDialogSitePort": "사이트 포트",
|
||||||
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
"createInternalResourceDialogSitePortDescription": "사이트에 연결되었을 때 리소스에 접근하기 위해 이 포트를 사용합니다.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
"createInternalResourceDialogTargetConfiguration": "대상 설정",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 또는 호스트 네임 주소입니다.",
|
"createInternalResourceDialogDestinationIP": "대상 IP",
|
||||||
|
"createInternalResourceDialogDestinationIPDescription": "사이트 네트워크의 자원 IP 주소입니다.",
|
||||||
|
"createInternalResourceDialogDestinationPort": "대상 포트",
|
||||||
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
"createInternalResourceDialogDestinationPortDescription": "대상 IP에서 리소스에 접근할 수 있는 포트입니다.",
|
||||||
"createInternalResourceDialogCancel": "취소",
|
"createInternalResourceDialogCancel": "취소",
|
||||||
"createInternalResourceDialogCreateResource": "리소스 생성",
|
"createInternalResourceDialogCreateResource": "리소스 생성",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Lisens",
|
"sidebarLicense": "Lisens",
|
||||||
"sidebarClients": "Klienter (Beta)",
|
"sidebarClients": "Klienter (Beta)",
|
||||||
"sidebarDomains": "Domener",
|
"sidebarDomains": "Domener",
|
||||||
"enableDockerSocket": "Aktiver Docker blåkopi",
|
"enableDockerSocket": "Aktiver Docker Socket",
|
||||||
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
|
"enableDockerSocketDescription": "Aktiver Docker Socket-oppdagelse for å fylle ut containerinformasjon. Socket-stien må oppgis til Newt.",
|
||||||
"enableDockerSocketLink": "Lær mer",
|
"enableDockerSocketLink": "Lær mer",
|
||||||
"viewDockerContainers": "Vis Docker-containere",
|
"viewDockerContainers": "Vis Docker-containere",
|
||||||
"containersIn": "Containere i {siteName}",
|
"containersIn": "Containere i {siteName}",
|
||||||
|
@ -1242,7 +1234,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.no",
|
"domainPickerPlaceholder": "minapp.eksempel.com, api.v1.mittdomene.com, eller bare minapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.",
|
"createInternalResourceDialogDestinationIP": "Destinasjons-IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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": "Referentie",
|
"site": "Website",
|
||||||
"dataIn": "Dataverbruik inkomend",
|
"dataIn": "Gegevens in",
|
||||||
"dataOut": "Dataverbruik uitgaand",
|
"dataOut": "Data Uit",
|
||||||
"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": "Sluiten",
|
"close": "Afsluiten",
|
||||||
"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": "Newttunnel (Aanbevolen)",
|
"siteNewtTunnel": "Nieuwstunnel (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 aanmaken",
|
"resourceDescription": "Veilige proxy's voor uw privé applicaties maken",
|
||||||
"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": "Beveiligd",
|
"protected": "Beschermd",
|
||||||
"notProtected": "Niet beveiligd",
|
"notProtected": "Niet beschermd",
|
||||||
"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": "TCP/UDP bron",
|
"resourceRaw": "Ruwe 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": "Deze instantie verwijderen is onomkeerbaar. Bevestig alstublieft dat u wilt doorgaan.",
|
"orgDangerZoneDescription": "Als u deze instantie verwijdert, is er geen weg terug. Wees het alstublieft zeker.",
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -501,8 +499,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 doel toegevoegd. Voeg deze toe via dit formulier.",
|
"targetNoOne": "Geen doelwitten. Voeg een doel toe via het formulier.",
|
||||||
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal load balancering mogelijk maken.",
|
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de 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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -598,7 +595,7 @@
|
||||||
"newtId": "Newt-ID",
|
"newtId": "Newt-ID",
|
||||||
"newtSecretKey": "Nieuwe geheime sleutel",
|
"newtSecretKey": "Nieuwe geheime sleutel",
|
||||||
"architecture": "Architectuur",
|
"architecture": "Architectuur",
|
||||||
"sites": "Verbindingen",
|
"sites": "Werkruimtes",
|
||||||
"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",
|
||||||
|
@ -729,7 +726,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": "Identiteitsaanbieders",
|
"idp": "Identiteit aanbieders",
|
||||||
"idpSearch": "Identiteitsaanbieders zoeken...",
|
"idpSearch": "Identiteitsaanbieders zoeken...",
|
||||||
"idpAdd": "Identity Provider toevoegen",
|
"idpAdd": "Identity Provider toevoegen",
|
||||||
"idpClientIdRequired": "Client-ID is vereist.",
|
"idpClientIdRequired": "Client-ID is vereist.",
|
||||||
|
@ -801,7 +798,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": "Organisatie",
|
"org": "Rekening",
|
||||||
"orgSelect": "Selecteer organisatie",
|
"orgSelect": "Selecteer organisatie",
|
||||||
"orgSearch": "Zoek in org",
|
"orgSearch": "Zoek in org",
|
||||||
"orgNotFound": "Geen org gevonden.",
|
"orgNotFound": "Geen org gevonden.",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -976,10 +971,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 beheer",
|
"serverAdmin": "Server Beheerder",
|
||||||
"managedSelfhosted": "Beheerde Self-Hosted",
|
"managedSelfhosted": "Beheerde Self-Hosted",
|
||||||
"otpEnable": "Twee-factor inschakelen",
|
"otpEnable": "Twee-factor inschakelen",
|
||||||
"otpDisable": "Tweestapsverificatie uitschakelen",
|
"otpDisable": "Tweestapsverificatie uitschakelen",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1128,7 +1120,7 @@
|
||||||
"sidebarOverview": "Overzicht.",
|
"sidebarOverview": "Overzicht.",
|
||||||
"sidebarHome": "Startpagina",
|
"sidebarHome": "Startpagina",
|
||||||
"sidebarSites": "Werkruimtes",
|
"sidebarSites": "Werkruimtes",
|
||||||
"sidebarResources": "Bronnen",
|
"sidebarResources": "Hulpmiddelen",
|
||||||
"sidebarAccessControl": "Toegangs controle",
|
"sidebarAccessControl": "Toegangs controle",
|
||||||
"sidebarUsers": "Gebruikers",
|
"sidebarUsers": "Gebruikers",
|
||||||
"sidebarInvitations": "Uitnodigingen",
|
"sidebarInvitations": "Uitnodigingen",
|
||||||
|
@ -1141,13 +1133,13 @@
|
||||||
"sidebarLicense": "Licentie",
|
"sidebarLicense": "Licentie",
|
||||||
"sidebarClients": "Clients (Bèta)",
|
"sidebarClients": "Clients (Bèta)",
|
||||||
"sidebarDomains": "Domeinen",
|
"sidebarDomains": "Domeinen",
|
||||||
"enableDockerSocket": "Schakel Docker Blauwdruk in",
|
"enableDockerSocket": "Docker Socket inschakelen",
|
||||||
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
|
"enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.",
|
||||||
"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",
|
||||||
|
@ -1242,7 +1234,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.nl",
|
"domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp",
|
||||||
"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",
|
||||||
|
@ -1349,7 +1341,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 deze naar een veilige plek.",
|
"clientCredentialsSaveDescription": "Je kunt dit slechts één keer zien. Kopieer het naar een beveiligde 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.",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Het IP of hostnaam adres van de bron op het netwerk van de site.",
|
"createInternalResourceDialogDestinationIP": "Bestemming IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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ł",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Licencja",
|
"sidebarLicense": "Licencja",
|
||||||
"sidebarClients": "Klienci (Beta)",
|
"sidebarClients": "Klienci (Beta)",
|
||||||
"sidebarDomains": "Domeny",
|
"sidebarDomains": "Domeny",
|
||||||
"enableDockerSocket": "Włącz schemat dokera",
|
"enableDockerSocket": "Włącz gniazdo dokera",
|
||||||
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
|
"enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka 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}",
|
||||||
|
@ -1242,7 +1234,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": "mojapp.example.com",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Adres IP lub nazwa hosta zasobu w sieci witryny.",
|
"createInternalResourceDialogDestinationIP": "IP docelowe",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Tipo:",
|
"sidebarLicense": "Tipo:",
|
||||||
"sidebarClients": "Clientes (Beta)",
|
"sidebarClients": "Clientes (Beta)",
|
||||||
"sidebarDomains": "Domínios",
|
"sidebarDomains": "Domínios",
|
||||||
"enableDockerSocket": "Habilitar o Diagrama Docker",
|
"enableDockerSocket": "Habilitar Docker Socket",
|
||||||
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
|
"enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao 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}",
|
||||||
|
@ -1242,7 +1234,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": "myapp.exemplo.com",
|
"domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp",
|
||||||
"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",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "O IP ou endereço do hostname do recurso na rede do site.",
|
"createInternalResourceDialogDestinationIP": "IP de Destino",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.",
|
"accessRoleErrorAddDescription": "Произошла ошибка при добавлении пользователя в роль.",
|
||||||
"userSaved": "Пользователь сохранён",
|
"userSaved": "Пользователь сохранён",
|
||||||
"userSavedDescription": "Пользователь был обновлён.",
|
"userSavedDescription": "Пользователь был обновлён.",
|
||||||
"autoProvisioned": "Автоподбор",
|
|
||||||
"autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем",
|
|
||||||
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
|
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
|
||||||
"accessControlsSubmit": "Сохранить контроль доступа",
|
"accessControlsSubmit": "Сохранить контроль доступа",
|
||||||
"roles": "Роли",
|
"roles": "Роли",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"ipAddressErrorInvalidFormat": "Неверный формат IP адреса",
|
"ipAddressErrorInvalidFormat": "Неверный формат IP адреса",
|
||||||
"ipAddressErrorInvalidOctet": "Неверный октет IP адреса",
|
"ipAddressErrorInvalidOctet": "Неверный октет IP адреса",
|
||||||
"path": "Путь",
|
"path": "Путь",
|
||||||
"matchPath": "Путь матча",
|
|
||||||
"ipAddressRange": "Диапазон IP",
|
"ipAddressRange": "Диапазон IP",
|
||||||
"rulesErrorFetch": "Не удалось получить правила",
|
"rulesErrorFetch": "Не удалось получить правила",
|
||||||
"rulesErrorFetchDescription": "Произошла ошибка при получении правил",
|
"rulesErrorFetchDescription": "Произошла ошибка при получении правил",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"idpConnectingToFinished": "Подключено",
|
"idpConnectingToFinished": "Подключено",
|
||||||
"idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.",
|
"idpErrorConnectingTo": "Возникла проблема при подключении к {name}. Пожалуйста, свяжитесь с вашим администратором.",
|
||||||
"idpErrorNotFound": "IdP не найден",
|
"idpErrorNotFound": "IdP не найден",
|
||||||
"idpGoogleAlt": "Google",
|
|
||||||
"idpAzureAlt": "Azure",
|
|
||||||
"inviteInvalid": "Недействительное приглашение",
|
"inviteInvalid": "Недействительное приглашение",
|
||||||
"inviteInvalidDescription": "Ссылка на приглашение недействительна.",
|
"inviteInvalidDescription": "Ссылка на приглашение недействительна.",
|
||||||
"inviteErrorWrongUser": "Приглашение не для этого пользователя",
|
"inviteErrorWrongUser": "Приглашение не для этого пользователя",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
|
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
|
||||||
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
||||||
"actionGetOrg": "Получить организацию",
|
"actionGetOrg": "Получить организацию",
|
||||||
"updateOrgUser": "Обновить пользователя Org",
|
|
||||||
"createOrgUser": "Создать пользователя Org",
|
|
||||||
"actionUpdateOrg": "Обновить организацию",
|
"actionUpdateOrg": "Обновить организацию",
|
||||||
"actionUpdateUser": "Обновить пользователя",
|
"actionUpdateUser": "Обновить пользователя",
|
||||||
"actionGetUser": "Получить пользователя",
|
"actionGetUser": "Получить пользователя",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"actionDeleteSite": "Удалить сайт",
|
"actionDeleteSite": "Удалить сайт",
|
||||||
"actionGetSite": "Получить сайт",
|
"actionGetSite": "Получить сайт",
|
||||||
"actionListSites": "Список сайтов",
|
"actionListSites": "Список сайтов",
|
||||||
"actionApplyBlueprint": "Применить чертёж",
|
|
||||||
"setupToken": "Код настройки",
|
"setupToken": "Код настройки",
|
||||||
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
|
||||||
"setupTokenRequired": "Токен настройки обязателен",
|
"setupTokenRequired": "Токен настройки обязателен",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Лицензия",
|
"sidebarLicense": "Лицензия",
|
||||||
"sidebarClients": "Клиенты (бета)",
|
"sidebarClients": "Клиенты (бета)",
|
||||||
"sidebarDomains": "Домены",
|
"sidebarDomains": "Домены",
|
||||||
"enableDockerSocket": "Включить чертёж Docker",
|
"enableDockerSocket": "Включить Docker Socket",
|
||||||
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
|
"enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.",
|
||||||
"enableDockerSocketLink": "Узнать больше",
|
"enableDockerSocketLink": "Узнать больше",
|
||||||
"viewDockerContainers": "Просмотр контейнеров Docker",
|
"viewDockerContainers": "Просмотр контейнеров Docker",
|
||||||
"containersIn": "Контейнеры в {siteName}",
|
"containersIn": "Контейнеры в {siteName}",
|
||||||
|
@ -1242,7 +1234,7 @@
|
||||||
"newtUpdateAvailable": "Доступно обновление",
|
"newtUpdateAvailable": "Доступно обновление",
|
||||||
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
"newtUpdateAvailableInfo": "Доступна новая версия Newt. Пожалуйста, обновитесь до последней версии для лучшего опыта.",
|
||||||
"domainPickerEnterDomain": "Домен",
|
"domainPickerEnterDomain": "Домен",
|
||||||
"domainPickerPlaceholder": "myapp.example.com",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, или просто myapp",
|
||||||
"domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.",
|
"domainPickerDescription": "Введите полный домен ресурса, чтобы увидеть доступные опции.",
|
||||||
"domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции",
|
"domainPickerDescriptionSaas": "Введите полный домен, поддомен или просто имя, чтобы увидеть доступные опции",
|
||||||
"domainPickerTabAll": "Все",
|
"domainPickerTabAll": "Все",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"editInternalResourceDialogProtocol": "Протокол",
|
"editInternalResourceDialogProtocol": "Протокол",
|
||||||
"editInternalResourceDialogSitePort": "Порт сайта",
|
"editInternalResourceDialogSitePort": "Порт сайта",
|
||||||
"editInternalResourceDialogTargetConfiguration": "Настройка цели",
|
"editInternalResourceDialogTargetConfiguration": "Настройка цели",
|
||||||
|
"editInternalResourceDialogDestinationIP": "Целевая IP",
|
||||||
|
"editInternalResourceDialogDestinationPort": "Целевой порт",
|
||||||
"editInternalResourceDialogCancel": "Отмена",
|
"editInternalResourceDialogCancel": "Отмена",
|
||||||
"editInternalResourceDialogSaveResource": "Сохранить ресурс",
|
"editInternalResourceDialogSaveResource": "Сохранить ресурс",
|
||||||
"editInternalResourceDialogSuccess": "Успешно",
|
"editInternalResourceDialogSuccess": "Успешно",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"createInternalResourceDialogSitePort": "Порт сайта",
|
"createInternalResourceDialogSitePort": "Порт сайта",
|
||||||
"createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.",
|
"createInternalResourceDialogSitePortDescription": "Используйте этот порт для доступа к ресурсу на сайте при подключении с клиентом.",
|
||||||
"createInternalResourceDialogTargetConfiguration": "Настройка цели",
|
"createInternalResourceDialogTargetConfiguration": "Настройка цели",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "IP или адрес хоста ресурса в сети сайта.",
|
"createInternalResourceDialogDestinationIP": "Целевая IP",
|
||||||
|
"createInternalResourceDialogDestinationIPDescription": "IP-адрес ресурса в сети сайта.",
|
||||||
|
"createInternalResourceDialogDestinationPort": "Целевой порт",
|
||||||
"createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.",
|
"createInternalResourceDialogDestinationPortDescription": "Порт на IP-адресе назначения, где доступен ресурс.",
|
||||||
"createInternalResourceDialogCancel": "Отмена",
|
"createInternalResourceDialogCancel": "Отмена",
|
||||||
"createInternalResourceDialogCreateResource": "Создать ресурс",
|
"createInternalResourceDialogCreateResource": "Создать ресурс",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"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",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"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",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"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",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"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",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"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",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "Lisans",
|
"sidebarLicense": "Lisans",
|
||||||
"sidebarClients": "Müşteriler (Beta)",
|
"sidebarClients": "Müşteriler (Beta)",
|
||||||
"sidebarDomains": "Alan Adları",
|
"sidebarDomains": "Alan Adları",
|
||||||
"enableDockerSocket": "Docker Soketini Etkinleştir",
|
"enableDockerSocket": "Docker Soketi Etkinleştir",
|
||||||
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
|
"enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e 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",
|
||||||
|
@ -1242,7 +1234,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",
|
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp",
|
||||||
"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ü",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"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ı",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"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",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "Kaynağın site ağındaki IP veya ana bilgisayar adresi.",
|
"createInternalResourceDialogDestinationIP": "Hedef IP",
|
||||||
|
"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",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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,8 +454,6 @@
|
||||||
"accessRoleErrorAddDescription": "添加用户到角色时出错。",
|
"accessRoleErrorAddDescription": "添加用户到角色时出错。",
|
||||||
"userSaved": "用户已保存",
|
"userSaved": "用户已保存",
|
||||||
"userSavedDescription": "用户已更新。",
|
"userSavedDescription": "用户已更新。",
|
||||||
"autoProvisioned": "自动设置",
|
|
||||||
"autoProvisionedDescription": "允许此用户由身份提供商自动管理",
|
|
||||||
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
|
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
|
||||||
"accessControlsSubmit": "保存访问控制",
|
"accessControlsSubmit": "保存访问控制",
|
||||||
"roles": "角色",
|
"roles": "角色",
|
||||||
|
@ -513,7 +511,6 @@
|
||||||
"ipAddressErrorInvalidFormat": "无效的 IP 地址格式",
|
"ipAddressErrorInvalidFormat": "无效的 IP 地址格式",
|
||||||
"ipAddressErrorInvalidOctet": "无效的 IP 地址",
|
"ipAddressErrorInvalidOctet": "无效的 IP 地址",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"matchPath": "匹配路径",
|
|
||||||
"ipAddressRange": "IP 范围",
|
"ipAddressRange": "IP 范围",
|
||||||
"rulesErrorFetch": "获取规则失败",
|
"rulesErrorFetch": "获取规则失败",
|
||||||
"rulesErrorFetchDescription": "获取规则时出错",
|
"rulesErrorFetchDescription": "获取规则时出错",
|
||||||
|
@ -914,8 +911,6 @@
|
||||||
"idpConnectingToFinished": "已连接",
|
"idpConnectingToFinished": "已连接",
|
||||||
"idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。",
|
"idpErrorConnectingTo": "无法连接到 {name},请联系管理员协助处理。",
|
||||||
"idpErrorNotFound": "找不到 IdP",
|
"idpErrorNotFound": "找不到 IdP",
|
||||||
"idpGoogleAlt": "Google",
|
|
||||||
"idpAzureAlt": "Azure",
|
|
||||||
"inviteInvalid": "无效邀请",
|
"inviteInvalid": "无效邀请",
|
||||||
"inviteInvalidDescription": "邀请链接无效。",
|
"inviteInvalidDescription": "邀请链接无效。",
|
||||||
"inviteErrorWrongUser": "邀请不是该用户的",
|
"inviteErrorWrongUser": "邀请不是该用户的",
|
||||||
|
@ -987,8 +982,6 @@
|
||||||
"licenseTierProfessionalRequired": "需要专业版",
|
"licenseTierProfessionalRequired": "需要专业版",
|
||||||
"licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。",
|
"licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。",
|
||||||
"actionGetOrg": "获取组织",
|
"actionGetOrg": "获取组织",
|
||||||
"updateOrgUser": "更新组织用户",
|
|
||||||
"createOrgUser": "创建组织用户",
|
|
||||||
"actionUpdateOrg": "更新组织",
|
"actionUpdateOrg": "更新组织",
|
||||||
"actionUpdateUser": "更新用户",
|
"actionUpdateUser": "更新用户",
|
||||||
"actionGetUser": "获取用户",
|
"actionGetUser": "获取用户",
|
||||||
|
@ -998,7 +991,6 @@
|
||||||
"actionDeleteSite": "删除站点",
|
"actionDeleteSite": "删除站点",
|
||||||
"actionGetSite": "获取站点",
|
"actionGetSite": "获取站点",
|
||||||
"actionListSites": "站点列表",
|
"actionListSites": "站点列表",
|
||||||
"actionApplyBlueprint": "应用蓝图",
|
|
||||||
"setupToken": "设置令牌",
|
"setupToken": "设置令牌",
|
||||||
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
"setupTokenDescription": "从服务器控制台输入设置令牌。",
|
||||||
"setupTokenRequired": "需要设置令牌",
|
"setupTokenRequired": "需要设置令牌",
|
||||||
|
@ -1141,8 +1133,8 @@
|
||||||
"sidebarLicense": "证书",
|
"sidebarLicense": "证书",
|
||||||
"sidebarClients": "客户端(测试版)",
|
"sidebarClients": "客户端(测试版)",
|
||||||
"sidebarDomains": "域",
|
"sidebarDomains": "域",
|
||||||
"enableDockerSocket": "启用 Docker 蓝图",
|
"enableDockerSocket": "启用停靠套接字",
|
||||||
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
|
"enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。",
|
||||||
"enableDockerSocketLink": "了解更多",
|
"enableDockerSocketLink": "了解更多",
|
||||||
"viewDockerContainers": "查看停靠容器",
|
"viewDockerContainers": "查看停靠容器",
|
||||||
"containersIn": "{siteName} 中的容器",
|
"containersIn": "{siteName} 中的容器",
|
||||||
|
@ -1242,7 +1234,7 @@
|
||||||
"newtUpdateAvailable": "更新可用",
|
"newtUpdateAvailable": "更新可用",
|
||||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
|
||||||
"domainPickerEnterDomain": "域名",
|
"domainPickerEnterDomain": "域名",
|
||||||
"domainPickerPlaceholder": "example.com",
|
"domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp",
|
||||||
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
|
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
|
||||||
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
|
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
|
||||||
"domainPickerTabAll": "所有",
|
"domainPickerTabAll": "所有",
|
||||||
|
@ -1400,6 +1392,8 @@
|
||||||
"editInternalResourceDialogProtocol": "协议",
|
"editInternalResourceDialogProtocol": "协议",
|
||||||
"editInternalResourceDialogSitePort": "站点端口",
|
"editInternalResourceDialogSitePort": "站点端口",
|
||||||
"editInternalResourceDialogTargetConfiguration": "目标配置",
|
"editInternalResourceDialogTargetConfiguration": "目标配置",
|
||||||
|
"editInternalResourceDialogDestinationIP": "目标IP",
|
||||||
|
"editInternalResourceDialogDestinationPort": "目标端口",
|
||||||
"editInternalResourceDialogCancel": "取消",
|
"editInternalResourceDialogCancel": "取消",
|
||||||
"editInternalResourceDialogSaveResource": "保存资源",
|
"editInternalResourceDialogSaveResource": "保存资源",
|
||||||
"editInternalResourceDialogSuccess": "成功",
|
"editInternalResourceDialogSuccess": "成功",
|
||||||
|
@ -1430,7 +1424,9 @@
|
||||||
"createInternalResourceDialogSitePort": "站点端口",
|
"createInternalResourceDialogSitePort": "站点端口",
|
||||||
"createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。",
|
"createInternalResourceDialogSitePortDescription": "使用此端口在连接到客户端时访问站点上的资源。",
|
||||||
"createInternalResourceDialogTargetConfiguration": "目标配置",
|
"createInternalResourceDialogTargetConfiguration": "目标配置",
|
||||||
"createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP或主机名地址。",
|
"createInternalResourceDialogDestinationIP": "目标IP",
|
||||||
|
"createInternalResourceDialogDestinationIPDescription": "站点网络上资源的IP地址。",
|
||||||
|
"createInternalResourceDialogDestinationPort": "目标端口",
|
||||||
"createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。",
|
"createInternalResourceDialogDestinationPortDescription": "资源在目标IP上可访问的端口。",
|
||||||
"createInternalResourceDialogCancel": "取消",
|
"createInternalResourceDialogCancel": "取消",
|
||||||
"createInternalResourceDialogCreateResource": "创建资源",
|
"createInternalResourceDialogCreateResource": "创建资源",
|
||||||
|
@ -1500,24 +1496,5 @@
|
||||||
"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": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.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'",
|
||||||
"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": "4.1.3",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@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.3",
|
"@react-email/components": "0.5.0",
|
||||||
"@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.2",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@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.12.2",
|
"axios": "1.11.0",
|
||||||
"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.5",
|
"drizzle-orm": "0.44.4",
|
||||||
"eslint": "9.35.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "15.4.6",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "8.1.0",
|
"express-rate-limit": "8.0.1",
|
||||||
"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,29 +81,30 @@
|
||||||
"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.544.0",
|
"lucide-react": "0.539.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.3",
|
"next": "15.4.6",
|
||||||
"next-intl": "^4.3.9",
|
"next-intl": "^4.3.4",
|
||||||
"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.6",
|
"nodemailer": "7.0.5",
|
||||||
"npm": "^11.6.0",
|
"npm": "^11.5.2",
|
||||||
"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.7.0",
|
"react-easy-sort": "^1.6.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.8",
|
"tw-animate-css": "^1.3.7",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^11.1.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",
|
||||||
|
@ -113,9 +114,9 @@
|
||||||
"zod-validation-error": "3.5.2"
|
"zod-validation-error": "3.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.49.1",
|
"@dotenvx/dotenvx": "1.49.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.13",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@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",
|
||||||
|
@ -125,25 +126,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.5.2",
|
"@types/node": "^24",
|
||||||
"@types/nodemailer": "7.0.1",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/pg": "8.15.5",
|
"@types/pg": "8.15.5",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.0",
|
||||||
"@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.10",
|
"esbuild": "0.25.9",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.2.11",
|
"react-email": "4.2.8",
|
||||||
"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.44.0"
|
"typescript-eslint": "^8.40.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
|
@ -101,9 +101,7 @@ 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, resources, siteResources } from "@server/db";
|
import { db } 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,44 +34,6 @@ 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,4 +50,3 @@ 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,7 +71,6 @@ 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"),
|
||||||
|
@ -96,7 +95,6 @@ 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", {
|
||||||
|
@ -115,9 +113,7 @@ 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", {
|
||||||
|
@ -131,8 +127,7 @@ 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
|
||||||
|
@ -143,7 +138,6 @@ 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(),
|
||||||
|
@ -218,8 +212,7 @@ 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", {
|
||||||
|
@ -465,7 +458,6 @@ 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,7 +18,6 @@ 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,7 +77,6 @@ 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"),
|
||||||
|
@ -108,7 +107,6 @@ 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", {
|
||||||
|
@ -127,9 +125,7 @@ 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", {
|
||||||
|
@ -143,28 +139,23 @@ 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", {
|
export const siteResources = sqliteTable("siteResources", { // this is for the clients
|
||||||
// this is for the clients
|
siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }),
|
||||||
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", {
|
||||||
|
@ -268,9 +259,7 @@ 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" })
|
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false),
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
endpoint: text("endpoint")
|
endpoint: text("endpoint")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -328,10 +317,7 @@ 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", {
|
||||||
|
@ -608,7 +594,6 @@ 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,5 +1,6 @@
|
||||||
#! /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";
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
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,
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
|
@ -1,53 +0,0 @@
|
||||||
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"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
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));
|
|
|
@ -1,109 +0,0 @@
|
||||||
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));
|
|
|
@ -1,885 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,366 +0,0 @@
|
||||||
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.10.1";
|
export const APP_VERSION = "1.9.0";
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
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,8 +30,7 @@ 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,7 +15,6 @@ 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;
|
||||||
|
@ -404,11 +403,27 @@ export class TraefikConfigManager {
|
||||||
[exitNode] = await db.select().from(exitNodes).limit(1);
|
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||||
}
|
}
|
||||||
if (exitNode) {
|
if (exitNode) {
|
||||||
await sendToExitNode(exitNode, {
|
try {
|
||||||
localPath: "/update-local-snis",
|
await axios.post(
|
||||||
method: "POST",
|
`${exitNode.reachableAt}/update-local-snis`,
|
||||||
data: { fullDomains: Array.from(domains) }
|
{ 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,40 +129,6 @@ 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,11 +19,6 @@ 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(
|
||||||
|
@ -37,6 +32,11 @@ 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,12 +343,6 @@ 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,
|
||||||
|
@ -588,14 +582,6 @@ 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(
|
||||||
|
@ -946,7 +932,7 @@ authRouter.post(
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 15,
|
max: 15,
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
`requestEmailVerificationCode:${req.user?.email || ipKeyGenerator(req.ip || "")}`,
|
`requestEmailVerificationCode:${req.body.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, idpOidcConfig } from "@server/db";
|
import { db } 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 { eq, sql } from "drizzle-orm";
|
import { 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,21 +33,23 @@ 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,
|
||||||
variant: idpOidcConfig.variant,
|
orgCount: sql<number>`count(${idpOrg.orgId})`
|
||||||
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}`)
|
||||||
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
.groupBy(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: Awaited<ReturnType<typeof query>>;
|
idps: Array<{
|
||||||
|
idpId: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
orgCount: number;
|
||||||
|
}>;
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|
|
@ -24,8 +24,7 @@ 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";
|
||||||
|
@ -470,21 +469,6 @@ 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,
|
||||||
|
@ -644,10 +628,3 @@ authenticated.post(
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||||
client.updateClient
|
client.updateClient
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/blueprint",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
|
|
||||||
org.applyBlueprint
|
|
||||||
);
|
|
|
@ -1,73 +0,0 @@
|
||||||
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,7 +10,6 @@ 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;
|
||||||
|
@ -77,15 +76,6 @@ 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,7 +2,6 @@ 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;
|
||||||
|
@ -58,15 +57,4 @@ 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
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,4 +5,3 @@ export * from "./handleReceiveBandwidthMessage";
|
||||||
export * from "./handleGetConfigMessage";
|
export * from "./handleGetConfigMessage";
|
||||||
export * from "./handleSocketMessages";
|
export * from "./handleSocketMessages";
|
||||||
export * from "./handleNewtPingRequestMessage";
|
export * from "./handleNewtPingRequestMessage";
|
||||||
export * from "./handleApplyBlueprintMessage";
|
|
|
@ -1,127 +0,0 @@
|
||||||
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,4 +7,3 @@ 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,8 +21,6 @@ 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({
|
||||||
|
@ -195,21 +193,76 @@ async function createHttpResource(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, domainId } = parsedBody.data;
|
const { name, domainId } = parsedBody.data;
|
||||||
const subdomain = parsedBody.data.subdomain;
|
let subdomain = parsedBody.data.subdomain;
|
||||||
|
|
||||||
// Validate domain and construct full domain
|
const [domainRes] = await db
|
||||||
const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain);
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.where(eq(domains.domainId, domainId))
|
||||||
|
.leftJoin(
|
||||||
|
orgDomains,
|
||||||
|
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
||||||
|
);
|
||||||
|
|
||||||
if (!domainResult.success) {
|
if (!domainRes || !domainRes.domains) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.NOT_FOUND,
|
||||||
domainResult.error
|
`Domain with ID ${domainId} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
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(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Domain with ID ${domainRes.domains.domainId} is not verified`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullDomain = "";
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
@ -230,18 +283,15 @@ 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: finalSubdomain,
|
subdomain,
|
||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
ssl: true
|
ssl: true
|
||||||
|
@ -341,13 +391,10 @@ 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,72 +2,32 @@ 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, and } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { 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()
|
||||||
.optional()
|
.transform(Number)
|
||||||
.transform(stoi)
|
.pipe(z.number().int().positive())
|
||||||
.pipe(z.number().int().positive().optional())
|
|
||||||
.optional(),
|
|
||||||
niceId: z.string().optional(),
|
|
||||||
orgId: z.string().optional()
|
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
async function query(resourceId?: number, niceId?: string, orgId?: string) {
|
export type GetResourceResponse = Resource;
|
||||||
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 by resourceId.",
|
description: "Get a resource.",
|
||||||
tags: [OpenAPITags.Resource],
|
tags: [OpenAPITags.Resource],
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: getResourceSchema
|
||||||
resourceId: z.number()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
responses: {}
|
responses: {}
|
||||||
});
|
});
|
||||||
|
@ -88,18 +48,29 @@ export async function getResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resourceId, niceId, orgId } = parsedParams.data;
|
const { resourceId } = parsedParams.data;
|
||||||
|
|
||||||
const resource = await query(resourceId, niceId, orgId);
|
const [resp] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const resource = resp;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource with ID ${resourceId} not found`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response<GetResourceResponse>(res, {
|
return response(res, {
|
||||||
data: resource,
|
data: {
|
||||||
|
...resource
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Resource retrieved successfully",
|
message: "Resource retrieved successfully",
|
||||||
|
|
|
@ -32,7 +32,6 @@ 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(
|
||||||
|
@ -89,8 +88,7 @@ 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,7 +16,6 @@ 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({
|
||||||
|
@ -55,8 +54,7 @@ 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,8 +20,6 @@ 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({
|
||||||
|
@ -46,8 +44,7 @@ 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, {
|
||||||
|
@ -85,18 +82,6 @@ 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;
|
||||||
|
@ -245,19 +230,78 @@ async function updateHttpResource(
|
||||||
if (updateData.domainId) {
|
if (updateData.domainId) {
|
||||||
const domainId = updateData.domainId;
|
const domainId = updateData.domainId;
|
||||||
|
|
||||||
// Validate domain and construct full domain
|
const [domainRes] = await db
|
||||||
const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain);
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.where(eq(domains.domainId, domainId))
|
||||||
|
.leftJoin(
|
||||||
|
orgDomains,
|
||||||
|
and(
|
||||||
|
eq(orgDomains.orgId, resource.orgId),
|
||||||
|
eq(orgDomains.domainId, domainId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (!domainResult.success) {
|
if (!domainRes || !domainRes.domains) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.NOT_FOUND,
|
||||||
domainResult.error
|
`Domain with ID ${updateData.domainId} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
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(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Domain with ID ${updateData.domainId} is not verified`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullDomain = "";
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
@ -288,8 +332,9 @@ async function updateHttpResource(
|
||||||
.where(eq(resources.resourceId, resource.resourceId));
|
.where(eq(resources.resourceId, resource.resourceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the subdomain in the update data
|
if (fullDomain === domainRes.domains.baseDomain) {
|
||||||
updateData.subdomain = finalSubdomain;
|
updateData.subdomain = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: "Site defaults chosen successfully",
|
message: "Organization retrieved successfully",
|
||||||
status: HttpCode.OK
|
status: HttpCode.OK
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -10,7 +10,6 @@ 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({
|
||||||
|
@ -122,14 +121,11 @@ 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,72 +12,21 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const getSiteResourceParamsSchema = z
|
const getSiteResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
siteResourceId: z
|
siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||||
.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();
|
||||||
|
|
||||||
async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) {
|
export type GetSiteResourceResponse = SiteResource;
|
||||||
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 by siteResourceId.",
|
description: "Get a specific site resource.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: getSiteResourceParamsSchema
|
||||||
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: {}
|
||||||
});
|
});
|
||||||
|
@ -98,10 +47,18 @@ export async function getSiteResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteResourceId, siteId, niceId, orgId } = parsedParams.data;
|
const { siteResourceId, siteId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
// Get the site resource
|
// Get the site resource
|
||||||
const siteResource = await query(siteResourceId, siteId, niceId, orgId);
|
const [siteResource] = await db
|
||||||
|
.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().optional(),
|
destinationIp: z.string().ip().optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
|
@ -30,9 +30,7 @@ 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();
|
||||||
|
|
||||||
|
@ -163,7 +161,7 @@ export async function createTarget(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
const { internalPort, targetIps } = await pickPort(site.siteId!);
|
||||||
|
|
||||||
if (!internalPort) {
|
if (!internalPort) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { db, Transaction } from "@server/db";
|
import { db } 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, trx: Transaction | typeof db): Promise<{
|
export async function pickPort(siteId: number): Promise<{
|
||||||
internalPort: number;
|
internalPort: number;
|
||||||
targetIps: string[];
|
targetIps: string[];
|
||||||
}> {
|
}> {
|
||||||
|
@ -12,7 +12,7 @@ export async function pickPort(siteId: number, trx: Transaction | typeof db): Pr
|
||||||
const targetIps: string[] = [];
|
const targetIps: string[] = [];
|
||||||
const targetInternalPorts: number[] = [];
|
const targetInternalPorts: number[] = [];
|
||||||
|
|
||||||
const targetsRes = await trx
|
const targetsRes = await db
|
||||||
.select()
|
.select()
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.where(eq(targets.siteId, siteId));
|
.where(eq(targets.siteId, siteId));
|
||||||
|
|
|
@ -44,9 +44,7 @@ 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,9 +26,7 @@ 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, {
|
||||||
|
@ -155,7 +153,7 @@ export async function updateTarget(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(site.siteId!, db);
|
const { internalPort, targetIps } = await pickPort(site.siteId!);
|
||||||
|
|
||||||
if (!internalPort) {
|
if (!internalPort) {
|
||||||
return next(
|
return next(
|
||||||
|
|
|
@ -54,8 +54,7 @@ export async function traefikConfigProvider(
|
||||||
config.getRawConfig().traefik.site_types
|
config.getRawConfig().traefik.site_types
|
||||||
);
|
);
|
||||||
|
|
||||||
if (traefikConfig?.http?.middlewares) {
|
if (traefikConfig?.http?.middlewares) { // BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING
|
||||||
// BECAUSE SOMETIMES THE CONFIG CAN BE EMPTY IF THERE IS NOTHING
|
|
||||||
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
traefikConfig.http.middlewares[badgerMiddlewareName] = {
|
||||||
plugin: {
|
plugin: {
|
||||||
[badgerMiddlewareName]: {
|
[badgerMiddlewareName]: {
|
||||||
|
@ -105,112 +104,106 @@ export async function getTraefikConfig(
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get resources with their targets and sites in a single optimized query
|
// Get all resources with related data
|
||||||
// Start from sites on this exit node, then join to targets and resources
|
const allResources = await db.transaction(async (tx) => {
|
||||||
const resourcesWithTargetsAndSites = await db
|
// Get resources with their targets and sites in a single optimized query
|
||||||
.select({
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
// Resource fields
|
const resourcesWithTargetsAndSites = await tx
|
||||||
resourceId: resources.resourceId,
|
.select({
|
||||||
fullDomain: resources.fullDomain,
|
// Resource fields
|
||||||
ssl: resources.ssl,
|
resourceId: resources.resourceId,
|
||||||
http: resources.http,
|
fullDomain: resources.fullDomain,
|
||||||
proxyPort: resources.proxyPort,
|
ssl: resources.ssl,
|
||||||
protocol: resources.protocol,
|
http: resources.http,
|
||||||
subdomain: resources.subdomain,
|
proxyPort: resources.proxyPort,
|
||||||
domainId: resources.domainId,
|
protocol: resources.protocol,
|
||||||
enabled: resources.enabled,
|
subdomain: resources.subdomain,
|
||||||
stickySession: resources.stickySession,
|
domainId: resources.domainId,
|
||||||
tlsServerName: resources.tlsServerName,
|
enabled: resources.enabled,
|
||||||
setHostHeader: resources.setHostHeader,
|
stickySession: resources.stickySession,
|
||||||
enableProxy: resources.enableProxy,
|
tlsServerName: resources.tlsServerName,
|
||||||
headers: resources.headers,
|
setHostHeader: resources.setHostHeader,
|
||||||
// Target fields
|
enableProxy: resources.enableProxy,
|
||||||
targetId: targets.targetId,
|
// Target fields
|
||||||
targetEnabled: targets.enabled,
|
targetId: targets.targetId,
|
||||||
ip: targets.ip,
|
targetEnabled: targets.enabled,
|
||||||
method: targets.method,
|
ip: targets.ip,
|
||||||
port: targets.port,
|
method: targets.method,
|
||||||
internalPort: targets.internalPort,
|
port: targets.port,
|
||||||
path: targets.path,
|
internalPort: targets.internalPort,
|
||||||
pathMatchType: targets.pathMatchType,
|
// Site fields
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Site fields
|
// Group by resource and include targets with their unique site data
|
||||||
siteId: sites.siteId,
|
const resourcesMap = new Map();
|
||||||
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
|
resourcesWithTargetsAndSites.forEach((row) => {
|
||||||
const resourcesMap = new Map();
|
const resourceId = row.resourceId;
|
||||||
|
|
||||||
resourcesWithTargetsAndSites.forEach((row) => {
|
if (!resourcesMap.has(resourceId)) {
|
||||||
const resourceId = row.resourceId;
|
resourcesMap.set(resourceId, {
|
||||||
const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths
|
resourceId: row.resourceId,
|
||||||
const pathMatchType = row.pathMatchType || "";
|
fullDomain: row.fullDomain,
|
||||||
|
ssl: row.ssl,
|
||||||
// Create a unique key combining resourceId and path+pathMatchType
|
http: row.http,
|
||||||
const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-");
|
proxyPort: row.proxyPort,
|
||||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
protocol: row.protocol,
|
||||||
|
subdomain: row.subdomain,
|
||||||
if (!resourcesMap.has(mapKey)) {
|
domainId: row.domainId,
|
||||||
resourcesMap.set(mapKey, {
|
enabled: row.enabled,
|
||||||
resourceId: row.resourceId,
|
stickySession: row.stickySession,
|
||||||
fullDomain: row.fullDomain,
|
tlsServerName: row.tlsServerName,
|
||||||
ssl: row.ssl,
|
setHostHeader: row.setHostHeader,
|
||||||
http: row.http,
|
enableProxy: row.enableProxy,
|
||||||
proxyPort: row.proxyPort,
|
targets: []
|
||||||
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: [],
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add target with its associated site data
|
||||||
|
resourcesMap.get(resourceId).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
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Array.from(resourcesMap.values());
|
||||||
});
|
});
|
||||||
|
|
||||||
// make sure we have at least one resource
|
if (!allResources.length) {
|
||||||
if (resourcesMap.size === 0) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,15 +219,14 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// get the key and the resource
|
for (const resource of allResources) {
|
||||||
for (const [key, resource] of resourcesMap.entries()) {
|
|
||||||
const targets = resource.targets;
|
const targets = resource.targets;
|
||||||
|
|
||||||
const routerName = `${key}-router`;
|
const routerName = `${resource.resourceId}-router`;
|
||||||
const serviceName = `${key}-service`;
|
const serviceName = `${resource.resourceId}-service`;
|
||||||
const fullDomain = `${resource.fullDomain}`;
|
const fullDomain = `${resource.fullDomain}`;
|
||||||
const transportName = `${key}-transport`;
|
const transportName = `${resource.resourceId}-transport`;
|
||||||
const headersMiddlewareName = `${key}-headers-middleware`;
|
const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`;
|
||||||
|
|
||||||
if (!resource.enabled) {
|
if (!resource.enabled) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -246,6 +238,9 @@ export async function getTraefikConfig(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resource.fullDomain) {
|
if (!resource.fullDomain) {
|
||||||
|
logger.error(
|
||||||
|
`Resource ${resource.resourceId} has no fullDomain`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,68 +296,16 @@ 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: routerMiddlewares,
|
middlewares: [badgerMiddlewareName, ...additionalMiddlewares],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: rule,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
priority: priority,
|
priority: 100,
|
||||||
...(resource.ssl ? { tls } : {})
|
...(resource.ssl ? { tls } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -373,8 +316,8 @@ export async function getTraefikConfig(
|
||||||
],
|
],
|
||||||
middlewares: [redirectHttpsMiddlewareName],
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: rule,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
priority: priority
|
priority: 100
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,64 +334,55 @@ export async function getTraefikConfig(
|
||||||
targets as TargetWithSite[]
|
targets as TargetWithSite[]
|
||||||
).some((target: TargetWithSite) => target.site.online);
|
).some((target: TargetWithSite) => target.site.online);
|
||||||
|
|
||||||
return (
|
return (targets as TargetWithSite[])
|
||||||
(targets as TargetWithSite[])
|
.filter((target: TargetWithSite) => {
|
||||||
.filter((target: TargetWithSite) => {
|
if (!target.enabled) {
|
||||||
if (!target.enabled) {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 any sites are online, exclude offline sites
|
if (
|
||||||
if (anySitesOnline && !target.site.online) {
|
!target.internalPort ||
|
||||||
|
!target.method ||
|
||||||
|
!target.site.subnet
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (
|
return true;
|
||||||
target.site.type === "local" ||
|
})
|
||||||
target.site.type === "wireguard"
|
.map((target: TargetWithSite) => {
|
||||||
) {
|
if (
|
||||||
if (
|
target.site.type === "local" ||
|
||||||
!target.ip ||
|
target.site.type === "wireguard"
|
||||||
!target.port ||
|
) {
|
||||||
!target.method
|
return {
|
||||||
) {
|
url: `${target.method}://${target.ip}:${target.port}`
|
||||||
return false;
|
};
|
||||||
}
|
} else if (target.site.type === "newt") {
|
||||||
} else if (target.site.type === "newt") {
|
const ip =
|
||||||
if (
|
target.site.subnet!.split("/")[0];
|
||||||
!target.internalPort ||
|
return {
|
||||||
!target.method ||
|
url: `${target.method}://${ip}:${target.internalPort}`
|
||||||
!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
|
||||||
? {
|
? {
|
||||||
|
@ -479,6 +413,27 @@ 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) {
|
||||||
|
@ -574,13 +529,3 @@ 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,14 +84,7 @@ export async function createOrgUser(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const {
|
const { username, email, name, type, idpId, roleId } = parsedBody.data;
|
||||||
username,
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
idpId,
|
|
||||||
roleId
|
|
||||||
} = parsedBody.data;
|
|
||||||
|
|
||||||
const [role] = await db
|
const [role] = await db
|
||||||
.select()
|
.select()
|
||||||
|
@ -148,12 +141,7 @@ export async function createOrgUser(
|
||||||
const [existingUser] = await trx
|
const [existingUser] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(
|
.where(eq(users.username, username));
|
||||||
and(
|
|
||||||
eq(users.username, username),
|
|
||||||
eq(users.idpId, idpId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
const [existingOrgUser] = await trx
|
const [existingOrgUser] = await trx
|
||||||
|
@ -180,8 +168,7 @@ 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 {
|
||||||
|
@ -197,7 +184,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();
|
||||||
|
|
||||||
|
@ -206,8 +193,7 @@ export async function createOrgUser(
|
||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
userId: newUser.userId,
|
userId: newUser.userId,
|
||||||
roleId: role.roleId,
|
roleId: role.roleId
|
||||||
autoProvisioned: false
|
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
@ -218,6 +204,7 @@ 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, idp, idpOidcConfig } from "@server/db";
|
import { db } 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,18 +25,10 @@ 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,4 +14,3 @@ 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, idpOidcConfig } from "@server/db";
|
import { db } 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,15 +61,12 @@ 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)
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
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,8 +4,7 @@ import {
|
||||||
handleGetConfigMessage,
|
handleGetConfigMessage,
|
||||||
handleDockerStatusMessage,
|
handleDockerStatusMessage,
|
||||||
handleDockerContainersMessage,
|
handleDockerContainersMessage,
|
||||||
handleNewtPingRequestMessage,
|
handleNewtPingRequestMessage
|
||||||
handleApplyBlueprintMessage
|
|
||||||
} from "../newt";
|
} from "../newt";
|
||||||
import {
|
import {
|
||||||
handleOlmRegisterMessage,
|
handleOlmRegisterMessage,
|
||||||
|
@ -24,8 +23,7 @@ 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,7 +9,6 @@ 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
|
||||||
|
@ -19,8 +18,7 @@ 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,8 +26,6 @@ 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
|
||||||
|
@ -53,8 +51,6 @@ 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;
|
||||||
|
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
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, "");
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
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, "");
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
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 "../../components/OrganizationLandingCard";
|
import OrganizationLandingCard from "./OrganizationLandingCard";
|
||||||
import MemberResourcesPortal from "../../components/MemberResourcesPortal";
|
import MemberResourcesPortal from "./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";
|
||||||
|
|
|
@ -9,10 +9,10 @@ import {
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
import { InvitationsDataTable } from "@app/components/InvitationsDataTable";
|
import { InvitationsDataTable } from "./InvitationsDataTable";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import RegenerateInvitationForm from "@app/components/RegenerateInvitationForm";
|
import RegenerateInvitationForm from "./RegenerateInvitationForm";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
|
@ -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 "../../../../../components/InvitationsTable";
|
import InvitationsTable, { InvitationRow } from "./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 "../../../../../components/AccessPageHeaderAndNav";
|
import AccessPageHeaderAndNav from "../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,6 +1,7 @@
|
||||||
interface AccessLayoutProps {
|
interface AccessLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
|
resourceId: number | string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { RoleRow } from "@app/components/RolesTable";
|
import { RoleRow } from "./RolesTable";
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
@ -13,10 +13,10 @@ import { useState } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { RolesDataTable } from "@app/components/RolesDataTable";
|
import { RolesDataTable } from "./RolesDataTable";
|
||||||
import { Role } from "@server/db";
|
import { Role } from "@server/db";
|
||||||
import CreateRoleForm from "@app/components/CreateRoleForm";
|
import CreateRoleForm from "./CreateRoleForm";
|
||||||
import DeleteRoleForm from "@app/components/DeleteRoleForm";
|
import DeleteRoleForm from "./DeleteRoleForm";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
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