mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-17 19:16:32 +02:00
Compare commits
110 Commits
776f3ea59f
...
1.15.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c4a7b690 | |||
| b735e7c34d | |||
| 5f85c3b3b8 | |||
| 5d9cb9fa21 | |||
| 643d56958d | |||
| f378d6f040 | |||
| bb57794388 | |||
| a9ca49b8a2 | |||
| c1b473294e | |||
| e3e4bdfe09 | |||
| bfbeace2e2 | |||
| efcf46ce8a | |||
| 2085715965 | |||
| d227db7b7b | |||
| 2af67ad355 | |||
| f100854423 | |||
| 92331d7a33 | |||
| 9a5bcb9099 | |||
| 8eb6bb2a95 | |||
| 2aa65ccab3 | |||
| be1577a3e7 | |||
| c8e1b3bf29 | |||
| e17b986628 | |||
| 5f19918ca0 | |||
| 2959ad0e70 | |||
| a76eec7bb7 | |||
| 068b2a0dcd | |||
| 316b7e5653 | |||
| 00fc1da33c | |||
| 9ef93df54f | |||
| fd9fdf6399 | |||
| 8fa1701e06 | |||
| 4abe83f8a9 | |||
| 0a7564acb6 | |||
| db0f7cfbae | |||
| 1724885371 | |||
| a97e9ea8b1 | |||
| 9d30e97526 | |||
| b91330a27a | |||
| 744bc9ebe9 | |||
| 89ed9e6d7f | |||
| b007e7f54a | |||
| 6651a6df42 | |||
| 3f29b165aa | |||
| b13b91face | |||
| 63c14fe2d5 | |||
| 14e74ed02d | |||
| 7e30750618 | |||
| 4d1dd16be5 | |||
| fa49cf5eba | |||
| 26b39fc1c6 | |||
| 0d36e368ea | |||
| 859f265c68 | |||
| 3219f520ba | |||
| 97e27b6caf | |||
| 09da83a72b | |||
| d13b210e2f | |||
| 09fb672718 | |||
| 9797ad0e17 | |||
| 8b3d61ac36 | |||
| 7161c9547a | |||
| 60d4362a87 | |||
| 1836e0c8fc | |||
| d3344aeb34 | |||
| cfeb093fa6 | |||
| a469b3ffcc | |||
| 14b3a3fdd8 | |||
| 94367ce387 | |||
| 5be518aa50 | |||
| d059a8da9e | |||
| 1dcacbef7a | |||
| a25edeccf7 | |||
| 315f73c77d | |||
| 666288fccc | |||
| 0ccf61c2a9 | |||
| c16b1b27a3 | |||
| ed9ba60be6 | |||
| 24d047e3d8 | |||
| 9671079ffb | |||
| 688892523c | |||
| b02c341f62 | |||
| 3e9bcada1e | |||
| 93d4bd6438 | |||
| 5146498b33 | |||
| 72da4f39a8 | |||
| a2b2fb804b | |||
| 3eac80e666 | |||
| 718d2122a4 | |||
| 310c6c90a3 | |||
| 9d80f62d58 | |||
| 77032fc989 | |||
| 64e6086f0c | |||
| fe729ec762 | |||
| d6a829abc2 | |||
| 75005ccf81 | |||
| fd6c600531 | |||
| 6996c2501e | |||
| efbd9bdb56 | |||
| 0d34213647 | |||
| 870b85d71b | |||
| 86ba6b6f86 | |||
| 02be3cd0c4 | |||
| 1b756ef9a0 | |||
| ceda06f9ae | |||
| 068eba015b | |||
| 90f66baf85 | |||
| 5743c0bb72 | |||
| ec10c37468 | |||
| 5ee3e140ed | |||
| 8e1fd4474f |
+28
-10
@@ -29,7 +29,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -264,7 +264,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.24
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ jobs:
|
|||||||
|
|
||||||
# Determine if this is an RC release
|
# Determine if this is an RC release
|
||||||
IS_RC="false"
|
IS_RC="false"
|
||||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
IS_RC="true"
|
IS_RC="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -442,7 +442,7 @@ jobs:
|
|||||||
|
|
||||||
# Determine if this is an RC release
|
# Determine if this is an RC release
|
||||||
IS_RC="false"
|
IS_RC="false"
|
||||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
IS_RC="true"
|
IS_RC="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -482,14 +482,32 @@ jobs:
|
|||||||
echo "==> cosign sign (key) --recursive ${REF}"
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
|
|
||||||
|
# Retry wrapper for verification to handle registry propagation delays
|
||||||
|
retry_verify() {
|
||||||
|
local cmd="$1"
|
||||||
|
local attempts=6
|
||||||
|
local delay=5
|
||||||
|
local i=1
|
||||||
|
until eval "$cmd"; do
|
||||||
|
if [ $i -ge $attempts ]; then
|
||||||
|
echo "Verification failed after $attempts attempts"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
|
||||||
|
sleep $delay
|
||||||
|
i=$((i+1))
|
||||||
|
delay=$((delay*2))
|
||||||
|
# Cap the delay to avoid very long waits
|
||||||
|
if [ $delay -gt 60 ]; then delay=60; fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> cosign verify (public key) ${REF}"
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) ${REF}"
|
echo "==> cosign verify (keyless policy) ${REF}"
|
||||||
cosign verify \
|
retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"
|
||||||
--certificate-oidc-issuer "${issuer}" \
|
|
||||||
--certificate-identity-regexp "${id_regex}" \
|
|
||||||
"${REF}" -o text
|
|
||||||
|
|
||||||
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
done
|
done
|
||||||
@@ -513,7 +531,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v2
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -4,13 +4,13 @@
|
|||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
@@ -35,6 +35,12 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.pangolin.net/careers/join-us">
|
||||||
|
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||||
@@ -74,6 +80,8 @@ Download the Pangolin client for your platform:
|
|||||||
- [Mac](https://pangolin.net/downloads/mac)
|
- [Mac](https://pangolin.net/downloads/mac)
|
||||||
- [Windows](https://pangolin.net/downloads/windows)
|
- [Windows](https://pangolin.net/downloads/windows)
|
||||||
- [Linux](https://pangolin.net/downloads/linux)
|
- [Linux](https://pangolin.net/downloads/linux)
|
||||||
|
- [iOS](https://pangolin.net/downloads/ios)
|
||||||
|
- [Android](https://pangolin.net/downloads/android)
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
|
|||||||
@@ -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://api.pangolin.net/v1/org/test/blueprint'
|
|
||||||
HEADERS = {
|
|
||||||
'accept': '*/*',
|
|
||||||
'Authorization': 'Bearer <your_token_here>',
|
|
||||||
'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,70 +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@pangolin.net
|
|
||||||
# whitelist-users:
|
|
||||||
# - owen@pangolin.net
|
|
||||||
# auto-login-idp: 1
|
|
||||||
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
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||||
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
|
type DeleteClientArgs = {
|
||||||
|
orgId: string;
|
||||||
|
niceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||||
|
command: "delete-client",
|
||||||
|
describe:
|
||||||
|
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("orgId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The organization ID"
|
||||||
|
})
|
||||||
|
.option("niceId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The client niceId (identifier)"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||||
|
try {
|
||||||
|
const { orgId, niceId } = argv;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the client
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
console.error(
|
||||||
|
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = client.clientId;
|
||||||
|
console.log(`Found client with clientId: ${clientId}`);
|
||||||
|
|
||||||
|
// Find all OLMs associated with this client
|
||||||
|
const associatedOlms = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId));
|
||||||
|
|
||||||
|
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||||
|
|
||||||
|
// Delete in a transaction to ensure atomicity
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Delete currentFingerprint entries for the associated OLMs
|
||||||
|
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||||
|
// we have control, even though cascade would handle it
|
||||||
|
let fingerprintCount = 0;
|
||||||
|
if (associatedOlms.length > 0) {
|
||||||
|
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||||
|
const deletedFingerprints = await trx
|
||||||
|
.delete(currentFingerprint)
|
||||||
|
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||||
|
.returning();
|
||||||
|
fingerprintCount = deletedFingerprints.length;
|
||||||
|
}
|
||||||
|
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||||
|
|
||||||
|
// Delete OLMs
|
||||||
|
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||||
|
const deletedOlms = await trx
|
||||||
|
.delete(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||||
|
|
||||||
|
// Delete approvals
|
||||||
|
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedApprovals = await trx
|
||||||
|
.delete(approvals)
|
||||||
|
.where(eq(approvals.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||||
|
|
||||||
|
// Delete userClients
|
||||||
|
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedUserClients = await trx
|
||||||
|
.delete(userClients)
|
||||||
|
.where(eq(userClients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||||
|
|
||||||
|
// Finally, delete the client itself
|
||||||
|
const deletedClients = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nClient deletion completed successfully!");
|
||||||
|
console.log("\nSummary:");
|
||||||
|
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||||
|
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||||
|
console.log(` - Current fingerprints: deleted`);
|
||||||
|
console.log(` - Approvals: deleted`);
|
||||||
|
console.log(` - UserClients: deleted`);
|
||||||
|
console.log(` - Snapshots: preserved (not deleted)`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting client:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
|||||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||||
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -15,5 +16,6 @@ yargs(hideBin(process.argv))
|
|||||||
.command(clearExitNodes)
|
.command(clearExitNodes)
|
||||||
.command(rotateServerSecret)
|
.command(rotateServerSecret)
|
||||||
.command(clearLicenseKeys)
|
.command(clearLicenseKeys)
|
||||||
|
.command(deleteClient)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
+24
-21
@@ -1,27 +1,30 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
# https://docs.pangolin.net/
|
||||||
|
|
||||||
app:
|
|
||||||
dashboard_url: http://localhost:3002
|
|
||||||
log_level: debug
|
|
||||||
|
|
||||||
domains:
|
|
||||||
domain1:
|
|
||||||
base_domain: example.com
|
|
||||||
|
|
||||||
server:
|
|
||||||
secret: my_secret_key
|
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
base_endpoint: example.com
|
start_port: 51820
|
||||||
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
|
|
||||||
orgs:
|
app:
|
||||||
block_size: 24
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
subnet_group: 100.90.137.0/20
|
log_level: "info"
|
||||||
|
telemetry:
|
||||||
|
anonymous_usage: true
|
||||||
|
|
||||||
|
domains:
|
||||||
|
domain1:
|
||||||
|
base_domain: "{{.BaseDomain}}"
|
||||||
|
|
||||||
|
server:
|
||||||
|
secret: "{{.Secret}}"
|
||||||
|
cors:
|
||||||
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
|
credentials: false
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
|
||||||
|
|||||||
@@ -21,9 +21,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
|
||||||
middlewares:
|
middlewares:
|
||||||
@@ -35,7 +34,6 @@ 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:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
@@ -53,3 +51,12 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-transport-v1:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 1
|
||||||
|
pp-transport-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
|
|||||||
@@ -3,32 +3,52 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
|
http:
|
||||||
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
directory: "/var/dynamic"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
watch: true
|
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
moduleName: "github.com/fosrl/badger"
|
moduleName: "github.com/fosrl/badger"
|
||||||
version: "v1.3.0"
|
version: "{{.BadgerVersion}}"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "DEBUG"
|
level: "INFO"
|
||||||
format: "common"
|
format: "common"
|
||||||
maxSize: 100
|
maxSize: 100
|
||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
email: "{{.LetsEncryptEmail}}"
|
||||||
|
storage: "/letsencrypt/acme.json"
|
||||||
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":9443"
|
address: ":443"
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
|
http:
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
|
ping:
|
||||||
|
entryPoint: "web"
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.38.0
|
golang.org/x/term v0.39.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.39.0 // indirect
|
require golang.org/x/sys v0.40.0 // indirect
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
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=
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
|
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
|
||||||
"resourcesTableDestination": "Дестинация",
|
"resourcesTableDestination": "Дестинация",
|
||||||
"resourcesTableAlias": "Псевдоним",
|
"resourcesTableAlias": "Псевдоним",
|
||||||
|
"resourcesTableAliasAddress": "Адрес на псевдоним.",
|
||||||
|
"resourcesTableAliasAddressInfo": "Този адрес е част от подсистемата на организацията. Използва се за разрешаване на псевдонимни записи чрез вътрешно DNS разрешаване.",
|
||||||
"resourcesTableClients": "Клиенти",
|
"resourcesTableClients": "Клиенти",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
|
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
|
||||||
"resourcesTableNoTargets": "Без цели",
|
"resourcesTableNoTargets": "Без цели",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Вход",
|
"logIn": "Вход",
|
||||||
"deviceInformation": "Информация за устройството",
|
"deviceInformation": "Информация за устройството",
|
||||||
"deviceInformationDescription": "Информация за устройството и агента",
|
"deviceInformationDescription": "Информация за устройството и агента",
|
||||||
|
"deviceSecurity": "Защита на устройството.",
|
||||||
|
"deviceSecurityDescription": "Информация за състоянието на защитата на устройството.",
|
||||||
"platform": "Платформа",
|
"platform": "Платформа",
|
||||||
"macosVersion": "Версия на macOS",
|
"macosVersion": "Версия на macOS",
|
||||||
"windowsVersion": "Версия на Windows",
|
"windowsVersion": "Версия на Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Име на хост",
|
"hostname": "Име на хост",
|
||||||
"firstSeen": "Видян за първи път",
|
"firstSeen": "Видян за първи път",
|
||||||
"lastSeen": "Последно видян",
|
"lastSeen": "Последно видян",
|
||||||
|
"biometricsEnabled": "Активирани биометрични данни.",
|
||||||
|
"diskEncrypted": "Криптиран диск.",
|
||||||
|
"firewallEnabled": "Активирана защитна стена.",
|
||||||
|
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
|
||||||
|
"tpmAvailable": "TPM е на разположение.",
|
||||||
|
"macosSipEnabled": "Protection на системната цялост (SIP).",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper.",
|
||||||
|
"macosFirewallStealthMode": "Скрит режим на защитната стена.",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor.",
|
||||||
|
"linuxSELinuxEnabled": "SELinux.",
|
||||||
"deviceSettingsDescription": "Разгледайте информация и настройки на устройството",
|
"deviceSettingsDescription": "Разгледайте информация и настройки на устройството",
|
||||||
"devicePendingApprovalDescription": "Това устройство чака одобрение",
|
"devicePendingApprovalDescription": "Това устройство чака одобрение",
|
||||||
"deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.",
|
"deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
|
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
|
||||||
"resourcesTableDestination": "Místo určení",
|
"resourcesTableDestination": "Místo určení",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Adresa aliasu",
|
||||||
|
"resourcesTableAliasAddressInfo": "Tato adresa je součástí subsítě veřejných služeb organizace. Používá se k řešení záznamů aliasů pomocí interního rozlišení DNS.",
|
||||||
"resourcesTableClients": "Klienti",
|
"resourcesTableClients": "Klienti",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
|
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
|
||||||
"resourcesTableNoTargets": "Žádné cíle",
|
"resourcesTableNoTargets": "Žádné cíle",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Přihlásit se",
|
"logIn": "Přihlásit se",
|
||||||
"deviceInformation": "Informace o zařízení",
|
"deviceInformation": "Informace o zařízení",
|
||||||
"deviceInformationDescription": "Informace o zařízení a agentovi",
|
"deviceInformationDescription": "Informace o zařízení a agentovi",
|
||||||
|
"deviceSecurity": "Zabezpečení zařízení",
|
||||||
|
"deviceSecurityDescription": "Informace o bezpečnostní pozici zařízení",
|
||||||
"platform": "Platforma",
|
"platform": "Platforma",
|
||||||
"macosVersion": "macOS verze",
|
"macosVersion": "macOS verze",
|
||||||
"windowsVersion": "Verze Windows",
|
"windowsVersion": "Verze Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "První vidění",
|
"firstSeen": "První vidění",
|
||||||
"lastSeen": "Naposledy viděno",
|
"lastSeen": "Naposledy viděno",
|
||||||
|
"biometricsEnabled": "Biometrie povolena",
|
||||||
|
"diskEncrypted": "Šifrovaný disk",
|
||||||
|
"firewallEnabled": "Firewall povolen",
|
||||||
|
"autoUpdatesEnabled": "Automatické aktualizace povoleny",
|
||||||
|
"tpmAvailable": "TPM k dispozici",
|
||||||
|
"macosSipEnabled": "Ochrana systémové integrity (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Režim neviditelnosti firewallu",
|
||||||
|
"linuxAppArmorEnabled": "Pancíř aplikace",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Zobrazit informace o zařízení a nastavení",
|
"deviceSettingsDescription": "Zobrazit informace o zařízení a nastavení",
|
||||||
"devicePendingApprovalDescription": "Toto zařízení čeká na schválení",
|
"devicePendingApprovalDescription": "Toto zařízení čeká na schválení",
|
||||||
"deviceBlockedDescription": "Toto zařízení je momentálně blokováno. Nebude se moci připojit k žádným zdrojům, dokud nebude odblokováno.",
|
"deviceBlockedDescription": "Toto zařízení je momentálně blokováno. Nebude se moci připojit k žádným zdrojům, dokud nebude odblokováno.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
|
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
|
||||||
"resourcesTableDestination": "Ziel",
|
"resourcesTableDestination": "Ziel",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias-Adresse",
|
||||||
|
"resourcesTableAliasAddressInfo": "Diese Adresse ist Teil des Utility-Subnetzes der Organisation. Sie wird verwendet, um Alias-Einträge mit interner DNS-Auflösung aufzulösen.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
||||||
"resourcesTableNoTargets": "Keine Ziele",
|
"resourcesTableNoTargets": "Keine Ziele",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Anmelden",
|
"logIn": "Anmelden",
|
||||||
"deviceInformation": "Geräteinformationen",
|
"deviceInformation": "Geräteinformationen",
|
||||||
"deviceInformationDescription": "Informationen über das Gerät und den Agent",
|
"deviceInformationDescription": "Informationen über das Gerät und den Agent",
|
||||||
|
"deviceSecurity": "Gerätesicherheit",
|
||||||
|
"deviceSecurityDescription": "Informationen zur Gerätesicherheit",
|
||||||
"platform": "Plattform",
|
"platform": "Plattform",
|
||||||
"macosVersion": "macOS-Version",
|
"macosVersion": "macOS-Version",
|
||||||
"windowsVersion": "Windows-Version",
|
"windowsVersion": "Windows-Version",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Erster Blick",
|
"firstSeen": "Erster Blick",
|
||||||
"lastSeen": "Zuletzt gesehen",
|
"lastSeen": "Zuletzt gesehen",
|
||||||
|
"biometricsEnabled": "Biometrie aktiviert",
|
||||||
|
"diskEncrypted": "Festplatte verschlüsselt",
|
||||||
|
"firewallEnabled": "Firewall aktiviert",
|
||||||
|
"autoUpdatesEnabled": "Automatische Updates aktiviert",
|
||||||
|
"tpmAvailable": "TPM verfügbar",
|
||||||
|
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Firewall Stealth-Modus",
|
||||||
|
"linuxAppArmorEnabled": "AppRüstung",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
|
"deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
|
||||||
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe",
|
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe",
|
||||||
"deviceBlockedDescription": "Dieses Gerät ist derzeit gesperrt. Es kann keine Verbindung zu anderen Ressourcen herstellen, es sei denn, es entsperrt.",
|
"deviceBlockedDescription": "Dieses Gerät ist derzeit gesperrt. Es kann keine Verbindung zu anderen Ressourcen herstellen, es sei denn, es entsperrt.",
|
||||||
|
|||||||
+3
-1
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||||
"resourcesTableDestination": "Destination",
|
"resourcesTableDestination": "Destination",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias Address",
|
||||||
|
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
"resourcesTableNoTargets": "No targets",
|
"resourcesTableNoTargets": "No targets",
|
||||||
@@ -2508,7 +2510,7 @@
|
|||||||
"firewallEnabled": "Firewall Enabled",
|
"firewallEnabled": "Firewall Enabled",
|
||||||
"autoUpdatesEnabled": "Auto Updates Enabled",
|
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||||
"tpmAvailable": "TPM Available",
|
"tpmAvailable": "TPM Available",
|
||||||
"windowsDefenderEnabled": "Windows Defender Enabled",
|
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||||
"macosSipEnabled": "System Integrity Protection (SIP)",
|
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||||
"macosGatekeeperEnabled": "Gatekeeper",
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
|
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
|
||||||
"resourcesTableDestination": "Destino",
|
"resourcesTableDestination": "Destino",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Dirección del alias",
|
||||||
|
"resourcesTableAliasAddressInfo": "Esta dirección es parte de la subred de utilidad de la organización. Se utiliza para resolver registros de alias usando resolución DNS interna.",
|
||||||
"resourcesTableClients": "Clientes",
|
"resourcesTableClients": "Clientes",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
||||||
"resourcesTableNoTargets": "Sin objetivos",
|
"resourcesTableNoTargets": "Sin objetivos",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Iniciar sesión",
|
"logIn": "Iniciar sesión",
|
||||||
"deviceInformation": "Información del dispositivo",
|
"deviceInformation": "Información del dispositivo",
|
||||||
"deviceInformationDescription": "Información sobre el dispositivo y el agente",
|
"deviceInformationDescription": "Información sobre el dispositivo y el agente",
|
||||||
|
"deviceSecurity": "Seguridad del dispositivo",
|
||||||
|
"deviceSecurityDescription": "Información de postura de seguridad del dispositivo",
|
||||||
"platform": "Plataforma",
|
"platform": "Plataforma",
|
||||||
"macosVersion": "versión macOS",
|
"macosVersion": "versión macOS",
|
||||||
"windowsVersion": "Versión de Windows",
|
"windowsVersion": "Versión de Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Primer detectado",
|
"firstSeen": "Primer detectado",
|
||||||
"lastSeen": "Último Visto",
|
"lastSeen": "Último Visto",
|
||||||
|
"biometricsEnabled": "Biometría habilitada",
|
||||||
|
"diskEncrypted": "Disco cifrado",
|
||||||
|
"firewallEnabled": "Cortafuegos activado",
|
||||||
|
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
|
||||||
|
"tpmAvailable": "TPM disponible",
|
||||||
|
"macosSipEnabled": "Protección de integridad del sistema (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Modo Sigilo Firewall",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Ver información y ajustes del dispositivo",
|
"deviceSettingsDescription": "Ver información y ajustes del dispositivo",
|
||||||
"devicePendingApprovalDescription": "Este dispositivo está esperando su aprobación",
|
"devicePendingApprovalDescription": "Este dispositivo está esperando su aprobación",
|
||||||
"deviceBlockedDescription": "Este dispositivo está actualmente bloqueado. No podrá conectarse a ningún recurso a menos que sea desbloqueado.",
|
"deviceBlockedDescription": "Este dispositivo está actualmente bloqueado. No podrá conectarse a ningún recurso a menos que sea desbloqueado.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
|
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
|
||||||
"resourcesTableDestination": "Destination",
|
"resourcesTableDestination": "Destination",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Adresse de l'alias",
|
||||||
|
"resourcesTableAliasAddressInfo": "Cette adresse fait partie du sous-réseau utilitaire de l'organisation. Elle est utilisée pour résoudre les enregistrements d'alias en utilisant une résolution DNS interne.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
||||||
"resourcesTableNoTargets": "Aucune cible",
|
"resourcesTableNoTargets": "Aucune cible",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Se connecter",
|
"logIn": "Se connecter",
|
||||||
"deviceInformation": "Informations sur l'appareil",
|
"deviceInformation": "Informations sur l'appareil",
|
||||||
"deviceInformationDescription": "Informations sur l'appareil et l'agent",
|
"deviceInformationDescription": "Informations sur l'appareil et l'agent",
|
||||||
|
"deviceSecurity": "Sécurité de l'appareil",
|
||||||
|
"deviceSecurityDescription": "Informations sur la posture de sécurité de l'appareil",
|
||||||
"platform": "Plateforme",
|
"platform": "Plateforme",
|
||||||
"macosVersion": "Version macOS",
|
"macosVersion": "Version macOS",
|
||||||
"windowsVersion": "Version de Windows",
|
"windowsVersion": "Version de Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Première vue",
|
"firstSeen": "Première vue",
|
||||||
"lastSeen": "Dernière vue",
|
"lastSeen": "Dernière vue",
|
||||||
|
"biometricsEnabled": "biométrique activée",
|
||||||
|
"diskEncrypted": "Disque chiffré",
|
||||||
|
"firewallEnabled": "Pare-feu activé",
|
||||||
|
"autoUpdatesEnabled": "Mises à jour automatiques activées",
|
||||||
|
"tpmAvailable": "TPM disponible",
|
||||||
|
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Mode furtif du pare-feu",
|
||||||
|
"linuxAppArmorEnabled": "Armure d'application",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Afficher les informations et les paramètres de l'appareil",
|
"deviceSettingsDescription": "Afficher les informations et les paramètres de l'appareil",
|
||||||
"devicePendingApprovalDescription": "Cet appareil est en attente d'approbation",
|
"devicePendingApprovalDescription": "Cet appareil est en attente d'approbation",
|
||||||
"deviceBlockedDescription": "Cet appareil est actuellement bloqué. Il ne pourra se connecter à aucune ressource à moins d'être débloqué.",
|
"deviceBlockedDescription": "Cet appareil est actuellement bloqué. Il ne pourra se connecter à aucune ressource à moins d'être débloqué.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
|
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
|
||||||
"resourcesTableDestination": "Destinazione",
|
"resourcesTableDestination": "Destinazione",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Indirizzo Alias",
|
||||||
|
"resourcesTableAliasAddressInfo": "Questo indirizzo fa parte della subnet di utilità dell'organizzazione. È usato per risolvere i record alias usando la risoluzione DNS interna.",
|
||||||
"resourcesTableClients": "Client",
|
"resourcesTableClients": "Client",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
||||||
"resourcesTableNoTargets": "Nessun obiettivo",
|
"resourcesTableNoTargets": "Nessun obiettivo",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Log In",
|
"logIn": "Log In",
|
||||||
"deviceInformation": "Informazioni Sul Dispositivo",
|
"deviceInformation": "Informazioni Sul Dispositivo",
|
||||||
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
|
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
|
||||||
|
"deviceSecurity": "Sicurezza Del Dispositivo",
|
||||||
|
"deviceSecurityDescription": "Informazioni postura sicurezza dispositivo",
|
||||||
"platform": "Piattaforma",
|
"platform": "Piattaforma",
|
||||||
"macosVersion": "versione macOS",
|
"macosVersion": "versione macOS",
|
||||||
"windowsVersion": "Versione Windows",
|
"windowsVersion": "Versione Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Prima Visto",
|
"firstSeen": "Prima Visto",
|
||||||
"lastSeen": "Visto L'Ultima",
|
"lastSeen": "Visto L'Ultima",
|
||||||
|
"biometricsEnabled": "Biometria Abilitata",
|
||||||
|
"diskEncrypted": "Cifratura Del Disco",
|
||||||
|
"firewallEnabled": "Firewall Abilitato",
|
||||||
|
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
|
||||||
|
"tpmAvailable": "TPM Disponibile",
|
||||||
|
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Modo Furtivo Del Firewall",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Visualizza informazioni e impostazioni del dispositivo",
|
"deviceSettingsDescription": "Visualizza informazioni e impostazioni del dispositivo",
|
||||||
"devicePendingApprovalDescription": "Questo dispositivo è in attesa di approvazione",
|
"devicePendingApprovalDescription": "Questo dispositivo è in attesa di approvazione",
|
||||||
"deviceBlockedDescription": "Questo dispositivo è attualmente bloccato. Non sarà in grado di connettersi a nessuna risorsa a meno che non sia sbloccato.",
|
"deviceBlockedDescription": "Questo dispositivo è attualmente bloccato. Non sarà in grado di connettersi a nessuna risorsa a meno che non sia sbloccato.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
|
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
|
||||||
"resourcesTableDestination": "대상지",
|
"resourcesTableDestination": "대상지",
|
||||||
"resourcesTableAlias": "별칭",
|
"resourcesTableAlias": "별칭",
|
||||||
|
"resourcesTableAliasAddress": "별칭 주소",
|
||||||
|
"resourcesTableAliasAddressInfo": "이 주소는 조직의 유틸리티 서브넷의 일부로, 내부 DNS 해석을 사용하여 별칭 레코드를 해석하는 데 사용됩니다.",
|
||||||
"resourcesTableClients": "클라이언트",
|
"resourcesTableClients": "클라이언트",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
||||||
"resourcesTableNoTargets": "대상 없음",
|
"resourcesTableNoTargets": "대상 없음",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "로그인",
|
"logIn": "로그인",
|
||||||
"deviceInformation": "장치 정보",
|
"deviceInformation": "장치 정보",
|
||||||
"deviceInformationDescription": "장치와 에이전트 정보",
|
"deviceInformationDescription": "장치와 에이전트 정보",
|
||||||
|
"deviceSecurity": "디바이스 보안",
|
||||||
|
"deviceSecurityDescription": "디바이스 보안 상태 정보",
|
||||||
"platform": "플랫폼",
|
"platform": "플랫폼",
|
||||||
"macosVersion": "macOS 버전",
|
"macosVersion": "macOS 버전",
|
||||||
"windowsVersion": "Windows 버전",
|
"windowsVersion": "Windows 버전",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "호스트 이름",
|
"hostname": "호스트 이름",
|
||||||
"firstSeen": "처음 발견됨",
|
"firstSeen": "처음 발견됨",
|
||||||
"lastSeen": "마지막으로 발견됨",
|
"lastSeen": "마지막으로 발견됨",
|
||||||
|
"biometricsEnabled": "생체 인식 활성화",
|
||||||
|
"diskEncrypted": "디스크 암호화됨",
|
||||||
|
"firewallEnabled": "방화벽 활성화",
|
||||||
|
"autoUpdatesEnabled": "자동 업데이트 활성화",
|
||||||
|
"tpmAvailable": "TPM 사용 가능",
|
||||||
|
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "방화벽 스텔스 모드",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "장치 정보 및 설정 보기",
|
"deviceSettingsDescription": "장치 정보 및 설정 보기",
|
||||||
"devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.",
|
"devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.",
|
||||||
"deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.",
|
"deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
|
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
|
||||||
"resourcesTableDestination": "Destinasjon",
|
"resourcesTableDestination": "Destinasjon",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias adresse",
|
||||||
|
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
|
||||||
"resourcesTableClients": "Klienter",
|
"resourcesTableClients": "Klienter",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
||||||
"resourcesTableNoTargets": "Ingen mål",
|
"resourcesTableNoTargets": "Ingen mål",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Logg inn",
|
"logIn": "Logg inn",
|
||||||
"deviceInformation": "Enhetens informasjon",
|
"deviceInformation": "Enhetens informasjon",
|
||||||
"deviceInformationDescription": "Informasjon om enheten og agenten",
|
"deviceInformationDescription": "Informasjon om enheten og agenten",
|
||||||
|
"deviceSecurity": "Enhetens sikkerhet",
|
||||||
|
"deviceSecurityDescription": "Sikkerhetsstillings informasjon om utstyr",
|
||||||
"platform": "Plattform",
|
"platform": "Plattform",
|
||||||
"macosVersion": "macOS versjon",
|
"macosVersion": "macOS versjon",
|
||||||
"windowsVersion": "Windows versjon",
|
"windowsVersion": "Windows versjon",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Først sett",
|
"firstSeen": "Først sett",
|
||||||
"lastSeen": "Sist sett",
|
"lastSeen": "Sist sett",
|
||||||
|
"biometricsEnabled": "Biometri aktivert",
|
||||||
|
"diskEncrypted": "Disk kryptert",
|
||||||
|
"firewallEnabled": "Brannmur aktivert",
|
||||||
|
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
|
||||||
|
"tpmAvailable": "TPM tilgjengelig",
|
||||||
|
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Brannmur Usynlig Modus",
|
||||||
|
"linuxAppArmorEnabled": "Rustning",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Vis enhetsinformasjon og innstillinger",
|
"deviceSettingsDescription": "Vis enhetsinformasjon og innstillinger",
|
||||||
"devicePendingApprovalDescription": "Denne enheten venter på godkjenning",
|
"devicePendingApprovalDescription": "Denne enheten venter på godkjenning",
|
||||||
"deviceBlockedDescription": "Denne enheten er blokkert. Det kan ikke kobles til noen ressurser med mindre de ikke blir blokkert.",
|
"deviceBlockedDescription": "Denne enheten er blokkert. Det kan ikke kobles til noen ressurser med mindre de ikke blir blokkert.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
|
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
|
||||||
"resourcesTableDestination": "Bestemming",
|
"resourcesTableDestination": "Bestemming",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias adres",
|
||||||
|
"resourcesTableAliasAddressInfo": "Dit adres is onderdeel van het hulpprogramma subnet van de organisatie. Het wordt gebruikt om aliasrecords op te lossen met behulp van interne DNS-resolutie.",
|
||||||
"resourcesTableClients": "Clienten",
|
"resourcesTableClients": "Clienten",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
||||||
"resourcesTableNoTargets": "Geen doelen",
|
"resourcesTableNoTargets": "Geen doelen",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Log in",
|
"logIn": "Log in",
|
||||||
"deviceInformation": "Apparaat informatie",
|
"deviceInformation": "Apparaat informatie",
|
||||||
"deviceInformationDescription": "Informatie over het apparaat en de agent",
|
"deviceInformationDescription": "Informatie over het apparaat en de agent",
|
||||||
|
"deviceSecurity": "Apparaat beveiliging",
|
||||||
|
"deviceSecurityDescription": "Apparaat beveiligingsinformatie",
|
||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"macosVersion": "macOS versie",
|
"macosVersion": "macOS versie",
|
||||||
"windowsVersion": "Windows versie",
|
"windowsVersion": "Windows versie",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Eerst gezien",
|
"firstSeen": "Eerst gezien",
|
||||||
"lastSeen": "Laatst gezien op",
|
"lastSeen": "Laatst gezien op",
|
||||||
|
"biometricsEnabled": "Biometrie ingeschakeld",
|
||||||
|
"diskEncrypted": "Schijf versleuteld",
|
||||||
|
"firewallEnabled": "Firewall ingeschakeld",
|
||||||
|
"autoUpdatesEnabled": "Auto Updates Ingeschakeld",
|
||||||
|
"tpmAvailable": "TPM beschikbaar",
|
||||||
|
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Firewall Verberg Modus",
|
||||||
|
"linuxAppArmorEnabled": "Appharnas",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Apparaatinformatie en -instellingen bekijken",
|
"deviceSettingsDescription": "Apparaatinformatie en -instellingen bekijken",
|
||||||
"devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring",
|
"devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring",
|
||||||
"deviceBlockedDescription": "Dit apparaat is momenteel geblokkeerd. Het kan geen verbinding maken met bronnen tenzij het wordt gedeblokkeerd.",
|
"deviceBlockedDescription": "Dit apparaat is momenteel geblokkeerd. Het kan geen verbinding maken met bronnen tenzij het wordt gedeblokkeerd.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
|
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
|
||||||
"resourcesTableDestination": "Miejsce docelowe",
|
"resourcesTableDestination": "Miejsce docelowe",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Adres aliasu",
|
||||||
|
"resourcesTableAliasAddressInfo": "Ten adres jest częścią podsieci użyteczności organizacji. Jest używany do rozwiązywania rekordów aliasu przy użyciu wewnętrznej rozdzielczości DNS.",
|
||||||
"resourcesTableClients": "Klientami",
|
"resourcesTableClients": "Klientami",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
||||||
"resourcesTableNoTargets": "Brak celów",
|
"resourcesTableNoTargets": "Brak celów",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Zaloguj się",
|
"logIn": "Zaloguj się",
|
||||||
"deviceInformation": "Informacje o urządzeniu",
|
"deviceInformation": "Informacje o urządzeniu",
|
||||||
"deviceInformationDescription": "Informacje o urządzeniu i agentach",
|
"deviceInformationDescription": "Informacje o urządzeniu i agentach",
|
||||||
|
"deviceSecurity": "Bezpieczeństwo urządzenia",
|
||||||
|
"deviceSecurityDescription": "Informacje o bezpieczeństwie urządzenia",
|
||||||
"platform": "Platforma",
|
"platform": "Platforma",
|
||||||
"macosVersion": "Wersja macOS",
|
"macosVersion": "Wersja macOS",
|
||||||
"windowsVersion": "Wersja Windows",
|
"windowsVersion": "Wersja Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Widziany po raz pierwszy",
|
"firstSeen": "Widziany po raz pierwszy",
|
||||||
"lastSeen": "Ostatnio widziane",
|
"lastSeen": "Ostatnio widziane",
|
||||||
|
"biometricsEnabled": "Biometria włączona",
|
||||||
|
"diskEncrypted": "Dysk zaszyfrowany",
|
||||||
|
"firewallEnabled": "Zapora włączona",
|
||||||
|
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
|
||||||
|
"tpmAvailable": "TPM dostępne",
|
||||||
|
"macosSipEnabled": "Ochrona integralności systemu (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Tryb Stealth zapory",
|
||||||
|
"linuxAppArmorEnabled": "Zbroja aplikacji",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Wyświetl informacje o urządzeniu i ustawienia",
|
"deviceSettingsDescription": "Wyświetl informacje o urządzeniu i ustawienia",
|
||||||
"devicePendingApprovalDescription": "To urządzenie czeka na zatwierdzenie",
|
"devicePendingApprovalDescription": "To urządzenie czeka na zatwierdzenie",
|
||||||
"deviceBlockedDescription": "To urządzenie jest obecnie zablokowane. Nie będzie można połączyć się z żadnymi zasobami, chyba że zostanie odblokowane.",
|
"deviceBlockedDescription": "To urządzenie jest obecnie zablokowane. Nie będzie można połączyć się z żadnymi zasobami, chyba że zostanie odblokowane.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
|
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
|
||||||
"resourcesTableDestination": "Destino",
|
"resourcesTableDestination": "Destino",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Endereço do Pseudônimo",
|
||||||
|
"resourcesTableAliasAddressInfo": "Este endereço faz parte da sub-rede de utilitários da organização. É usado para resolver registros de alias usando resolução de DNS interno.",
|
||||||
"resourcesTableClients": "Clientes",
|
"resourcesTableClients": "Clientes",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
||||||
"resourcesTableNoTargets": "Nenhum alvo",
|
"resourcesTableNoTargets": "Nenhum alvo",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Iniciar sessão",
|
"logIn": "Iniciar sessão",
|
||||||
"deviceInformation": "Informações do dispositivo",
|
"deviceInformation": "Informações do dispositivo",
|
||||||
"deviceInformationDescription": "Informações sobre o dispositivo e o agente",
|
"deviceInformationDescription": "Informações sobre o dispositivo e o agente",
|
||||||
|
"deviceSecurity": "Segurança do dispositivo",
|
||||||
|
"deviceSecurityDescription": "Informações sobre postagem de segurança",
|
||||||
"platform": "Plataforma",
|
"platform": "Plataforma",
|
||||||
"macosVersion": "Versão do macOS",
|
"macosVersion": "Versão do macOS",
|
||||||
"windowsVersion": "Versão do Windows",
|
"windowsVersion": "Versão do Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Visto primeiro",
|
"firstSeen": "Visto primeiro",
|
||||||
"lastSeen": "Visto por último",
|
"lastSeen": "Visto por último",
|
||||||
|
"biometricsEnabled": "Biometria habilitada",
|
||||||
|
"diskEncrypted": "Disco criptografado",
|
||||||
|
"firewallEnabled": "Firewall habilitado",
|
||||||
|
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
|
||||||
|
"tpmAvailable": "TPM disponível",
|
||||||
|
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Modo Furtivo do Firewall",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Ver informações e configurações do dispositivo",
|
"deviceSettingsDescription": "Ver informações e configurações do dispositivo",
|
||||||
"devicePendingApprovalDescription": "Este dispositivo está aguardando aprovação",
|
"devicePendingApprovalDescription": "Este dispositivo está aguardando aprovação",
|
||||||
"deviceBlockedDescription": "Este dispositivo está bloqueado no momento. Ele não será capaz de se conectar a qualquer recurso a menos que seja desbloqueado.",
|
"deviceBlockedDescription": "Este dispositivo está bloqueado no momento. Ele não será capaz de se conectar a qualquer recurso a menos que seja desbloqueado.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
|
"resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
|
||||||
"resourcesTableDestination": "Пункт назначения",
|
"resourcesTableDestination": "Пункт назначения",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Псевдоним адреса",
|
||||||
|
"resourcesTableAliasAddressInfo": "Этот адрес является частью вспомогательной подсети организации. Он используется для разрешения псевдонимов с использованием внутреннего разрешения DNS.",
|
||||||
"resourcesTableClients": "Клиенты",
|
"resourcesTableClients": "Клиенты",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
|
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
|
||||||
"resourcesTableNoTargets": "Нет ярлыков",
|
"resourcesTableNoTargets": "Нет ярлыков",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Войти",
|
"logIn": "Войти",
|
||||||
"deviceInformation": "Информация об устройстве",
|
"deviceInformation": "Информация об устройстве",
|
||||||
"deviceInformationDescription": "Информация о устройстве и агенте",
|
"deviceInformationDescription": "Информация о устройстве и агенте",
|
||||||
|
"deviceSecurity": "Безопасность устройства",
|
||||||
|
"deviceSecurityDescription": "Информация о позе безопасности устройства",
|
||||||
"platform": "Платформа",
|
"platform": "Платформа",
|
||||||
"macosVersion": "Версия macOS",
|
"macosVersion": "Версия macOS",
|
||||||
"windowsVersion": "Версия Windows",
|
"windowsVersion": "Версия Windows",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Первый раз виден",
|
"firstSeen": "Первый раз виден",
|
||||||
"lastSeen": "Последнее посещение",
|
"lastSeen": "Последнее посещение",
|
||||||
|
"biometricsEnabled": "Включены биометрические данные",
|
||||||
|
"diskEncrypted": "Диск зашифрован",
|
||||||
|
"firewallEnabled": "Брандмауэр включен",
|
||||||
|
"autoUpdatesEnabled": "Автоматические обновления включены",
|
||||||
|
"tpmAvailable": "Доступно TPM",
|
||||||
|
"macosSipEnabled": "Защита целостности системы (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Стилс-режим брандмауэра",
|
||||||
|
"linuxAppArmorEnabled": "Броня",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Просмотр информации и настроек устройства",
|
"deviceSettingsDescription": "Просмотр информации и настроек устройства",
|
||||||
"devicePendingApprovalDescription": "Это устройство ожидает одобрения",
|
"devicePendingApprovalDescription": "Это устройство ожидает одобрения",
|
||||||
"deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.",
|
"deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
|
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
|
||||||
"resourcesTableDestination": "Hedef",
|
"resourcesTableDestination": "Hedef",
|
||||||
"resourcesTableAlias": "Takma Ad",
|
"resourcesTableAlias": "Takma Ad",
|
||||||
|
"resourcesTableAliasAddress": "Alias Adresi",
|
||||||
|
"resourcesTableAliasAddressInfo": "Bu adres, kuruluşun yardımcı ağ alt bantının bir parçasıdır. Alias kayıtlarını çözümlemek için dahili DNS çözümlemesi kullanılır.",
|
||||||
"resourcesTableClients": "İstemciler",
|
"resourcesTableClients": "İstemciler",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
||||||
"resourcesTableNoTargets": "Hedef yok",
|
"resourcesTableNoTargets": "Hedef yok",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "Giriş Yap",
|
"logIn": "Giriş Yap",
|
||||||
"deviceInformation": "Cihaz Bilgisi",
|
"deviceInformation": "Cihaz Bilgisi",
|
||||||
"deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi",
|
"deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi",
|
||||||
|
"deviceSecurity": "Cihaz Güvenliği",
|
||||||
|
"deviceSecurityDescription": "Cihaz güvenliği durumu bilgisi",
|
||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"macosVersion": "macOS Sürümü",
|
"macosVersion": "macOS Sürümü",
|
||||||
"windowsVersion": "Windows Sürümü",
|
"windowsVersion": "Windows Sürümü",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Ana Makine Adı",
|
"hostname": "Ana Makine Adı",
|
||||||
"firstSeen": "İlk Görüldü",
|
"firstSeen": "İlk Görüldü",
|
||||||
"lastSeen": "Son Görüldü",
|
"lastSeen": "Son Görüldü",
|
||||||
|
"biometricsEnabled": "Biyometri Etkin",
|
||||||
|
"diskEncrypted": "Disk Şifrelenmiş",
|
||||||
|
"firewallEnabled": "Güvenlik Duvarı Etkin",
|
||||||
|
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
|
||||||
|
"tpmAvailable": "TPM Mevcut",
|
||||||
|
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "Cihaz bilgilerini ve ayarlarını görüntüleyin",
|
"deviceSettingsDescription": "Cihaz bilgilerini ve ayarlarını görüntüleyin",
|
||||||
"devicePendingApprovalDescription": "Bu cihaz onay bekliyor",
|
"devicePendingApprovalDescription": "Bu cihaz onay bekliyor",
|
||||||
"deviceBlockedDescription": "Bu cihaz şu anda engellidir. Engeli kaldırılmadığı sürece hiçbir kaynağa bağlanamayacaktır.",
|
"deviceBlockedDescription": "Bu cihaz şu anda engellidir. Engeli kaldırılmadığı sürece hiçbir kaynağa bağlanamayacaktır.",
|
||||||
|
|||||||
@@ -1625,6 +1625,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
||||||
"resourcesTableDestination": "目标",
|
"resourcesTableDestination": "目标",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "别名地址",
|
||||||
|
"resourcesTableAliasAddressInfo": "此地址是组织实用子网的一部分。它用来使用内部DNS解析来解析别名记录。",
|
||||||
"resourcesTableClients": "客户端",
|
"resourcesTableClients": "客户端",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
||||||
"resourcesTableNoTargets": "没有目标",
|
"resourcesTableNoTargets": "没有目标",
|
||||||
@@ -2489,6 +2491,8 @@
|
|||||||
"logIn": "登录",
|
"logIn": "登录",
|
||||||
"deviceInformation": "设备信息",
|
"deviceInformation": "设备信息",
|
||||||
"deviceInformationDescription": "关于设备和代理的信息",
|
"deviceInformationDescription": "关于设备和代理的信息",
|
||||||
|
"deviceSecurity": "设备安全",
|
||||||
|
"deviceSecurityDescription": "设备安全态势信息",
|
||||||
"platform": "平台",
|
"platform": "平台",
|
||||||
"macosVersion": "macOS 版本",
|
"macosVersion": "macOS 版本",
|
||||||
"windowsVersion": "Windows 版本",
|
"windowsVersion": "Windows 版本",
|
||||||
@@ -2501,6 +2505,16 @@
|
|||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "第一次查看",
|
"firstSeen": "第一次查看",
|
||||||
"lastSeen": "上次查看时间",
|
"lastSeen": "上次查看时间",
|
||||||
|
"biometricsEnabled": "生物计已启用",
|
||||||
|
"diskEncrypted": "磁盘加密",
|
||||||
|
"firewallEnabled": "防火墙已启用",
|
||||||
|
"autoUpdatesEnabled": "启用自动更新",
|
||||||
|
"tpmAvailable": "TPM 可用",
|
||||||
|
"macosSipEnabled": "系统完整性保护 (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "防火墙隐形模式",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
"deviceSettingsDescription": "查看设备信息和设置",
|
"deviceSettingsDescription": "查看设备信息和设置",
|
||||||
"devicePendingApprovalDescription": "此设备正在等待批准",
|
"devicePendingApprovalDescription": "此设备正在等待批准",
|
||||||
"deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。",
|
"deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。",
|
||||||
|
|||||||
+2392
-2095
File diff suppressed because it is too large
Load Diff
Generated
+856
-920
File diff suppressed because it is too large
Load Diff
+15
-15
@@ -33,9 +33,9 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.2.0",
|
"@asteasolutions/zod-to-openapi": "8.4.0",
|
||||||
"@aws-sdk/client-s3": "3.955.0",
|
"@aws-sdk/client-s3": "3.971.0",
|
||||||
"@faker-js/faker": "10.1.0",
|
"@faker-js/faker": "10.2.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"i": "0.3.7",
|
"i": "0.3.7",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.9.2",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"maxmind": "5.0.1",
|
"maxmind": "5.0.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"next-intl": "4.6.1",
|
"next-intl": "4.7.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
@@ -109,21 +109,21 @@
|
|||||||
"npm": "11.7.0",
|
"npm": "11.7.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.16.3",
|
"pg": "8.17.1",
|
||||||
"posthog-node": "5.17.4",
|
"posthog-node": "5.23.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "9.13.0",
|
"react-day-picker": "9.13.0",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.68.0",
|
"react-hook-form": "7.71.1",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"reodotdev": "1.0.0",
|
"reodotdev": "1.0.0",
|
||||||
"resend": "6.6.0",
|
"resend": "6.8.0",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"stripe": "20.1.0",
|
"stripe": "20.2.0",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
@@ -133,10 +133,10 @@
|
|||||||
"visionscarto-world-atlas": "1.0.0",
|
"visionscarto-world-atlas": "1.0.0",
|
||||||
"winston": "3.19.0",
|
"winston": "3.19.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.19.0",
|
||||||
"yaml": "2.8.2",
|
"yaml": "2.8.2",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "4.2.1",
|
"zod": "4.3.5",
|
||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -170,12 +170,12 @@
|
|||||||
"esbuild": "0.27.2",
|
"esbuild": "0.27.2",
|
||||||
"esbuild-node-externals": "1.20.1",
|
"esbuild-node-externals": "1.20.1",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.0",
|
||||||
"react-email": "5.0.7",
|
"react-email": "5.2.5",
|
||||||
"tailwindcss": "4.1.18",
|
"tailwindcss": "4.1.18",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.49.0"
|
"typescript-eslint": "8.53.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", {
|
|||||||
|
|
||||||
// Windows-specific posture check information
|
// Windows-specific posture check information
|
||||||
|
|
||||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
|
||||||
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
|||||||
|
|
||||||
// Windows-specific posture check information
|
// Windows-specific posture check information
|
||||||
|
|
||||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
|
|||||||
|
|
||||||
// Windows-specific posture check information
|
// Windows-specific posture check information
|
||||||
|
|
||||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
})
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
|
|||||||
|
|
||||||
// Windows-specific posture check information
|
// Windows-specific posture check information
|
||||||
|
|
||||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
})
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers";
|
|||||||
import { resourcePassword } from "@server/db";
|
import { resourcePassword } from "@server/db";
|
||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export type ProxyResourcesResults = {
|
export type ProxyResourcesResults = {
|
||||||
@@ -213,11 +213,7 @@ export async function updateProxyResources(
|
|||||||
// Update existing resource
|
// Update existing resource
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (build == "enterprise" && !isLicensed) {
|
if (!isLicensed) {
|
||||||
logger.warn(
|
|
||||||
"Server is not licensed! Clearing set maintenance screen values"
|
|
||||||
);
|
|
||||||
// null the maintenance mode fields if not licensed
|
|
||||||
resourceData.maintenance = undefined;
|
resourceData.maintenance = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +590,7 @@ export async function updateProxyResources(
|
|||||||
existingRule.action !== getRuleAction(rule.action) ||
|
existingRule.action !== getRuleAction(rule.action) ||
|
||||||
existingRule.match !== rule.match.toUpperCase() ||
|
existingRule.match !== rule.match.toUpperCase() ||
|
||||||
existingRule.value !==
|
existingRule.value !==
|
||||||
getRuleValue(rule.match.toUpperCase(), rule.value) ||
|
getRuleValue(rule.match.toUpperCase(), rule.value) ||
|
||||||
existingRule.priority !== intendedPriority
|
existingRule.priority !== intendedPriority
|
||||||
) {
|
) {
|
||||||
validateRule(rule);
|
validateRule(rule);
|
||||||
@@ -653,11 +649,7 @@ export async function updateProxyResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (build == "enterprise" && !isLicensed) {
|
if (!isLicensed) {
|
||||||
logger.warn(
|
|
||||||
"Server is not licensed! Clearing set maintenance screen values"
|
|
||||||
);
|
|
||||||
// null the maintenance mode fields if not licensed
|
|
||||||
resourceData.maintenance = undefined;
|
resourceData.maintenance = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { getUniqueClientName } from "@server/db/names";
|
import { getUniqueClientName } from "@server/db/names";
|
||||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { sendTerminateClient } from "@server/routers/client/terminate";
|
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||||
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
|
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
|
||||||
|
|||||||
@@ -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.14.0";
|
export const APP_VERSION = "1.15.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,17 +1,3 @@
|
|||||||
import { build } from "@server/build";
|
|
||||||
import license from "#dynamic/license/license";
|
|
||||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
|
||||||
|
|
||||||
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
||||||
if (build === "enterprise") {
|
return false;
|
||||||
return await license.isUnlocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (build === "saas") {
|
|
||||||
const { tier } = await getOrgTierData(orgId);
|
|
||||||
return tier === TierId.STANDARD;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import license from "#private/license/license";
|
||||||
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
|
|
||||||
|
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
||||||
|
if (build === "enterprise") {
|
||||||
|
return await license.isUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build === "saas") {
|
||||||
|
const { tier } = await getOrgTierData(orgId);
|
||||||
|
return tier === TierId.STANDARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
import { getOrgTierData } from "#private/lib/billing";
|
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
|
||||||
|
|
||||||
export async function verifyValidLicense(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (build != "saas") {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tier, active } = await getOrgTierData(orgId);
|
|
||||||
const subscribed = tier === TierId.STANDARD;
|
|
||||||
if (!subscribed) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"This organization's current plan does not support this feature."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (e) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying subscription"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
|
|
||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from "express";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { getOrgTierData } from "@server/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import {
|
import {
|
||||||
approvals,
|
approvals,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { approvals, clients, db, orgs, type Approval } from "@server/db";
|
import { approvals, clients, db, orgs, type Approval } from "@server/db";
|
||||||
import { getOrgTierData } from "@server/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function upsertLoginPageBranding(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
const parsedBody = await bodySchema.safeParseAsync(req.body);
|
||||||
if (!parsedBody.success) {
|
if (!parsedBody.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ const applyBlueprintSchema = z
|
|||||||
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
|
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
source: z.enum(["API", "UI", "CLI"]).optional()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export async function applyYAMLBlueprint(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { blueprint: contents, name } = parsedBody.data;
|
const { blueprint: contents, name, source = "UI" } = parsedBody.data;
|
||||||
|
|
||||||
logger.debug(`Received blueprint:`, contents);
|
logger.debug(`Received blueprint:`, contents);
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ export async function applyYAMLBlueprint(
|
|||||||
blueprint = await applyBlueprint({
|
blueprint = await applyBlueprint({
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
source: "UI",
|
source,
|
||||||
configData: parsedConfig
|
configData: parsedConfig
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Blueprint } from "@server/db";
|
import type { Blueprint } from "@server/db";
|
||||||
|
|
||||||
export type BlueprintSource = "API" | "UI" | "NEWT";
|
export type BlueprintSource = "API" | "UI" | "NEWT" | "CLI";
|
||||||
|
|
||||||
export type BlueprintData = Omit<Blueprint, "source"> & {
|
export type BlueprintData = Omit<Blueprint, "source"> & {
|
||||||
source: BlueprintSource;
|
source: BlueprintSource;
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import createHttpError from "http-errors";
|
|||||||
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";
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
|
||||||
import { sendTerminateClient } from "./terminate";
|
|
||||||
import { OlmErrorCodes } from "../olm/error";
|
|
||||||
|
|
||||||
const archiveClientSchema = z.strictObject({
|
const archiveClientSchema = z.strictObject({
|
||||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -77,9 +74,6 @@ export async function archiveClient(
|
|||||||
.update(clients)
|
.update(clients)
|
||||||
.set({ archived: true })
|
.set({ archived: true })
|
||||||
.where(eq(clients.clientId, clientId));
|
.where(eq(clients.clientId, clientId));
|
||||||
|
|
||||||
// Rebuild associations to clean up related data
|
|
||||||
await rebuildClientAssociationsFromClient(client, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { getUserDeviceName } from "@server/db/names";
|
import { getUserDeviceName } from "@server/db/names";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const getClientSchema = z.strictObject({
|
const getClientSchema = z.strictObject({
|
||||||
clientId: z
|
clientId: z
|
||||||
@@ -58,7 +59,7 @@ type PostureData = {
|
|||||||
firewallEnabled?: boolean | null;
|
firewallEnabled?: boolean | null;
|
||||||
autoUpdatesEnabled?: boolean | null;
|
autoUpdatesEnabled?: boolean | null;
|
||||||
tpmAvailable?: boolean | null;
|
tpmAvailable?: boolean | null;
|
||||||
windowsDefenderEnabled?: boolean | null;
|
windowsAntivirusEnabled?: boolean | null;
|
||||||
macosSipEnabled?: boolean | null;
|
macosSipEnabled?: boolean | null;
|
||||||
macosGatekeeperEnabled?: boolean | null;
|
macosGatekeeperEnabled?: boolean | null;
|
||||||
macosFirewallStealthMode?: boolean | null;
|
macosFirewallStealthMode?: boolean | null;
|
||||||
@@ -75,75 +76,123 @@ function getPlatformPostureData(
|
|||||||
const normalizedPlatform = platform?.toLowerCase() || "unknown";
|
const normalizedPlatform = platform?.toLowerCase() || "unknown";
|
||||||
const posture: PostureData = {};
|
const posture: PostureData = {};
|
||||||
|
|
||||||
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender
|
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status
|
||||||
if (normalizedPlatform === "windows") {
|
if (normalizedPlatform === "windows") {
|
||||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
if (
|
||||||
|
fingerprint.diskEncrypted !== null &&
|
||||||
|
fingerprint.diskEncrypted !== undefined
|
||||||
|
) {
|
||||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||||
}
|
}
|
||||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.firewallEnabled !== null &&
|
||||||
|
fingerprint.firewallEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
|
if (
|
||||||
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
fingerprint.tpmAvailable !== null &&
|
||||||
}
|
fingerprint.tpmAvailable !== undefined
|
||||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
) {
|
||||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||||
}
|
}
|
||||||
if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) {
|
if (
|
||||||
posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled;
|
fingerprint.windowsAntivirusEnabled !== null &&
|
||||||
|
fingerprint.windowsAntivirusEnabled !== undefined
|
||||||
|
) {
|
||||||
|
posture.windowsAntivirusEnabled =
|
||||||
|
fingerprint.windowsAntivirusEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
|
// macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
|
||||||
else if (normalizedPlatform === "macos") {
|
else if (normalizedPlatform === "macos") {
|
||||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
if (
|
||||||
|
fingerprint.diskEncrypted !== null &&
|
||||||
|
fingerprint.diskEncrypted !== undefined
|
||||||
|
) {
|
||||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||||
}
|
}
|
||||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.biometricsEnabled !== null &&
|
||||||
|
fingerprint.biometricsEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.firewallEnabled !== null &&
|
||||||
|
fingerprint.firewallEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.macosSipEnabled !== null &&
|
||||||
|
fingerprint.macosSipEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.macosSipEnabled = fingerprint.macosSipEnabled;
|
posture.macosSipEnabled = fingerprint.macosSipEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.macosGatekeeperEnabled !== null &&
|
||||||
|
fingerprint.macosGatekeeperEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
|
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
|
if (
|
||||||
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
|
fingerprint.macosFirewallStealthMode !== null &&
|
||||||
|
fingerprint.macosFirewallStealthMode !== undefined
|
||||||
|
) {
|
||||||
|
posture.macosFirewallStealthMode =
|
||||||
|
fingerprint.macosFirewallStealthMode;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fingerprint.autoUpdatesEnabled !== null &&
|
||||||
|
fingerprint.autoUpdatesEnabled !== undefined
|
||||||
|
) {
|
||||||
|
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
|
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
|
||||||
else if (normalizedPlatform === "linux") {
|
else if (normalizedPlatform === "linux") {
|
||||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
if (
|
||||||
|
fingerprint.diskEncrypted !== null &&
|
||||||
|
fingerprint.diskEncrypted !== undefined
|
||||||
|
) {
|
||||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||||
}
|
}
|
||||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.firewallEnabled !== null &&
|
||||||
|
fingerprint.firewallEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.linuxAppArmorEnabled !== null &&
|
||||||
|
fingerprint.linuxAppArmorEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
|
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) {
|
if (
|
||||||
|
fingerprint.linuxSELinuxEnabled !== null &&
|
||||||
|
fingerprint.linuxSELinuxEnabled !== undefined
|
||||||
|
) {
|
||||||
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
|
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
|
||||||
}
|
}
|
||||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
if (
|
||||||
|
fingerprint.tpmAvailable !== null &&
|
||||||
|
fingerprint.tpmAvailable !== undefined
|
||||||
|
) {
|
||||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// iOS: Biometric configuration
|
// iOS: Biometric configuration
|
||||||
else if (normalizedPlatform === "ios") {
|
else if (normalizedPlatform === "ios") {
|
||||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
// none supported yet
|
||||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Android: Screen lock, Biometric configuration, Hard drive encryption
|
// Android: Screen lock, Biometric configuration, Hard drive encryption
|
||||||
else if (normalizedPlatform === "android") {
|
else if (normalizedPlatform === "android") {
|
||||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
if (
|
||||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
fingerprint.diskEncrypted !== null &&
|
||||||
}
|
fingerprint.diskEncrypted !== undefined
|
||||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
) {
|
||||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,23 +289,27 @@ export async function getClient(
|
|||||||
// Build fingerprint data if available
|
// Build fingerprint data if available
|
||||||
const fingerprintData = client.currentFingerprint
|
const fingerprintData = client.currentFingerprint
|
||||||
? {
|
? {
|
||||||
username: client.currentFingerprint.username || null,
|
username: client.currentFingerprint.username || null,
|
||||||
hostname: client.currentFingerprint.hostname || null,
|
hostname: client.currentFingerprint.hostname || null,
|
||||||
platform: client.currentFingerprint.platform || null,
|
platform: client.currentFingerprint.platform || null,
|
||||||
osVersion: client.currentFingerprint.osVersion || null,
|
osVersion: client.currentFingerprint.osVersion || null,
|
||||||
kernelVersion:
|
kernelVersion:
|
||||||
client.currentFingerprint.kernelVersion || null,
|
client.currentFingerprint.kernelVersion || null,
|
||||||
arch: client.currentFingerprint.arch || null,
|
arch: client.currentFingerprint.arch || null,
|
||||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||||
lastSeen: client.currentFingerprint.lastSeen || null
|
lastSeen: client.currentFingerprint.lastSeen || null
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Build posture data if available (platform-specific)
|
// Build posture data if available (platform-specific)
|
||||||
|
// Only return posture data if org is licensed/subscribed
|
||||||
let postureData: PostureData | null = null;
|
let postureData: PostureData | null = null;
|
||||||
if (build !== "oss") {
|
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||||
|
client.clients.orgId
|
||||||
|
);
|
||||||
|
if (isOrgLicensed) {
|
||||||
postureData = getPlatformPostureData(
|
postureData = getPlatformPostureData(
|
||||||
client.currentFingerprint?.platform || null,
|
client.currentFingerprint?.platform || null,
|
||||||
client.currentFingerprint
|
client.currentFingerprint
|
||||||
|
|||||||
@@ -175,10 +175,7 @@ async function getSiteAssociations(clientIds: number[]) {
|
|||||||
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientWithSites = Omit<
|
type ClientWithSites = Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||||
Awaited<ReturnType<typeof queryClients>>[0],
|
|
||||||
"deviceModel"
|
|
||||||
> & {
|
|
||||||
sites: Array<{
|
sites: Array<{
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteName: string | null;
|
siteName: string | null;
|
||||||
@@ -324,9 +321,8 @@ export async function listClients(
|
|||||||
const clientsWithSites = clientsList.map((client) => {
|
const clientsWithSites = clientsList.map((client) => {
|
||||||
const model = client.deviceModel || null;
|
const model = client.deviceModel || null;
|
||||||
const newName = getUserDeviceName(model, client.name);
|
const newName = getUserDeviceName(model, client.name);
|
||||||
const { deviceModel, ...clientWithoutDeviceModel } = client;
|
|
||||||
return {
|
return {
|
||||||
...clientWithoutDeviceModel,
|
...client,
|
||||||
name: newName,
|
name: newName,
|
||||||
sites: sitesByClient[client.clientId] || []
|
sites: sitesByClient[client.clientId] || []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { olms, clients } from "@server/db";
|
import { olms } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -8,9 +8,6 @@ import response from "@server/lib/response";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
|
||||||
import { sendTerminateClient } from "../client/terminate";
|
|
||||||
import { OlmErrorCodes } from "./error";
|
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -37,26 +34,7 @@ export async function archiveUserOlm(
|
|||||||
|
|
||||||
const { olmId } = parsedParams.data;
|
const { olmId } = parsedParams.data;
|
||||||
|
|
||||||
// Archive the OLM and disconnect associated clients in a transaction
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Find all clients associated with this OLM
|
|
||||||
const associatedClients = await trx
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.olmId, olmId));
|
|
||||||
|
|
||||||
// Disconnect clients from the OLM (set olmId to null)
|
|
||||||
for (const client of associatedClients) {
|
|
||||||
await trx
|
|
||||||
.update(clients)
|
|
||||||
.set({ olmId: null })
|
|
||||||
.where(eq(clients.clientId, client.clientId));
|
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(client, trx);
|
|
||||||
await sendTerminateClient(client.clientId, OlmErrorCodes.TERMINATED_ARCHIVED, olmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Archive the OLM (set archived to true)
|
|
||||||
await trx
|
await trx
|
||||||
.update(olms)
|
.update(olms)
|
||||||
.set({ archived: true })
|
.set({ archived: true })
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
|
|||||||
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
|
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
|
||||||
tpmAvailable: postures.tpmAvailable ?? false,
|
tpmAvailable: postures.tpmAvailable ?? false,
|
||||||
|
|
||||||
windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false,
|
windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false,
|
||||||
|
|
||||||
macosSipEnabled: postures.macosSipEnabled ?? false,
|
macosSipEnabled: postures.macosSipEnabled ?? false,
|
||||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
|
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
|
||||||
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
|
|||||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||||
tpmAvailable: postures.tpmAvailable,
|
tpmAvailable: postures.tpmAvailable,
|
||||||
|
|
||||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||||
|
|
||||||
macosSipEnabled: postures.macosSipEnabled,
|
macosSipEnabled: postures.macosSipEnabled,
|
||||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||||
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
|
|||||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||||
tpmAvailable: postures.tpmAvailable,
|
tpmAvailable: postures.tpmAvailable,
|
||||||
|
|
||||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||||
|
|
||||||
macosSipEnabled: postures.macosSipEnabled,
|
macosSipEnabled: postures.macosSipEnabled,
|
||||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||||
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
|
|||||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||||
tpmAvailable: postures.tpmAvailable,
|
tpmAvailable: postures.tpmAvailable,
|
||||||
|
|
||||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||||
|
|
||||||
macosSipEnabled: postures.macosSipEnabled,
|
macosSipEnabled: postures.macosSipEnabled,
|
||||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||||
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
|
|||||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||||
tpmAvailable: postures.tpmAvailable,
|
tpmAvailable: postures.tpmAvailable,
|
||||||
|
|
||||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||||
|
|
||||||
macosSipEnabled: postures.macosSipEnabled,
|
macosSipEnabled: postures.macosSipEnabled,
|
||||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("Handling fingerprint insertion for olm register...", {
|
||||||
|
olmId: olm.olmId,
|
||||||
|
fingerprint,
|
||||||
|
postures
|
||||||
|
});
|
||||||
|
|
||||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -143,7 +149,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!policyCheck.policies?.passwordAge?.compliant === false) {
|
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
||||||
);
|
);
|
||||||
@@ -153,7 +159,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
!policyCheck.policies?.maxSessionLength?.compliant === false
|
policyCheck.policies?.maxSessionLength?.compliant === false
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export async function recoverOlmWithFingerprint(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(olms.userId, userId),
|
eq(olms.userId, userId),
|
||||||
eq(olms.archived, false),
|
|
||||||
eq(
|
eq(
|
||||||
currentFingerprint.platformFingerprint,
|
currentFingerprint.platformFingerprint,
|
||||||
platformFingerprint
|
platformFingerprint
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { build } from "@server/build";
|
|||||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { cache } from "@server/lib/cache";
|
import { cache } from "@server/lib/cache";
|
||||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const updateOrgParamsSchema = z.strictObject({
|
const updateOrgParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -89,7 +89,7 @@ export async function updateOrg(
|
|||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (build == "enterprise" && !isLicensed) {
|
if (!isLicensed) {
|
||||||
parsedBody.data.requireTwoFactor = undefined;
|
parsedBody.data.requireTwoFactor = undefined;
|
||||||
parsedBody.data.maxSessionLengthHours = undefined;
|
parsedBody.data.maxSessionLengthHours = undefined;
|
||||||
parsedBody.data.passwordExpiryDays = undefined;
|
parsedBody.data.passwordExpiryDays = undefined;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { OpenAPITags } from "@server/openApi";
|
|||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const updateResourceParamsSchema = z.strictObject({
|
const updateResourceParamsSchema = z.strictObject({
|
||||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -342,11 +342,7 @@ async function updateHttpResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
||||||
if (build == "enterprise" && !isLicensed) {
|
if (!isLicensed) {
|
||||||
logger.warn(
|
|
||||||
"Server is not licensed! Clearing set maintenance screen values"
|
|
||||||
);
|
|
||||||
// null the maintenance mode fields if not licensed
|
|
||||||
updateData.maintenanceModeEnabled = undefined;
|
updateData.maintenanceModeEnabled = undefined;
|
||||||
updateData.maintenanceModeType = undefined;
|
updateData.maintenanceModeType = undefined;
|
||||||
updateData.maintenanceTitle = undefined;
|
updateData.maintenanceTitle = undefined;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ActionsEnum } from "@server/auth/actions";
|
|||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
|
||||||
const createRoleParamsSchema = z.strictObject({
|
const createRoleParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -101,7 +101,7 @@ export async function createRole(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (build === "oss" || !isLicensed) {
|
if (!isLicensed) {
|
||||||
roleData.requireDeviceApproval = undefined;
|
roleData.requireDeviceApproval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { build } from "@server/build";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const updateRoleParamsSchema = z.strictObject({
|
const updateRoleParamsSchema = z.strictObject({
|
||||||
@@ -112,7 +111,7 @@ export async function updateRole(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (build === "oss" || !isLicensed) {
|
if (!isLicensed) {
|
||||||
updateData.requireDeviceApproval = undefined;
|
updateData.requireDeviceApproval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
destination: siteResources.destination,
|
destination: siteResources.destination,
|
||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
alias: siteResources.alias,
|
alias: siteResources.alias,
|
||||||
|
aliasAddress: siteResources.aliasAddress,
|
||||||
tcpPortRangeString: siteResources.tcpPortRangeString,
|
tcpPortRangeString: siteResources.tcpPortRangeString,
|
||||||
udpPortRangeString: siteResources.udpPortRangeString,
|
udpPortRangeString: siteResources.udpPortRangeString,
|
||||||
disableIcmp: siteResources.disableIcmp,
|
disableIcmp: siteResources.disableIcmp,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default async function migration() {
|
|||||||
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
||||||
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
|
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
||||||
@@ -75,7 +75,7 @@ export default async function migration() {
|
|||||||
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
||||||
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
|
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
||||||
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' (
|
|||||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||||
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
||||||
'tpmAvailable' integer DEFAULT false NOT NULL,
|
'tpmAvailable' integer DEFAULT false NOT NULL,
|
||||||
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
|
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
||||||
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
|
|||||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||||
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
||||||
'tpmAvailable' integer DEFAULT false NOT NULL,
|
'tpmAvailable' integer DEFAULT false NOT NULL,
|
||||||
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
|
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
||||||
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
||||||
|
|||||||
@@ -19,17 +19,6 @@ export interface ApprovalFeedPageProps {
|
|||||||
export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
|
export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
let approvals: ApprovalItem[] = [];
|
|
||||||
const res = await internal
|
|
||||||
.get<
|
|
||||||
AxiosResponse<{ approvals: ApprovalItem[] }>
|
|
||||||
>(`/org/${params.orgId}/approvals`, await authCookieHeader())
|
|
||||||
.catch((e) => {});
|
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
approvals = res.data.data.approvals;
|
|
||||||
}
|
|
||||||
|
|
||||||
let org: GetOrgResponse | null = null;
|
let org: GetOrgResponse | null = null;
|
||||||
const orgRes = await getCachedOrg(params.orgId);
|
const orgRes = await getCachedOrg(params.orgId);
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
|
|||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
import { OlmInstallCommands } from "@app/components/olm-install-commands";
|
||||||
|
|
||||||
export default function CredentialsPage() {
|
export default function CredentialsPage() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -204,6 +205,12 @@ export default function CredentialsPage() {
|
|||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
)}
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<OlmInstallCommands
|
||||||
|
id={displayOlmId ?? "********"}
|
||||||
|
endpoint={env.app.dashboardUrl}
|
||||||
|
secret={displaySecret ?? "********"}
|
||||||
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<ConfirmDeleteDialog
|
<ConfirmDeleteDialog
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
|
import {
|
||||||
|
InfoSection,
|
||||||
|
InfoSectionContent,
|
||||||
|
InfoSections,
|
||||||
|
InfoSectionTitle
|
||||||
|
} from "@app/components/InfoSection";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionForm,
|
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -19,44 +26,24 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { createElement, useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { ChevronDown, ChevronUp, InfoIcon, Terminal } from "lucide-react";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
|
||||||
import {
|
|
||||||
InfoSection,
|
|
||||||
InfoSectionContent,
|
|
||||||
InfoSections,
|
|
||||||
InfoSectionTitle
|
|
||||||
} from "@app/components/InfoSection";
|
|
||||||
import {
|
|
||||||
FaApple,
|
|
||||||
FaCubes,
|
|
||||||
FaDocker,
|
|
||||||
FaFreebsd,
|
|
||||||
FaWindows
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { SiNixos, SiKubernetes } from "react-icons/si";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import {
|
||||||
CreateClientBody,
|
CreateClientBody,
|
||||||
CreateClientResponse,
|
CreateClientResponse,
|
||||||
PickClientDefaultsResponse
|
PickClientDefaultsResponse
|
||||||
} from "@server/routers/client";
|
} from "@server/routers/client";
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { OlmInstallCommands } from "@app/components/olm-install-commands";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type ClientType = "olm";
|
type ClientType = "olm";
|
||||||
@@ -68,18 +55,6 @@ interface TunnelTypeOption {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandItem = string | { title: string; command: string };
|
|
||||||
|
|
||||||
type Commands = {
|
|
||||||
unix: Record<string, CommandItem[]>;
|
|
||||||
windows: Record<string, CommandItem[]>;
|
|
||||||
docker: Record<string, CommandItem[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const platforms = ["unix", "docker", "windows"] as const;
|
|
||||||
|
|
||||||
type Platform = (typeof platforms)[number];
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
@@ -113,13 +88,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
|
|
||||||
const [platform, setPlatform] = useState<Platform>("unix");
|
|
||||||
const [architecture, setArchitecture] = useState("All");
|
|
||||||
const [commands, setCommands] = useState<Commands | null>(null);
|
|
||||||
|
|
||||||
const [olmId, setOlmId] = useState("");
|
const [olmId, setOlmId] = useState("");
|
||||||
const [olmSecret, setOlmSecret] = useState("");
|
const [olmSecret, setOlmSecret] = useState("");
|
||||||
const [olmCommand, setOlmCommand] = useState("");
|
const [olmVersion, setOlmVersion] = useState("latest");
|
||||||
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||||
@@ -127,136 +98,6 @@ export default function Page() {
|
|||||||
const [clientDefaults, setClientDefaults] =
|
const [clientDefaults, setClientDefaults] =
|
||||||
useState<PickClientDefaultsResponse | null>(null);
|
useState<PickClientDefaultsResponse | null>(null);
|
||||||
|
|
||||||
const hydrateCommands = (
|
|
||||||
id: string,
|
|
||||||
secret: string,
|
|
||||||
endpoint: string,
|
|
||||||
version: string
|
|
||||||
) => {
|
|
||||||
const commands = {
|
|
||||||
unix: {
|
|
||||||
All: [
|
|
||||||
{
|
|
||||||
title: t("install"),
|
|
||||||
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("run"),
|
|
||||||
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
windows: {
|
|
||||||
x64: [
|
|
||||||
{
|
|
||||||
title: t("install"),
|
|
||||||
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("run"),
|
|
||||||
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
docker: {
|
|
||||||
"Docker Compose": [
|
|
||||||
`services:
|
|
||||||
olm:
|
|
||||||
image: fosrl/olm
|
|
||||||
container_name: olm
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
devices:
|
|
||||||
- /dev/net/tun:/dev/net/tun
|
|
||||||
environment:
|
|
||||||
- PANGOLIN_ENDPOINT=${endpoint}
|
|
||||||
- OLM_ID=${id}
|
|
||||||
- OLM_SECRET=${secret}`
|
|
||||||
],
|
|
||||||
"Docker Run": [
|
|
||||||
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setCommands(commands);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArchitectures = () => {
|
|
||||||
switch (platform) {
|
|
||||||
case "unix":
|
|
||||||
return ["All"];
|
|
||||||
case "windows":
|
|
||||||
return ["x64"];
|
|
||||||
case "docker":
|
|
||||||
return ["Docker Compose", "Docker Run"];
|
|
||||||
default:
|
|
||||||
return ["x64"];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatformName = (platformName: string) => {
|
|
||||||
switch (platformName) {
|
|
||||||
case "windows":
|
|
||||||
return "Windows";
|
|
||||||
case "unix":
|
|
||||||
return "Unix & macOS";
|
|
||||||
case "docker":
|
|
||||||
return "Docker";
|
|
||||||
default:
|
|
||||||
return "Unix & macOS";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCommand = (): CommandItem[] => {
|
|
||||||
const placeholder: CommandItem[] = [t("unknownCommand")];
|
|
||||||
if (!commands) {
|
|
||||||
return placeholder;
|
|
||||||
}
|
|
||||||
let platformCommands = commands[platform as keyof Commands];
|
|
||||||
|
|
||||||
if (!platformCommands) {
|
|
||||||
// get first key
|
|
||||||
const firstPlatform = Object.keys(commands)[0] as Platform;
|
|
||||||
platformCommands = commands[firstPlatform as keyof Commands];
|
|
||||||
|
|
||||||
setPlatform(firstPlatform);
|
|
||||||
}
|
|
||||||
|
|
||||||
let architectureCommands = platformCommands[architecture];
|
|
||||||
if (!architectureCommands) {
|
|
||||||
// get first key
|
|
||||||
const firstArchitecture = Object.keys(platformCommands)[0];
|
|
||||||
architectureCommands = platformCommands[firstArchitecture];
|
|
||||||
|
|
||||||
setArchitecture(firstArchitecture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return architectureCommands || placeholder;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatformIcon = (platformName: string) => {
|
|
||||||
switch (platformName) {
|
|
||||||
case "windows":
|
|
||||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
|
||||||
case "unix":
|
|
||||||
return <Terminal className="h-4 w-4 mr-2" />;
|
|
||||||
case "docker":
|
|
||||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
|
||||||
case "kubernetes":
|
|
||||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
|
||||||
case "podman":
|
|
||||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
|
||||||
case "freebsd":
|
|
||||||
return <FaFreebsd className="h-4 w-4 mr-2" />;
|
|
||||||
case "nixos":
|
|
||||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
|
||||||
default:
|
|
||||||
return <Terminal className="h-4 w-4 mr-2" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm<CreateClientFormValues>({
|
const form = useForm<CreateClientFormValues>({
|
||||||
resolver: zodResolver(createClientFormSchema),
|
resolver: zodResolver(createClientFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -311,23 +152,6 @@ export default function Page() {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoadingPage(true);
|
setLoadingPage(true);
|
||||||
|
|
||||||
// Fetch available sites
|
|
||||||
|
|
||||||
// const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
|
||||||
// `/org/${orgId}/sites/`
|
|
||||||
// );
|
|
||||||
// const sites = res.data.data.sites.filter(
|
|
||||||
// (s) => s.type === "newt" && s.subnet
|
|
||||||
// );
|
|
||||||
// setSites(
|
|
||||||
// sites.map((site) => ({
|
|
||||||
// id: site.siteId.toString(),
|
|
||||||
// text: site.name
|
|
||||||
// }))
|
|
||||||
// );
|
|
||||||
|
|
||||||
let olmVersion = "latest";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||||
@@ -348,7 +172,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const latestVersion = data.tag_name;
|
const latestVersion = data.tag_name;
|
||||||
olmVersion = latestVersion;
|
setOlmVersion(latestVersion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "AbortError") {
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
console.error(t("olmErrorFetchTimeout"));
|
console.error(t("olmErrorFetchTimeout"));
|
||||||
@@ -377,18 +201,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const olmId = data.olmId;
|
const olmId = data.olmId;
|
||||||
const olmSecret = data.olmSecret;
|
const olmSecret = data.olmSecret;
|
||||||
const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`;
|
|
||||||
|
|
||||||
setOlmId(olmId);
|
setOlmId(olmId);
|
||||||
setOlmSecret(olmSecret);
|
setOlmSecret(olmSecret);
|
||||||
setOlmCommand(olmCommand);
|
|
||||||
|
|
||||||
hydrateCommands(
|
|
||||||
olmId,
|
|
||||||
olmSecret,
|
|
||||||
env.app.dashboardUrl,
|
|
||||||
olmVersion
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.subnet) {
|
if (data.subnet) {
|
||||||
form.setValue("subnet", data.subnet);
|
form.setValue("subnet", data.subnet);
|
||||||
@@ -571,118 +386,12 @@ export default function Page() {
|
|||||||
</InfoSections>
|
</InfoSections>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<SettingsSection>
|
<OlmInstallCommands
|
||||||
<SettingsSectionHeader>
|
id={olmId}
|
||||||
<SettingsSectionTitle>
|
endpoint={env.app.dashboardUrl}
|
||||||
{t("clientInstallOlm")}
|
secret={olmSecret}
|
||||||
</SettingsSectionTitle>
|
version={olmVersion}
|
||||||
<SettingsSectionDescription>
|
/>
|
||||||
{t("clientInstallOlmDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<div>
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{t("operatingSystem")}
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
||||||
{platforms.map((os) => (
|
|
||||||
<Button
|
|
||||||
key={os}
|
|
||||||
variant={
|
|
||||||
platform === os
|
|
||||||
? "squareOutlinePrimary"
|
|
||||||
: "squareOutline"
|
|
||||||
}
|
|
||||||
className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
|
||||||
onClick={() => {
|
|
||||||
setPlatform(os);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getPlatformIcon(os)}
|
|
||||||
{getPlatformName(os)}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{["docker", "podman"].includes(
|
|
||||||
platform
|
|
||||||
)
|
|
||||||
? t("method")
|
|
||||||
: t("architecture")}
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
||||||
{getArchitectures().map(
|
|
||||||
(arch) => (
|
|
||||||
<Button
|
|
||||||
key={arch}
|
|
||||||
variant={
|
|
||||||
architecture ===
|
|
||||||
arch
|
|
||||||
? "squareOutlinePrimary"
|
|
||||||
: "squareOutline"
|
|
||||||
}
|
|
||||||
className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
|
||||||
onClick={() =>
|
|
||||||
setArchitecture(
|
|
||||||
arch
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{arch}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="pt-4">
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{t("commands")}
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 space-y-3">
|
|
||||||
{getCommand().map(
|
|
||||||
(item, index) => {
|
|
||||||
const commandText =
|
|
||||||
typeof item ===
|
|
||||||
"string"
|
|
||||||
? item
|
|
||||||
: item.command;
|
|
||||||
const title =
|
|
||||||
typeof item ===
|
|
||||||
"string"
|
|
||||||
? undefined
|
|
||||||
: item.title;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{title && (
|
|
||||||
<p className="text-sm font-medium mb-1.5">
|
|
||||||
{
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CopyTextBox
|
|
||||||
text={
|
|
||||||
commandText
|
|
||||||
}
|
|
||||||
outline={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|||||||
@@ -656,17 +656,17 @@ export default function GeneralPage() {
|
|||||||
</InfoSection>
|
</InfoSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{client.posture.windowsDefenderEnabled !== null &&
|
{client.posture.windowsAntivirusEnabled !== null &&
|
||||||
client.posture.windowsDefenderEnabled !== undefined && (
|
client.posture.windowsAntivirusEnabled !== undefined && (
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("windowsDefenderEnabled")}
|
{t("windowsAntivirusEnabled")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{isPaidUser
|
{isPaidUser
|
||||||
? formatPostureValue(
|
? formatPostureValue(
|
||||||
client.posture
|
client.posture
|
||||||
.windowsDefenderEnabled
|
.windowsAntivirusEnabled
|
||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
|
|||||||
@@ -265,4 +265,3 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default async function ClientResourcesPage(
|
|||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
// destinationPort: siteResource.destinationPort,
|
// destinationPort: siteResource.destinationPort,
|
||||||
alias: siteResource.alias || null,
|
alias: siteResource.alias || null,
|
||||||
|
aliasAddress: siteResource.aliasAddress || null,
|
||||||
siteNiceId: siteResource.siteNiceId,
|
siteNiceId: siteResource.siteNiceId,
|
||||||
niceId: siteResource.niceId,
|
niceId: siteResource.niceId,
|
||||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { ContainersSelector } from "@app/components/ContainersSelector";
|
|
||||||
import { HeadersInput } from "@app/components/HeadersInput";
|
import { HeadersInput } from "@app/components/HeadersInput";
|
||||||
import {
|
import {
|
||||||
PathMatchDisplay,
|
PathMatchDisplay,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
PathRewriteDisplay,
|
PathRewriteDisplay,
|
||||||
PathRewriteModal
|
PathRewriteModal
|
||||||
} from "@app/components/PathMatchRenameModal";
|
} from "@app/components/PathMatchRenameModal";
|
||||||
|
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
@@ -30,15 +30,6 @@ import {
|
|||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList
|
|
||||||
} from "@app/components/ui/command";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -48,11 +39,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger
|
|
||||||
} from "@app/components/ui/popover";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -73,12 +59,9 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
|
||||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
|
||||||
import { tlsNameSchema } from "@server/lib/schemas";
|
import { tlsNameSchema } from "@server/lib/schemas";
|
||||||
import { type GetResourceResponse } from "@server/routers/resource";
|
import { type GetResourceResponse } from "@server/routers/resource";
|
||||||
import type { ListSitesResponse } from "@server/routers/site";
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
@@ -98,7 +81,6 @@ import {
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckIcon,
|
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
Info,
|
Info,
|
||||||
@@ -107,7 +89,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { use, useActionState, useEffect, useState } from "react";
|
import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -202,7 +184,7 @@ function ProxyResourceTargetsForm({
|
|||||||
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshContainersForSite = async (siteId: number) => {
|
const refreshContainersForSite = useCallback(async (siteId: number) => {
|
||||||
const dockerManager = new DockerManager(api, siteId);
|
const dockerManager = new DockerManager(api, siteId);
|
||||||
const containers = await dockerManager.fetchContainers();
|
const containers = await dockerManager.fetchContainers();
|
||||||
|
|
||||||
@@ -214,9 +196,9 @@ function ProxyResourceTargetsForm({
|
|||||||
}
|
}
|
||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
};
|
}, [api]);
|
||||||
|
|
||||||
const getDockerStateForSite = (siteId: number): DockerState => {
|
const getDockerStateForSite = useCallback((siteId: number): DockerState => {
|
||||||
return (
|
return (
|
||||||
dockerStates.get(siteId) || {
|
dockerStates.get(siteId) || {
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
@@ -224,7 +206,7 @@ function ProxyResourceTargetsForm({
|
|||||||
containers: []
|
containers: []
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
}, [dockerStates]);
|
||||||
|
|
||||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -234,8 +216,40 @@ function ProxyResourceTargetsForm({
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
const isHttp = resource.http;
|
||||||
const isHttp = resource.http;
|
|
||||||
|
const removeTarget = useCallback((targetId: number) => {
|
||||||
|
setTargets((prevTargets) => {
|
||||||
|
const targetToRemove = prevTargets.find((target) => target.targetId === targetId);
|
||||||
|
if (targetToRemove && !targetToRemove.new) {
|
||||||
|
setTargetsToRemove((prev) => [...prev, targetId]);
|
||||||
|
}
|
||||||
|
return prevTargets.filter((target) => target.targetId !== targetId);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateTarget = useCallback((targetId: number, data: Partial<LocalTarget>) => {
|
||||||
|
setTargets((prevTargets) => {
|
||||||
|
const site = sites.find((site) => site.siteId === data.siteId);
|
||||||
|
return prevTargets.map((target) =>
|
||||||
|
target.targetId === targetId
|
||||||
|
? {
|
||||||
|
...target,
|
||||||
|
...data,
|
||||||
|
updated: true,
|
||||||
|
siteType: site ? site.type : target.siteType
|
||||||
|
}
|
||||||
|
: target
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [sites]);
|
||||||
|
|
||||||
|
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
||||||
|
setSelectedTargetForHealthCheck(target);
|
||||||
|
setHealthCheckDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
||||||
|
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "priority",
|
id: "priority",
|
||||||
@@ -419,213 +433,15 @@ function ProxyResourceTargetsForm({
|
|||||||
accessorKey: "address",
|
accessorKey: "address",
|
||||||
header: () => <span className="p-3">{t("address")}</span>,
|
header: () => <span className="p-3">{t("address")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const selectedSite = sites.find(
|
|
||||||
(site) => site.siteId === row.original.siteId
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContainerSelectForTarget = (
|
|
||||||
hostname: string,
|
|
||||||
port?: number
|
|
||||||
) => {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
ip: hostname,
|
|
||||||
...(port && { port: port })
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full">
|
<ResourceTargetAddressItem
|
||||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
|
isHttp={isHttp}
|
||||||
{selectedSite &&
|
sites={sites}
|
||||||
selectedSite.type === "newt" &&
|
getDockerStateForSite={getDockerStateForSite}
|
||||||
(() => {
|
proxyTarget={row.original}
|
||||||
const dockerState = getDockerStateForSite(
|
refreshContainersForSite={refreshContainersForSite}
|
||||||
selectedSite.siteId
|
updateTarget={updateTarget}
|
||||||
);
|
/>
|
||||||
return (
|
|
||||||
<ContainersSelector
|
|
||||||
site={selectedSite}
|
|
||||||
containers={dockerState.containers}
|
|
||||||
isAvailable={
|
|
||||||
dockerState.isAvailable
|
|
||||||
}
|
|
||||||
onContainerSelect={
|
|
||||||
handleContainerSelectForTarget
|
|
||||||
}
|
|
||||||
onRefresh={() =>
|
|
||||||
refreshContainersForSite(
|
|
||||||
selectedSite.siteId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
|
||||||
!row.original.siteId &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="truncate max-w-[150px]">
|
|
||||||
{row.original.siteId
|
|
||||||
? selectedSite?.name
|
|
||||||
: t("siteSelect")}
|
|
||||||
</span>
|
|
||||||
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0 w-[180px]">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder={t("siteSearch")}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
{t("siteNotFound")}
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map((site) => (
|
|
||||||
<CommandItem
|
|
||||||
key={site.siteId}
|
|
||||||
value={`${site.siteId}:${site.name}`}
|
|
||||||
onSelect={() =>
|
|
||||||
updateTarget(
|
|
||||||
row.original
|
|
||||||
.targetId,
|
|
||||||
{
|
|
||||||
siteId: site.siteId
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
row.original
|
|
||||||
.siteId
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{site.name}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{resource.http && (
|
|
||||||
<Select
|
|
||||||
defaultValue={row.original.method ?? "http"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
method: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 px-2 w-[70px] text-sm font-normal border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
|
|
||||||
{row.original.method || "http"}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="http">
|
|
||||||
http
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https">
|
|
||||||
https
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="h2c">h2c</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{resource.http && (
|
|
||||||
<div className="flex items-center justify-center px-2 h-9">
|
|
||||||
{"://"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
defaultValue={row.original.ip}
|
|
||||||
placeholder="Host"
|
|
||||||
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const input = e.target.value.trim();
|
|
||||||
const hasProtocol =
|
|
||||||
/^(https?|h2c):\/\//.test(input);
|
|
||||||
const hasPort = /:\d+(?:\/|$)/.test(input);
|
|
||||||
|
|
||||||
if (hasProtocol || hasPort) {
|
|
||||||
const parsed = parseHostTarget(input);
|
|
||||||
if (parsed) {
|
|
||||||
updateTarget(
|
|
||||||
row.original.targetId,
|
|
||||||
{
|
|
||||||
...row.original,
|
|
||||||
method: hasProtocol
|
|
||||||
? parsed.protocol
|
|
||||||
: row.original.method,
|
|
||||||
ip: parsed.host,
|
|
||||||
port: hasPort
|
|
||||||
? parsed.port
|
|
||||||
: row.original.port
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateTarget(
|
|
||||||
row.original.targetId,
|
|
||||||
{
|
|
||||||
...row.original,
|
|
||||||
ip: input
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
ip: input
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-center px-2 h-9">
|
|
||||||
{":"}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="Port"
|
|
||||||
defaultValue={
|
|
||||||
row.original.port === 0
|
|
||||||
? ""
|
|
||||||
: row.original.port
|
|
||||||
}
|
|
||||||
className="w-[75px] pl-0 border-none placeholder-gray-400"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
if (!isNaN(value) && value > 0) {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: value
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
size: 400,
|
size: 400,
|
||||||
@@ -765,7 +581,7 @@ function ProxyResourceTargetsForm({
|
|||||||
actionsColumn
|
actionsColumn
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
}, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]);
|
||||||
|
|
||||||
function addNewTarget() {
|
function addNewTarget() {
|
||||||
const isHttp = resource.http;
|
const isHttp = resource.http;
|
||||||
@@ -806,32 +622,6 @@ function ProxyResourceTargetsForm({
|
|||||||
setTargets((prev) => [...prev, newTarget]);
|
setTargets((prev) => [...prev, newTarget]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTarget = (targetId: number) => {
|
|
||||||
setTargets([
|
|
||||||
...targets.filter((target) => target.targetId !== targetId)
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!targets.find((target) => target.targetId === targetId)?.new) {
|
|
||||||
setTargetsToRemove([...targetsToRemove, targetId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
|
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
|
||||||
setTargets(
|
|
||||||
targets.map((target) =>
|
|
||||||
target.targetId === targetId
|
|
||||||
? {
|
|
||||||
...target,
|
|
||||||
...data,
|
|
||||||
updated: true,
|
|
||||||
siteType: site ? site.type : target.siteType
|
|
||||||
}
|
|
||||||
: target
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTargetHealthCheck(targetId: number, config: any) {
|
function updateTargetHealthCheck(targetId: number, config: any) {
|
||||||
setTargets(
|
setTargets(
|
||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
@@ -846,14 +636,6 @@ function ProxyResourceTargetsForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openHealthCheckDialog = (target: LocalTarget) => {
|
|
||||||
console.log(target);
|
|
||||||
setSelectedTargetForHealthCheck(target);
|
|
||||||
setHealthCheckDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = getColumns();
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: targets,
|
data: targets,
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
|
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
||||||
|
import {
|
||||||
|
PathMatchDisplay,
|
||||||
|
PathMatchModal,
|
||||||
|
PathRewriteDisplay,
|
||||||
|
PathRewriteModal
|
||||||
|
} from "@app/components/PathMatchRenameModal";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
@@ -9,6 +18,10 @@ import {
|
|||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
|
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -18,22 +31,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
|
||||||
import { createApiClient } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { Resource } from "@server/db";
|
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -41,48 +39,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList
|
|
||||||
} from "@app/components/ui/command";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger
|
|
||||||
} from "@app/components/ui/popover";
|
|
||||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import {
|
|
||||||
ArrowRight,
|
|
||||||
CircleCheck,
|
|
||||||
CircleX,
|
|
||||||
Info,
|
|
||||||
MoveRight,
|
|
||||||
Plus,
|
|
||||||
Settings,
|
|
||||||
SquareArrowOutUpRight
|
|
||||||
} from "lucide-react";
|
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
import { ContainersSelector } from "@app/components/ContainersSelector";
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
flexRender,
|
|
||||||
Row
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -91,30 +48,49 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow
|
||||||
} from "@app/components/ui/table";
|
} from "@app/components/ui/table";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
|
||||||
import { ArrayElement } from "@server/types/ArrayElement";
|
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
|
||||||
import { ListTargetsResponse } from "@server/routers/target";
|
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
|
||||||
import { toASCII, toUnicode } from "punycode";
|
|
||||||
import { DomainRow } from "@app/components/DomainsTable";
|
|
||||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Resource } from "@server/db";
|
||||||
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
PathMatchDisplay,
|
ColumnDef,
|
||||||
PathMatchModal,
|
flexRender,
|
||||||
PathRewriteDisplay,
|
getCoreRowModel,
|
||||||
PathRewriteModal
|
getFilteredRowModel,
|
||||||
} from "@app/components/PathMatchRenameModal";
|
getPaginationRowModel,
|
||||||
import { Badge } from "@app/components/ui/badge";
|
getSortedRowModel,
|
||||||
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
useReactTable
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
} from "@tanstack/react-table";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import {
|
||||||
|
CircleCheck,
|
||||||
|
CircleX,
|
||||||
|
Info,
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
SquareArrowOutUpRight
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { toASCII } from "punycode";
|
||||||
|
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
@@ -204,10 +180,6 @@ const addTargetSchema = z
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
|
||||||
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
|
|
||||||
type TcpUdpResourceFormValues = z.infer<typeof tcpUdpResourceFormSchema>;
|
|
||||||
|
|
||||||
type ResourceType = "http" | "raw";
|
type ResourceType = "http" | "raw";
|
||||||
|
|
||||||
interface ResourceTypeOption {
|
interface ResourceTypeOption {
|
||||||
@@ -217,7 +189,7 @@ interface ResourceTypeOption {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalTarget = Omit<
|
export type LocalTarget = Omit<
|
||||||
ArrayElement<ListTargetsResponse["targets"]> & {
|
ArrayElement<ListTargetsResponse["targets"]> & {
|
||||||
new?: boolean;
|
new?: boolean;
|
||||||
updated?: boolean;
|
updated?: boolean;
|
||||||
@@ -233,18 +205,16 @@ export default function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
const { data: sites = [], isLoading: loadingPage } = useQuery(
|
||||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
orgQueries.sites({ orgId: orgId as string })
|
||||||
const [baseDomains, setBaseDomains] = useState<
|
);
|
||||||
{ domainId: string; baseDomain: string }[]
|
|
||||||
>([]);
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [niceId, setNiceId] = useState<string>("");
|
const [niceId, setNiceId] = useState<string>("");
|
||||||
|
|
||||||
// Target management state
|
// Target management state
|
||||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
|
||||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||||
new Map()
|
new Map()
|
||||||
);
|
);
|
||||||
@@ -405,102 +375,60 @@ export default function Page() {
|
|||||||
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshContainersForSite = async (siteId: number) => {
|
const refreshContainersForSite = useCallback(
|
||||||
const dockerManager = new DockerManager(api, siteId);
|
async (siteId: number) => {
|
||||||
const containers = await dockerManager.fetchContainers();
|
const dockerManager = new DockerManager(api, siteId);
|
||||||
|
const containers = await dockerManager.fetchContainers();
|
||||||
|
|
||||||
setDockerStates((prev) => {
|
setDockerStates((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
const existingState = newMap.get(siteId);
|
const existingState = newMap.get(siteId);
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
newMap.set(siteId, { ...existingState, containers });
|
newMap.set(siteId, { ...existingState, containers });
|
||||||
}
|
}
|
||||||
return newMap;
|
return newMap;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[api]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDockerStateForSite = useCallback(
|
||||||
|
(siteId: number): DockerState => {
|
||||||
|
return (
|
||||||
|
dockerStates.get(siteId) || {
|
||||||
|
isEnabled: false,
|
||||||
|
isAvailable: false,
|
||||||
|
containers: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dockerStates]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeTarget = useCallback((targetId: number) => {
|
||||||
|
setTargets((prevTargets) => {
|
||||||
|
return prevTargets.filter((target) => target.targetId !== targetId);
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getDockerStateForSite = (siteId: number): DockerState => {
|
const updateTarget = useCallback(
|
||||||
return (
|
(targetId: number, data: Partial<LocalTarget>) => {
|
||||||
dockerStates.get(siteId) || {
|
setTargets((prevTargets) => {
|
||||||
isEnabled: false,
|
const site = sites.find((site) => site.siteId === data.siteId);
|
||||||
isAvailable: false,
|
return prevTargets.map((target) =>
|
||||||
containers: []
|
target.targetId === targetId
|
||||||
}
|
? {
|
||||||
);
|
...target,
|
||||||
};
|
...data,
|
||||||
|
updated: true,
|
||||||
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
siteType: site ? site.type : target.siteType
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
}
|
||||||
|
: target
|
||||||
const isHttp = baseForm.watch("http");
|
);
|
||||||
|
});
|
||||||
const newTarget: LocalTarget = {
|
},
|
||||||
...data,
|
[sites]
|
||||||
path: isHttp ? data.path || null : null,
|
);
|
||||||
pathMatchType: isHttp ? data.pathMatchType || null : null,
|
|
||||||
rewritePath: isHttp ? data.rewritePath || null : null,
|
|
||||||
rewritePathType: isHttp ? data.rewritePathType || null : null,
|
|
||||||
siteType: site?.type || null,
|
|
||||||
enabled: true,
|
|
||||||
targetId: new Date().getTime(),
|
|
||||||
new: true,
|
|
||||||
resourceId: 0, // Will be set when resource is created
|
|
||||||
priority: isHttp ? data.priority || 100 : 100, // Default priority
|
|
||||||
hcEnabled: false,
|
|
||||||
hcPath: null,
|
|
||||||
hcMethod: null,
|
|
||||||
hcInterval: null,
|
|
||||||
hcTimeout: null,
|
|
||||||
hcHeaders: null,
|
|
||||||
hcScheme: null,
|
|
||||||
hcHostname: null,
|
|
||||||
hcPort: null,
|
|
||||||
hcFollowRedirects: null,
|
|
||||||
hcHealth: "unknown",
|
|
||||||
hcStatus: null,
|
|
||||||
hcMode: null,
|
|
||||||
hcUnhealthyInterval: null,
|
|
||||||
hcTlsServerName: null
|
|
||||||
};
|
|
||||||
|
|
||||||
setTargets([...targets, newTarget]);
|
|
||||||
addTargetForm.reset({
|
|
||||||
ip: "",
|
|
||||||
method: baseForm.watch("http") ? "http" : null,
|
|
||||||
port: "" as any as number,
|
|
||||||
path: null,
|
|
||||||
pathMatchType: null,
|
|
||||||
rewritePath: null,
|
|
||||||
rewritePathType: null,
|
|
||||||
priority: isHttp ? 100 : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeTarget = (targetId: number) => {
|
|
||||||
setTargets([
|
|
||||||
...targets.filter((target) => target.targetId !== targetId)
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!targets.find((target) => target.targetId === targetId)?.new) {
|
|
||||||
setTargetsToRemove([...targetsToRemove, targetId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
|
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
|
||||||
setTargets(
|
|
||||||
targets.map((target) =>
|
|
||||||
target.targetId === targetId
|
|
||||||
? {
|
|
||||||
...target,
|
|
||||||
...data,
|
|
||||||
updated: true,
|
|
||||||
siteType: site ? site.type : target.siteType
|
|
||||||
}
|
|
||||||
: target
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
setCreateLoading(true);
|
setCreateLoading(true);
|
||||||
@@ -638,82 +566,18 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
// Initialize Docker for newt sites
|
||||||
setLoadingPage(true);
|
for (const site of sites) {
|
||||||
|
if (site.type === "newt") {
|
||||||
|
initializeDockerForSite(site.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchSites = async () => {
|
// If there's at least one site, set it as the default in the form
|
||||||
const res = await api
|
if (sites.length > 0) {
|
||||||
.get<
|
addTargetForm.setValue("siteId", sites[0].siteId);
|
||||||
AxiosResponse<ListSitesResponse>
|
}
|
||||||
>(`/org/${orgId}/sites/`)
|
}, [sites]);
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("sitesErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("sitesErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
setSites(res.data.data.sites);
|
|
||||||
|
|
||||||
// Initialize Docker for newt sites
|
|
||||||
for (const site of res.data.data.sites) {
|
|
||||||
if (site.type === "newt") {
|
|
||||||
initializeDockerForSite(site.siteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's only one site, set it as the default in the form
|
|
||||||
if (res.data.data.sites.length) {
|
|
||||||
addTargetForm.setValue(
|
|
||||||
"siteId",
|
|
||||||
res.data.data.sites[0].siteId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchDomains = async () => {
|
|
||||||
const res = await api
|
|
||||||
.get<
|
|
||||||
AxiosResponse<ListDomainsResponse>
|
|
||||||
>(`/org/${orgId}/domains/`)
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("domainsErrorFetch"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("domainsErrorFetchDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
const rawDomains = res.data.data.domains as DomainRow[];
|
|
||||||
const domains = rawDomains.map((domain) => ({
|
|
||||||
...domain,
|
|
||||||
baseDomain: toUnicode(domain.baseDomain)
|
|
||||||
}));
|
|
||||||
setBaseDomains(domains);
|
|
||||||
// if (domains.length) {
|
|
||||||
// httpForm.setValue("domainId", domains[0].domainId);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetchSites();
|
|
||||||
await fetchDomains();
|
|
||||||
|
|
||||||
setLoadingPage(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function TargetHealthCheck(targetId: number, config: any) {
|
function TargetHealthCheck(targetId: number, config: any) {
|
||||||
setTargets(
|
setTargets(
|
||||||
@@ -729,16 +593,15 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openHealthCheckDialog = (target: LocalTarget) => {
|
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
||||||
console.log(target);
|
console.log(target);
|
||||||
setSelectedTargetForHealthCheck(target);
|
setSelectedTargetForHealthCheck(target);
|
||||||
setHealthCheckDialogOpen(true);
|
setHealthCheckDialogOpen(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
const isHttp = baseForm.watch("http");
|
||||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
|
||||||
const isHttp = baseForm.watch("http");
|
|
||||||
|
|
||||||
|
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "priority",
|
id: "priority",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -875,7 +738,7 @@ export default function Page() {
|
|||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-[200px]"
|
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-50"
|
||||||
>
|
>
|
||||||
<PathMatchDisplay
|
<PathMatchDisplay
|
||||||
value={{
|
value={{
|
||||||
@@ -899,7 +762,7 @@ export default function Page() {
|
|||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-50"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t("matchPath")}
|
{t("matchPath")}
|
||||||
@@ -918,216 +781,16 @@ export default function Page() {
|
|||||||
const addressColumn: ColumnDef<LocalTarget> = {
|
const addressColumn: ColumnDef<LocalTarget> = {
|
||||||
accessorKey: "address",
|
accessorKey: "address",
|
||||||
header: () => <span className="p-3">{t("address")}</span>,
|
header: () => <span className="p-3">{t("address")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const selectedSite = sites.find(
|
<ResourceTargetAddressItem
|
||||||
(site) => site.siteId === row.original.siteId
|
isHttp={isHttp}
|
||||||
);
|
sites={sites}
|
||||||
|
getDockerStateForSite={getDockerStateForSite}
|
||||||
const handleContainerSelectForTarget = (
|
proxyTarget={row.original}
|
||||||
hostname: string,
|
refreshContainersForSite={refreshContainersForSite}
|
||||||
port?: number
|
updateTarget={updateTarget}
|
||||||
) => {
|
/>
|
||||||
updateTarget(row.original.targetId, {
|
),
|
||||||
...row.original,
|
|
||||||
ip: hostname,
|
|
||||||
...(port && { port: port })
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center w-full">
|
|
||||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
|
|
||||||
{selectedSite &&
|
|
||||||
selectedSite.type === "newt" &&
|
|
||||||
(() => {
|
|
||||||
const dockerState = getDockerStateForSite(
|
|
||||||
selectedSite.siteId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<ContainersSelector
|
|
||||||
site={selectedSite}
|
|
||||||
containers={dockerState.containers}
|
|
||||||
isAvailable={
|
|
||||||
dockerState.isAvailable
|
|
||||||
}
|
|
||||||
onContainerSelect={
|
|
||||||
handleContainerSelectForTarget
|
|
||||||
}
|
|
||||||
onRefresh={() =>
|
|
||||||
refreshContainersForSite(
|
|
||||||
selectedSite.siteId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
|
||||||
!row.original.siteId &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="truncate max-w-[150px]">
|
|
||||||
{row.original.siteId
|
|
||||||
? selectedSite?.name
|
|
||||||
: t("siteSelect")}
|
|
||||||
</span>
|
|
||||||
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0 w-[180px]">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder={t("siteSearch")}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
{t("siteNotFound")}
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map((site) => (
|
|
||||||
<CommandItem
|
|
||||||
key={site.siteId}
|
|
||||||
value={`${site.siteId}:${site.name}`}
|
|
||||||
onSelect={() =>
|
|
||||||
updateTarget(
|
|
||||||
row.original
|
|
||||||
.targetId,
|
|
||||||
{
|
|
||||||
siteId: site.siteId
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
row.original
|
|
||||||
.siteId
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{site.name}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{isHttp && (
|
|
||||||
<Select
|
|
||||||
defaultValue={row.original.method ?? "http"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
method: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 px-2 w-[70px] border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
|
|
||||||
{row.original.method || "http"}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="http">
|
|
||||||
http
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="https">
|
|
||||||
https
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="h2c">h2c</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isHttp && (
|
|
||||||
<div className="flex items-center justify-center px-2 h-9">
|
|
||||||
{"://"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Input
|
|
||||||
defaultValue={row.original.ip}
|
|
||||||
placeholder="Host"
|
|
||||||
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const input = e.target.value.trim();
|
|
||||||
const hasProtocol =
|
|
||||||
/^(https?|h2c):\/\//.test(input);
|
|
||||||
const hasPort = /:\d+(?:\/|$)/.test(input);
|
|
||||||
|
|
||||||
if (hasProtocol || hasPort) {
|
|
||||||
const parsed = parseHostTarget(input);
|
|
||||||
if (parsed) {
|
|
||||||
updateTarget(
|
|
||||||
row.original.targetId,
|
|
||||||
{
|
|
||||||
...row.original,
|
|
||||||
method: hasProtocol
|
|
||||||
? parsed.protocol
|
|
||||||
: row.original.method,
|
|
||||||
ip: parsed.host,
|
|
||||||
port: hasPort
|
|
||||||
? parsed.port
|
|
||||||
: row.original.port
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateTarget(
|
|
||||||
row.original.targetId,
|
|
||||||
{
|
|
||||||
...row.original,
|
|
||||||
ip: input
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
ip: input
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-center px-2 h-9">
|
|
||||||
{":"}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="Port"
|
|
||||||
defaultValue={
|
|
||||||
row.original.port === 0
|
|
||||||
? ""
|
|
||||||
: row.original.port
|
|
||||||
}
|
|
||||||
className="w-[75px] pl-0 border-none placeholder-gray-400"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const value = parseInt(e.target.value, 10);
|
|
||||||
if (!isNaN(value) && value > 0) {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: value
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
size: 400,
|
size: 400,
|
||||||
minSize: 350,
|
minSize: 350,
|
||||||
maxSize: 500
|
maxSize: 500
|
||||||
@@ -1186,7 +849,7 @@ export default function Page() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={noPathMatch}
|
disabled={noPathMatch}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-50"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t("rewritePath")}
|
{t("rewritePath")}
|
||||||
@@ -1265,9 +928,17 @@ export default function Page() {
|
|||||||
actionsColumn
|
actionsColumn
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
isAdvancedMode,
|
||||||
const columns = getColumns();
|
isHttp,
|
||||||
|
sites,
|
||||||
|
updateTarget,
|
||||||
|
getDockerStateForSite,
|
||||||
|
refreshContainersForSite,
|
||||||
|
openHealthCheckDialog,
|
||||||
|
removeTarget,
|
||||||
|
t
|
||||||
|
]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: targets,
|
data: targets,
|
||||||
@@ -1649,9 +1320,6 @@ export default function Page() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
{/* <TableCaption> */}
|
|
||||||
{/* {t('targetNoOneDescription')} */}
|
|
||||||
{/* </TableCaption> */}
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
} from "@app/lib/wireguard";
|
} from "@app/lib/wireguard";
|
||||||
import { QRCodeCanvas } from "qrcode.react";
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
|
||||||
|
|
||||||
export default function CredentialsPage() {
|
export default function CredentialsPage() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -186,107 +187,119 @@ export default function CredentialsPage() {
|
|||||||
return site?.name || site?.niceId || "My site";
|
return site?.name || site?.niceId || "My site";
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayNewtId = currentNewtId || siteDefaults?.newtId || null;
|
const displayNewtId = currentNewtId ?? siteDefaults?.newtId ?? null;
|
||||||
const displaySecret = regeneratedSecret || null;
|
const displaySecret = regeneratedSecret ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{site?.type === "newt" && (
|
{site?.type === "newt" && (
|
||||||
<SettingsSection>
|
<>
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t("siteNewtCredentials")}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t("siteNewtCredentials")}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t("siteNewtCredentialsDescription")}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t("siteNewtCredentialsDescription")}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<InfoSections cols={3}>
|
<InfoSections cols={3}>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("newtEndpoint")}
|
{t("newtEndpoint")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<CopyToClipboard
|
|
||||||
text={env.app.dashboardUrl}
|
|
||||||
/>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>
|
|
||||||
{t("newtId")}
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
{displayNewtId ? (
|
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={displayNewtId}
|
text={env.app.dashboardUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
</InfoSectionContent>
|
||||||
<span>{"••••••••••••••••"}</span>
|
</InfoSection>
|
||||||
)}
|
<InfoSection>
|
||||||
</InfoSectionContent>
|
<InfoSectionTitle>
|
||||||
</InfoSection>
|
{t("newtId")}
|
||||||
<InfoSection>
|
</InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
{t("newtSecretKey")}
|
{displayNewtId ? (
|
||||||
</InfoSectionTitle>
|
<CopyToClipboard
|
||||||
<InfoSectionContent>
|
text={displayNewtId}
|
||||||
{displaySecret ? (
|
/>
|
||||||
<CopyToClipboard
|
) : (
|
||||||
text={displaySecret}
|
<span>
|
||||||
/>
|
{"••••••••••••••••"}
|
||||||
) : (
|
</span>
|
||||||
<span>
|
)}
|
||||||
{
|
</InfoSectionContent>
|
||||||
"••••••••••••••••••••••••••••••••"
|
</InfoSection>
|
||||||
}
|
<InfoSection>
|
||||||
</span>
|
<InfoSectionTitle>
|
||||||
)}
|
{t("newtSecretKey")}
|
||||||
</InfoSectionContent>
|
</InfoSectionTitle>
|
||||||
</InfoSection>
|
<InfoSectionContent>
|
||||||
</InfoSections>
|
{displaySecret ? (
|
||||||
|
<CopyToClipboard
|
||||||
|
text={displaySecret}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
"••••••••••••••••••••••••••••••••"
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
|
||||||
{showCredentialsAlert && displaySecret && (
|
{showCredentialsAlert && displaySecret && (
|
||||||
<Alert variant="neutral" className="mt-4">
|
<Alert variant="neutral" className="mt-4">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">
|
<AlertTitle className="font-semibold">
|
||||||
{t("siteCredentialsSave")}
|
{t("siteCredentialsSave")}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t("siteCredentialsSaveDescription")}
|
{t(
|
||||||
</AlertDescription>
|
"siteCredentialsSaveDescription"
|
||||||
</Alert>
|
)}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</SettingsSectionBody>
|
||||||
|
{build !== "oss" && (
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShouldDisconnect(false);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
disabled={isSecurityFeatureDisabled()}
|
||||||
|
>
|
||||||
|
{t("regenerateCredentialsButton")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShouldDisconnect(true);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
disabled={isSecurityFeatureDisabled()}
|
||||||
|
>
|
||||||
|
{t("siteRegenerateAndDisconnect")}
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSection>
|
||||||
{build !== "oss" && (
|
|
||||||
<SettingsSectionFooter>
|
<NewtSiteInstallCommands
|
||||||
<Button
|
id={displayNewtId ?? "**********"}
|
||||||
variant="outline"
|
secret={displaySecret ?? "**************"}
|
||||||
onClick={() => {
|
endpoint={env.app.dashboardUrl}
|
||||||
setShouldDisconnect(false);
|
/>
|
||||||
setModalOpen(true);
|
</>
|
||||||
}}
|
|
||||||
disabled={isSecurityFeatureDisabled()}
|
|
||||||
>
|
|
||||||
{t("regenerateCredentialsButton")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShouldDisconnect(true);
|
|
||||||
setModalOpen(true);
|
|
||||||
}}
|
|
||||||
disabled={isSecurityFeatureDisabled()}
|
|
||||||
>
|
|
||||||
{t("siteRegenerateAndDisconnect")}
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
)}
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{site?.type === "wireguard" && (
|
{site?.type === "wireguard" && (
|
||||||
@@ -306,7 +319,7 @@ export default function CredentialsPage() {
|
|||||||
{!loadingDefaults && (
|
{!loadingDefaults && (
|
||||||
<>
|
<>
|
||||||
{wgConfig ? (
|
{wgConfig ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={wgConfig}
|
text={wgConfig}
|
||||||
outline={true}
|
outline={true}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ import { QRCodeCanvas } from "qrcode.react";
|
|||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
|
||||||
|
import { id } from "date-fns/locale";
|
||||||
|
|
||||||
type SiteType = "newt" | "wireguard" | "local";
|
type SiteType = "newt" | "wireguard" | "local";
|
||||||
|
|
||||||
@@ -189,10 +191,6 @@ export default function Page() {
|
|||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
|
|
||||||
const [platform, setPlatform] = useState<Platform>("unix");
|
|
||||||
const [architecture, setArchitecture] = useState("amd64");
|
|
||||||
const [commands, setCommands] = useState<Commands | null>(null);
|
|
||||||
|
|
||||||
const [newtId, setNewtId] = useState("");
|
const [newtId, setNewtId] = useState("");
|
||||||
const [newtSecret, setNewtSecret] = useState("");
|
const [newtSecret, setNewtSecret] = useState("");
|
||||||
const [newtEndpoint, setNewtEndpoint] = useState("");
|
const [newtEndpoint, setNewtEndpoint] = useState("");
|
||||||
@@ -202,7 +200,6 @@ export default function Page() {
|
|||||||
const [wgConfig, setWgConfig] = useState("");
|
const [wgConfig, setWgConfig] = useState("");
|
||||||
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [acceptClients, setAcceptClients] = useState(true);
|
|
||||||
const [newtVersion, setNewtVersion] = useState("latest");
|
const [newtVersion, setNewtVersion] = useState("latest");
|
||||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||||
|
|
||||||
@@ -216,187 +213,6 @@ export default function Page() {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const hydrateCommands = (
|
|
||||||
id: string,
|
|
||||||
secret: string,
|
|
||||||
endpoint: string,
|
|
||||||
version: string,
|
|
||||||
acceptClients: boolean = true
|
|
||||||
) => {
|
|
||||||
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
|
||||||
const acceptClientsEnv = !acceptClients
|
|
||||||
? "\n - DISABLE_CLIENTS=true"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const commands = {
|
|
||||||
unix: {
|
|
||||||
All: [
|
|
||||||
{
|
|
||||||
title: t("install"),
|
|
||||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("run"),
|
|
||||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
windows: {
|
|
||||||
x64: [
|
|
||||||
{
|
|
||||||
title: t("install"),
|
|
||||||
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("run"),
|
|
||||||
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
docker: {
|
|
||||||
"Docker Compose": [
|
|
||||||
`services:
|
|
||||||
newt:
|
|
||||||
image: fosrl/newt
|
|
||||||
container_name: newt
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- PANGOLIN_ENDPOINT=${endpoint}
|
|
||||||
- NEWT_ID=${id}
|
|
||||||
- NEWT_SECRET=${secret}${acceptClientsEnv}`
|
|
||||||
],
|
|
||||||
"Docker Run": [
|
|
||||||
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
kubernetes: {
|
|
||||||
"Helm Chart": [
|
|
||||||
`helm repo add fossorial https://charts.fossorial.io`,
|
|
||||||
`helm repo update fossorial`,
|
|
||||||
`helm install newt fossorial/newt \\
|
|
||||||
--create-namespace \\
|
|
||||||
--set newtInstances[0].name="main-tunnel" \\
|
|
||||||
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
|
||||||
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
|
||||||
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
podman: {
|
|
||||||
"Podman Quadlet": [
|
|
||||||
`[Unit]
|
|
||||||
Description=Newt container
|
|
||||||
|
|
||||||
[Container]
|
|
||||||
ContainerName=newt
|
|
||||||
Image=docker.io/fosrl/newt
|
|
||||||
Environment=PANGOLIN_ENDPOINT=${endpoint}
|
|
||||||
Environment=NEWT_ID=${id}
|
|
||||||
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
|
|
||||||
# Secret=newt-secret,type=env,target=NEWT_SECRET
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target`
|
|
||||||
],
|
|
||||||
"Podman Run": [
|
|
||||||
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
nixos: {
|
|
||||||
All: [
|
|
||||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setCommands(commands);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArchitectures = () => {
|
|
||||||
switch (platform) {
|
|
||||||
case "unix":
|
|
||||||
return ["All"];
|
|
||||||
case "windows":
|
|
||||||
return ["x64"];
|
|
||||||
case "docker":
|
|
||||||
return ["Docker Compose", "Docker Run"];
|
|
||||||
case "kubernetes":
|
|
||||||
return ["Helm Chart"];
|
|
||||||
case "podman":
|
|
||||||
return ["Podman Quadlet", "Podman Run"];
|
|
||||||
case "nixos":
|
|
||||||
return ["All"];
|
|
||||||
default:
|
|
||||||
return ["x64"];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatformName = (platformName: string) => {
|
|
||||||
switch (platformName) {
|
|
||||||
case "windows":
|
|
||||||
return "Windows";
|
|
||||||
case "unix":
|
|
||||||
return "Unix & macOS";
|
|
||||||
case "docker":
|
|
||||||
return "Docker";
|
|
||||||
case "kubernetes":
|
|
||||||
return "Kubernetes";
|
|
||||||
case "podman":
|
|
||||||
return "Podman";
|
|
||||||
case "nixos":
|
|
||||||
return "NixOS";
|
|
||||||
default:
|
|
||||||
return "Unix / macOS";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCommand = (): CommandItem[] => {
|
|
||||||
const placeholder: CommandItem[] = [t("unknownCommand")];
|
|
||||||
if (!commands) {
|
|
||||||
return placeholder;
|
|
||||||
}
|
|
||||||
let platformCommands = commands[platform as keyof Commands];
|
|
||||||
|
|
||||||
if (!platformCommands) {
|
|
||||||
// get first key
|
|
||||||
const firstPlatform = Object.keys(commands)[0] as Platform;
|
|
||||||
platformCommands = commands[firstPlatform as keyof Commands];
|
|
||||||
|
|
||||||
setPlatform(firstPlatform);
|
|
||||||
}
|
|
||||||
|
|
||||||
let architectureCommands = platformCommands[architecture];
|
|
||||||
if (!architectureCommands) {
|
|
||||||
// get first key
|
|
||||||
const firstArchitecture = Object.keys(platformCommands)[0];
|
|
||||||
architectureCommands = platformCommands[firstArchitecture];
|
|
||||||
|
|
||||||
setArchitecture(firstArchitecture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return architectureCommands || placeholder;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatformIcon = (platformName: string) => {
|
|
||||||
switch (platformName) {
|
|
||||||
case "windows":
|
|
||||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
|
||||||
case "unix":
|
|
||||||
return <Terminal className="h-4 w-4 mr-2" />;
|
|
||||||
case "docker":
|
|
||||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
|
||||||
case "kubernetes":
|
|
||||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
|
||||||
case "podman":
|
|
||||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
|
||||||
case "nixos":
|
|
||||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
|
||||||
default:
|
|
||||||
return <Terminal className="h-4 w-4 mr-2" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(createSiteFormSchema),
|
resolver: zodResolver(createSiteFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -414,7 +230,7 @@ WantedBy=default.target`
|
|||||||
|
|
||||||
let payload: CreateSiteBody = {
|
let payload: CreateSiteBody = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
type: data.method as "newt" | "wireguard" | "local"
|
type: data.method
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.method == "wireguard") {
|
if (data.method == "wireguard") {
|
||||||
@@ -568,14 +384,6 @@ WantedBy=default.target`
|
|||||||
setNewtEndpoint(newtEndpoint);
|
setNewtEndpoint(newtEndpoint);
|
||||||
setClientAddress(clientAddress);
|
setClientAddress(clientAddress);
|
||||||
|
|
||||||
hydrateCommands(
|
|
||||||
newtId,
|
|
||||||
newtSecret,
|
|
||||||
env.app.dashboardUrl,
|
|
||||||
currentNewtVersion,
|
|
||||||
acceptClients
|
|
||||||
);
|
|
||||||
|
|
||||||
const wgConfig = generateWireGuardConfig(
|
const wgConfig = generateWireGuardConfig(
|
||||||
privateKey,
|
privateKey,
|
||||||
data.publicKey,
|
data.publicKey,
|
||||||
@@ -631,11 +439,6 @@ WantedBy=default.target`
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Sync form acceptClients value with local state
|
|
||||||
useEffect(() => {
|
|
||||||
form.setValue("acceptClients", acceptClients);
|
|
||||||
}, [acceptClients, form]);
|
|
||||||
|
|
||||||
// Sync form exitNodeId value with local state
|
// Sync form exitNodeId value with local state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (build !== "saas") {
|
if (build !== "saas") {
|
||||||
@@ -847,214 +650,15 @@ WantedBy=default.target`
|
|||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
||||||
{/* <Form {...form}> */}
|
|
||||||
{/* <form */}
|
|
||||||
{/* className="space-y-4" */}
|
|
||||||
{/* id="create-site-form" */}
|
|
||||||
{/* > */}
|
|
||||||
{/* <FormField */}
|
|
||||||
{/* control={form.control} */}
|
|
||||||
{/* name="copied" */}
|
|
||||||
{/* render={({ field }) => ( */}
|
|
||||||
{/* <FormItem> */}
|
|
||||||
{/* <div className="flex items-center space-x-2"> */}
|
|
||||||
{/* <Checkbox */}
|
|
||||||
{/* id="terms" */}
|
|
||||||
{/* defaultChecked={ */}
|
|
||||||
{/* form.getValues( */}
|
|
||||||
{/* "copied" */}
|
|
||||||
{/* ) as boolean */}
|
|
||||||
{/* } */}
|
|
||||||
{/* onCheckedChange={( */}
|
|
||||||
{/* e */}
|
|
||||||
{/* ) => { */}
|
|
||||||
{/* form.setValue( */}
|
|
||||||
{/* "copied", */}
|
|
||||||
{/* e as boolean */}
|
|
||||||
{/* ); */}
|
|
||||||
{/* }} */}
|
|
||||||
{/* /> */}
|
|
||||||
{/* <label */}
|
|
||||||
{/* htmlFor="terms" */}
|
|
||||||
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
|
|
||||||
{/* > */}
|
|
||||||
{/* {t('siteConfirmCopy')} */}
|
|
||||||
{/* </label> */}
|
|
||||||
{/* </div> */}
|
|
||||||
{/* <FormMessage /> */}
|
|
||||||
{/* </FormItem> */}
|
|
||||||
{/* )} */}
|
|
||||||
{/* /> */}
|
|
||||||
{/* </form> */}
|
|
||||||
{/* </Form> */}
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("siteInstallNewt")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("siteInstallNewtDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<div>
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{t("operatingSystem")}
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
||||||
{platforms.map((os) => (
|
|
||||||
<Button
|
|
||||||
key={os}
|
|
||||||
variant={
|
|
||||||
platform === os
|
|
||||||
? "squareOutlinePrimary"
|
|
||||||
: "squareOutline"
|
|
||||||
}
|
|
||||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
|
||||||
onClick={() => {
|
|
||||||
setPlatform(os);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getPlatformIcon(os)}
|
|
||||||
{getPlatformName(os)}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<NewtSiteInstallCommands
|
||||||
<p className="font-bold mb-3">
|
id={newtId}
|
||||||
{["docker", "podman"].includes(
|
secret={newtSecret}
|
||||||
platform
|
endpoint={env.app.dashboardUrl}
|
||||||
)
|
version={newtVersion}
|
||||||
? t("method")
|
/>
|
||||||
: t("architecture")}
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
|
||||||
{getArchitectures().map(
|
|
||||||
(arch) => (
|
|
||||||
<Button
|
|
||||||
key={arch}
|
|
||||||
variant={
|
|
||||||
architecture ===
|
|
||||||
arch
|
|
||||||
? "squareOutlinePrimary"
|
|
||||||
: "squareOutline"
|
|
||||||
}
|
|
||||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
|
||||||
onClick={() =>
|
|
||||||
setArchitecture(
|
|
||||||
arch
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{arch}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{t("siteConfiguration")}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
<CheckboxWithLabel
|
|
||||||
id="acceptClients"
|
|
||||||
aria-describedby="acceptClients-desc"
|
|
||||||
checked={acceptClients}
|
|
||||||
onCheckedChange={(
|
|
||||||
checked
|
|
||||||
) => {
|
|
||||||
const value =
|
|
||||||
checked as boolean;
|
|
||||||
setAcceptClients(
|
|
||||||
value
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"acceptClients",
|
|
||||||
value
|
|
||||||
);
|
|
||||||
// Re-hydrate commands with new acceptClients value
|
|
||||||
if (
|
|
||||||
newtId &&
|
|
||||||
newtSecret &&
|
|
||||||
newtVersion
|
|
||||||
) {
|
|
||||||
hydrateCommands(
|
|
||||||
newtId,
|
|
||||||
newtSecret,
|
|
||||||
env.app
|
|
||||||
.dashboardUrl,
|
|
||||||
newtVersion,
|
|
||||||
value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
label={t(
|
|
||||||
"siteAcceptClientConnections"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
id="acceptClients-desc"
|
|
||||||
className="text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"siteAcceptClientConnectionsDescription"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<p className="font-bold mb-3">
|
|
||||||
{t("commands")}
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 space-y-3">
|
|
||||||
{getCommand().map(
|
|
||||||
(item, index) => {
|
|
||||||
const commandText =
|
|
||||||
typeof item ===
|
|
||||||
"string"
|
|
||||||
? item
|
|
||||||
: item.command;
|
|
||||||
const title =
|
|
||||||
typeof item ===
|
|
||||||
"string"
|
|
||||||
? undefined
|
|
||||||
: item.title;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{title && (
|
|
||||||
<p className="text-sm font-medium mb-1.5">
|
|
||||||
{
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CopyTextBox
|
|
||||||
text={
|
|
||||||
commandText
|
|
||||||
}
|
|
||||||
outline={
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1069,7 +673,7 @@ WantedBy=default.target`
|
|||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||||
<CopyTextBox text={wgConfig} />
|
<CopyTextBox text={wgConfig} />
|
||||||
<div
|
<div
|
||||||
className={`relative w-fit border rounded-md`}
|
className={`relative w-fit border rounded-md`}
|
||||||
@@ -1087,7 +691,7 @@ WantedBy=default.target`
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{build == "saas" &&
|
{build === "saas" &&
|
||||||
form.watch("method") === "local" && (
|
form.watch("method") === "local" && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const ApprovalsBanner = () => {
|
|||||||
description={t("approvalsBannerDescription")}
|
description={t("approvalsBannerDescription")}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/manage/access/approvals"
|
href="https://docs.pangolin.net/manage/access-control/approvals"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ export default function BlueprintDetailsForm({
|
|||||||
Dashboard
|
Dashboard
|
||||||
</Badge>
|
</Badge>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
|
{blueprint.source === "CLI" && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="inline-flex items-center gap-1 "
|
||||||
|
>
|
||||||
|
<Terminal className="w-3 h-3 flex-none" />
|
||||||
|
CLI
|
||||||
|
</Badge>
|
||||||
|
)}{" "}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
|
|||||||
@@ -128,6 +128,19 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case "CLI": {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-1 ">
|
||||||
|
<Terminal className="w-3 h-3" />
|
||||||
|
CLI
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from "react";
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Download } from "lucide-react";
|
import { Download } from "lucide-react";
|
||||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||||
|
import { SiAndroid } from "react-icons/si";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import DismissableBanner from "./DismissableBanner";
|
import DismissableBanner from "./DismissableBanner";
|
||||||
@@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => {
|
|||||||
Linux
|
Linux
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="https://pangolin.net/downloads/ios"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<FaApple className="w-4 h-4" />
|
||||||
|
iOS
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="https://pangolin.net/downloads/android"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
<SiAndroid className="w-4 h-4" />
|
||||||
|
Android
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</DismissableBanner>
|
</DismissableBanner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
@@ -40,6 +41,7 @@ export type InternalResourceRow = {
|
|||||||
destination: string;
|
destination: string;
|
||||||
// destinationPort: number | null;
|
// destinationPort: number | null;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
|
aliasAddress: string | null;
|
||||||
niceId: string;
|
niceId: string;
|
||||||
tcpPortRangeString: string | null;
|
tcpPortRangeString: string | null;
|
||||||
udpPortRangeString: string | null;
|
udpPortRangeString: string | null;
|
||||||
@@ -228,6 +230,29 @@ export default function ClientResourcesTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "aliasAddress",
|
||||||
|
friendlyName: t("resourcesTableAliasAddress"),
|
||||||
|
enableHiding: true,
|
||||||
|
header: () => (
|
||||||
|
<div className="flex items-center gap-2 p-3">
|
||||||
|
<span>{t("resourcesTableAliasAddress")}</span>
|
||||||
|
<InfoPopup info={t("resourcesTableAliasAddressInfo")} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
return resourceRow.aliasAddress ? (
|
||||||
|
<CopyToClipboard
|
||||||
|
text={resourceRow.aliasAddress}
|
||||||
|
isLink={false}
|
||||||
|
displayText={resourceRow.aliasAddress}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -316,7 +341,8 @@ export default function ClientResourcesTable({
|
|||||||
enableColumnVisibility={true}
|
enableColumnVisibility={true}
|
||||||
persistColumnVisibility="internal-resources"
|
persistColumnVisibility="internal-resources"
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
niceId: false
|
niceId: false,
|
||||||
|
aliasAddress: false
|
||||||
}}
|
}}
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
stickyRightColumn="actions"
|
stickyRightColumn="actions"
|
||||||
|
|||||||
@@ -94,12 +94,6 @@ export default function DomainPicker({
|
|||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
console.log({
|
|
||||||
defaultFullDomain,
|
|
||||||
defaultSubdomain,
|
|
||||||
defaultDomainId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||||
orgQueries.domains({ orgId })
|
orgQueries.domains({ orgId })
|
||||||
);
|
);
|
||||||
@@ -369,9 +363,6 @@ export default function DomainPicker({
|
|||||||
setSelectedProvidedDomain(null);
|
setSelectedProvidedDomain(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({
|
|
||||||
setSelectedBaseDomain: option
|
|
||||||
});
|
|
||||||
setSelectedBaseDomain(option);
|
setSelectedBaseDomain(option);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
@@ -442,9 +433,6 @@ export default function DomainPicker({
|
|||||||
0,
|
0,
|
||||||
providedDomainsShown
|
providedDomainsShown
|
||||||
);
|
);
|
||||||
console.log({
|
|
||||||
displayedProvidedOptions
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedDomainNamespaceId =
|
const selectedDomainNamespaceId =
|
||||||
selectedProvidedDomain?.domainNamespaceId ??
|
selectedProvidedDomain?.domainNamespaceId ??
|
||||||
|
|||||||
@@ -60,6 +60,18 @@ export default function LocaleSwitcher() {
|
|||||||
{
|
{
|
||||||
value: "zh-TW",
|
value: "zh-TW",
|
||||||
label: "繁體中文"
|
label: "繁體中文"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ru-RU",
|
||||||
|
label: "Русский"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bg-BG",
|
||||||
|
label: "Български"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "cs-CZ",
|
||||||
|
label: "Čeština"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
console.log({ filters });
|
|
||||||
const dateRange = {
|
const dateRange = {
|
||||||
startDate: filters.timeStart
|
startDate: filters.timeStart
|
||||||
? new Date(filters.timeStart)
|
? new Date(filters.timeStart)
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ export default function LoginOrgSelector({
|
|||||||
<IdpLoginButtons
|
<IdpLoginButtons
|
||||||
idps={idps}
|
idps={idps}
|
||||||
redirect={redirect}
|
redirect={redirect}
|
||||||
orgId={org.orgId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{clientRow.approvalState === "pending" && (
|
{clientRow.approvalState === "pending" && build !== "oss" && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => approveDevice(clientRow)}
|
onClick={() => approveDevice(clientRow)}
|
||||||
@@ -653,7 +653,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (build === "oss") {
|
if (build === "oss") {
|
||||||
return allOptions.filter((option) => option.value !== "pending");
|
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
|
||||||
}
|
}
|
||||||
|
|
||||||
return allOptions;
|
return allOptions;
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import CopyTextBox from "./CopyTextBox";
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "./Settings";
|
||||||
|
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
|
||||||
|
import { Terminal } from "lucide-react";
|
||||||
|
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||||
|
|
||||||
|
export type CommandItem = string | { title: string; command: string };
|
||||||
|
|
||||||
|
const PLATFORMS = [
|
||||||
|
"unix",
|
||||||
|
"windows",
|
||||||
|
"docker",
|
||||||
|
"kubernetes",
|
||||||
|
"podman",
|
||||||
|
"nixos"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type Platform = (typeof PLATFORMS)[number];
|
||||||
|
|
||||||
|
export type NewtSiteInstallCommandsProps = {
|
||||||
|
id: string;
|
||||||
|
secret: string;
|
||||||
|
endpoint: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NewtSiteInstallCommands({
|
||||||
|
id,
|
||||||
|
secret,
|
||||||
|
endpoint,
|
||||||
|
version = "latest"
|
||||||
|
}: NewtSiteInstallCommandsProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [acceptClients, setAcceptClients] = useState(true);
|
||||||
|
const [platform, setPlatform] = useState<Platform>("unix");
|
||||||
|
const [architecture, setArchitecture] = useState(
|
||||||
|
() => getArchitectures(platform)[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
||||||
|
const acceptClientsEnv = !acceptClients
|
||||||
|
? "\n - DISABLE_CLIENTS=true"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||||
|
unix: {
|
||||||
|
All: [
|
||||||
|
{
|
||||||
|
title: t("install"),
|
||||||
|
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("run"),
|
||||||
|
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
x64: [
|
||||||
|
{
|
||||||
|
title: t("install"),
|
||||||
|
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("run"),
|
||||||
|
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
docker: {
|
||||||
|
"Docker Compose": [
|
||||||
|
`services:
|
||||||
|
newt:
|
||||||
|
image: fosrl/newt
|
||||||
|
container_name: newt
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=${endpoint}
|
||||||
|
- NEWT_ID=${id}
|
||||||
|
- NEWT_SECRET=${secret}${acceptClientsEnv}`
|
||||||
|
],
|
||||||
|
"Docker Run": [
|
||||||
|
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
kubernetes: {
|
||||||
|
"Helm Chart": [
|
||||||
|
`helm repo add fossorial https://charts.fossorial.io`,
|
||||||
|
`helm repo update fossorial`,
|
||||||
|
`helm install newt fossorial/newt \\
|
||||||
|
--create-namespace \\
|
||||||
|
--set newtInstances[0].name="main-tunnel" \\
|
||||||
|
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
||||||
|
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
||||||
|
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
podman: {
|
||||||
|
"Podman Quadlet": [
|
||||||
|
`[Unit]
|
||||||
|
Description=Newt container
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=newt
|
||||||
|
Image=docker.io/fosrl/newt
|
||||||
|
Environment=PANGOLIN_ENDPOINT=${endpoint}
|
||||||
|
Environment=NEWT_ID=${id}
|
||||||
|
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
|
||||||
|
# Secret=newt-secret,type=env,target=NEWT_SECRET
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target`
|
||||||
|
],
|
||||||
|
"Podman Run": [
|
||||||
|
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
nixos: {
|
||||||
|
All: [
|
||||||
|
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commands = commandList[platform][architecture];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("siteInstallNewt")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("siteInstallNewtDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
{PLATFORMS.map((os) => (
|
||||||
|
<Button
|
||||||
|
key={os}
|
||||||
|
variant={
|
||||||
|
platform === os
|
||||||
|
? "squareOutlinePrimary"
|
||||||
|
: "squareOutline"
|
||||||
|
}
|
||||||
|
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||||
|
onClick={() => {
|
||||||
|
setPlatform(os);
|
||||||
|
const architectures = getArchitectures(os);
|
||||||
|
setArchitecture(architectures[0]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getPlatformIcon(os)}
|
||||||
|
{getPlatformName(os)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-bold mb-3">
|
||||||
|
{["docker", "podman"].includes(platform)
|
||||||
|
? t("method")
|
||||||
|
: t("architecture")}
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
{getArchitectures(platform).map((arch) => (
|
||||||
|
<Button
|
||||||
|
key={arch}
|
||||||
|
variant={
|
||||||
|
architecture === arch
|
||||||
|
? "squareOutlinePrimary"
|
||||||
|
: "squareOutline"
|
||||||
|
}
|
||||||
|
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||||
|
onClick={() => setArchitecture(arch)}
|
||||||
|
>
|
||||||
|
{arch}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<p className="font-bold mb-3">
|
||||||
|
{t("siteConfiguration")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<CheckboxWithLabel
|
||||||
|
id="acceptClients"
|
||||||
|
aria-describedby="acceptClients-desc"
|
||||||
|
checked={acceptClients}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const value = checked as boolean;
|
||||||
|
setAcceptClients(value);
|
||||||
|
}}
|
||||||
|
label={t("siteAcceptClientConnections")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
id="acceptClients-desc"
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{t("siteAcceptClientConnectionsDescription")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<p className="font-bold mb-3">{t("commands")}</p>
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{commands.map((item, index) => {
|
||||||
|
const commandText =
|
||||||
|
typeof item === "string"
|
||||||
|
? item
|
||||||
|
: item.command;
|
||||||
|
const title =
|
||||||
|
typeof item === "string"
|
||||||
|
? undefined
|
||||||
|
: item.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{title && (
|
||||||
|
<p className="text-sm font-medium mb-1.5">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<CopyTextBox
|
||||||
|
text={commandText}
|
||||||
|
outline={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformIcon(platformName: Platform) {
|
||||||
|
switch (platformName) {
|
||||||
|
case "windows":
|
||||||
|
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||||
|
case "unix":
|
||||||
|
return <Terminal className="h-4 w-4 mr-2" />;
|
||||||
|
case "docker":
|
||||||
|
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||||
|
case "kubernetes":
|
||||||
|
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||||
|
case "podman":
|
||||||
|
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||||
|
case "nixos":
|
||||||
|
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||||
|
default:
|
||||||
|
return <Terminal className="h-4 w-4 mr-2" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformName(platformName: Platform) {
|
||||||
|
switch (platformName) {
|
||||||
|
case "windows":
|
||||||
|
return "Windows";
|
||||||
|
case "unix":
|
||||||
|
return "Unix & macOS";
|
||||||
|
case "docker":
|
||||||
|
return "Docker";
|
||||||
|
case "kubernetes":
|
||||||
|
return "Kubernetes";
|
||||||
|
case "podman":
|
||||||
|
return "Podman";
|
||||||
|
case "nixos":
|
||||||
|
return "NixOS";
|
||||||
|
default:
|
||||||
|
return "Unix / macOS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArchitectures(platform: Platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case "unix":
|
||||||
|
return ["All"];
|
||||||
|
case "windows":
|
||||||
|
return ["x64"];
|
||||||
|
case "docker":
|
||||||
|
return ["Docker Compose", "Docker Run"];
|
||||||
|
case "kubernetes":
|
||||||
|
return ["Helm Chart"];
|
||||||
|
case "podman":
|
||||||
|
return ["Podman Quadlet", "Podman Run"];
|
||||||
|
case "nixos":
|
||||||
|
return ["All"];
|
||||||
|
default:
|
||||||
|
return ["x64"];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { Terminal } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaDocker, FaWindows } from "react-icons/fa";
|
||||||
|
import CopyTextBox from "./CopyTextBox";
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "./Settings";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
export type CommandItem = string | { title: string; command: string };
|
||||||
|
|
||||||
|
const PLATFORMS = ["unix", "windows", "docker"] as const;
|
||||||
|
|
||||||
|
type Platform = (typeof PLATFORMS)[number];
|
||||||
|
|
||||||
|
export type OlmInstallCommandsProps = {
|
||||||
|
id: string;
|
||||||
|
secret: string;
|
||||||
|
endpoint: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OlmInstallCommands({
|
||||||
|
id,
|
||||||
|
secret,
|
||||||
|
endpoint,
|
||||||
|
version = "latest"
|
||||||
|
}: OlmInstallCommandsProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [platform, setPlatform] = useState<Platform>("unix");
|
||||||
|
const [architecture, setArchitecture] = useState(
|
||||||
|
() => getArchitectures(platform)[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||||
|
unix: {
|
||||||
|
All: [
|
||||||
|
{
|
||||||
|
title: t("install"),
|
||||||
|
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("run"),
|
||||||
|
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
x64: [
|
||||||
|
{
|
||||||
|
title: t("install"),
|
||||||
|
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("run"),
|
||||||
|
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
docker: {
|
||||||
|
"Docker Compose": [
|
||||||
|
`services:
|
||||||
|
olm:
|
||||||
|
image: fosrl/olm
|
||||||
|
container_name: olm
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
environment:
|
||||||
|
- PANGOLIN_ENDPOINT=${endpoint}
|
||||||
|
- OLM_ID=${id}
|
||||||
|
- OLM_SECRET=${secret}`
|
||||||
|
],
|
||||||
|
"Docker Run": [
|
||||||
|
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commands = commandList[platform][architecture];
|
||||||
|
return (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("clientInstallOlm")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("clientInstallOlmDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
{PLATFORMS.map((os) => (
|
||||||
|
<Button
|
||||||
|
key={os}
|
||||||
|
variant={
|
||||||
|
platform === os
|
||||||
|
? "squareOutlinePrimary"
|
||||||
|
: "squareOutline"
|
||||||
|
}
|
||||||
|
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||||
|
onClick={() => {
|
||||||
|
setPlatform(os);
|
||||||
|
const architectures = getArchitectures(os);
|
||||||
|
setArchitecture(architectures[0]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getPlatformIcon(os)}
|
||||||
|
{getPlatformName(os)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-bold mb-3">
|
||||||
|
{["docker", "podman"].includes(platform)
|
||||||
|
? t("method")
|
||||||
|
: t("architecture")}
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
{getArchitectures(platform).map((arch) => (
|
||||||
|
<Button
|
||||||
|
key={arch}
|
||||||
|
variant={
|
||||||
|
architecture === arch
|
||||||
|
? "squareOutlinePrimary"
|
||||||
|
: "squareOutline"
|
||||||
|
}
|
||||||
|
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||||
|
onClick={() => setArchitecture(arch)}
|
||||||
|
>
|
||||||
|
{arch}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="pt-4">
|
||||||
|
<p className="font-bold mb-3">{t("commands")}</p>
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{commands.map((item, index) => {
|
||||||
|
const commandText =
|
||||||
|
typeof item === "string"
|
||||||
|
? item
|
||||||
|
: item.command;
|
||||||
|
const title =
|
||||||
|
typeof item === "string"
|
||||||
|
? undefined
|
||||||
|
: item.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{title && (
|
||||||
|
<p className="text-sm font-medium mb-1.5">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<CopyTextBox
|
||||||
|
text={commandText}
|
||||||
|
outline={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArchitectures(platform: Platform) {
|
||||||
|
switch (platform) {
|
||||||
|
case "unix":
|
||||||
|
return ["All"];
|
||||||
|
case "windows":
|
||||||
|
return ["x64"];
|
||||||
|
case "docker":
|
||||||
|
return ["Docker Compose", "Docker Run"];
|
||||||
|
default:
|
||||||
|
return ["x64"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformName(platformName: Platform) {
|
||||||
|
switch (platformName) {
|
||||||
|
case "windows":
|
||||||
|
return "Windows";
|
||||||
|
case "unix":
|
||||||
|
return "Unix & macOS";
|
||||||
|
case "docker":
|
||||||
|
return "Docker";
|
||||||
|
default:
|
||||||
|
return "Unix & macOS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatformIcon(platformName: Platform) {
|
||||||
|
switch (platformName) {
|
||||||
|
case "windows":
|
||||||
|
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||||
|
case "unix":
|
||||||
|
return <Terminal className="h-4 w-4 mr-2" />;
|
||||||
|
case "docker":
|
||||||
|
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||||
|
default:
|
||||||
|
return <Terminal className="h-4 w-4 mr-2" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import type { DockerState } from "@app/lib/docker";
|
||||||
|
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||||
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
|
import { type ListTargetsResponse } from "@server/routers/target";
|
||||||
|
import type { ArrayElement } from "@server/types/ArrayElement";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { ContainersSelector } from "./ContainersSelector";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "./ui/command";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||||
|
|
||||||
|
type SiteWithUpdateAvailable = ListSitesResponse["sites"][number];
|
||||||
|
|
||||||
|
export type LocalTarget = Omit<
|
||||||
|
ArrayElement<ListTargetsResponse["targets"]> & {
|
||||||
|
new?: boolean;
|
||||||
|
updated?: boolean;
|
||||||
|
siteType: string | null;
|
||||||
|
},
|
||||||
|
"protocol"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ResourceTargetAddressItemProps = {
|
||||||
|
getDockerStateForSite: (siteId: number) => DockerState;
|
||||||
|
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
|
||||||
|
sites: SiteWithUpdateAvailable[];
|
||||||
|
proxyTarget: LocalTarget;
|
||||||
|
isHttp: boolean;
|
||||||
|
refreshContainersForSite: (siteId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ResourceTargetAddressItem({
|
||||||
|
sites,
|
||||||
|
getDockerStateForSite,
|
||||||
|
updateTarget,
|
||||||
|
proxyTarget,
|
||||||
|
isHttp,
|
||||||
|
refreshContainersForSite
|
||||||
|
}: ResourceTargetAddressItemProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const selectedSite = sites.find(
|
||||||
|
(site) => site.siteId === proxyTarget.siteId
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContainerSelectForTarget = (
|
||||||
|
hostname: string,
|
||||||
|
port?: number
|
||||||
|
) => {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
ip: hostname,
|
||||||
|
...(port && { port: port })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center w-full" key={proxyTarget.targetId}>
|
||||||
|
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
|
||||||
|
{selectedSite &&
|
||||||
|
selectedSite.type === "newt" &&
|
||||||
|
(() => {
|
||||||
|
const dockerState = getDockerStateForSite(
|
||||||
|
selectedSite.siteId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ContainersSelector
|
||||||
|
site={selectedSite}
|
||||||
|
containers={dockerState.containers}
|
||||||
|
isAvailable={dockerState.isAvailable}
|
||||||
|
onContainerSelect={
|
||||||
|
handleContainerSelectForTarget
|
||||||
|
}
|
||||||
|
onRefresh={() =>
|
||||||
|
refreshContainersForSite(
|
||||||
|
selectedSite.siteId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-45 justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
|
"rounded-l-md rounded-r-xs",
|
||||||
|
!proxyTarget.siteId && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate max-w-37.5">
|
||||||
|
{proxyTarget.siteId
|
||||||
|
? selectedSite?.name
|
||||||
|
: t("siteSelect")}
|
||||||
|
</span>
|
||||||
|
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0 w-45">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={t("siteSearch")} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{sites.map((site) => (
|
||||||
|
<CommandItem
|
||||||
|
key={site.siteId}
|
||||||
|
value={`${site.siteId}:${site.name}`}
|
||||||
|
onSelect={() =>
|
||||||
|
updateTarget(
|
||||||
|
proxyTarget.targetId,
|
||||||
|
{
|
||||||
|
siteId: site.siteId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
site.siteId ===
|
||||||
|
proxyTarget.siteId
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{site.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{isHttp && (
|
||||||
|
<Select
|
||||||
|
defaultValue={proxyTarget.method ?? "http"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
method: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 px-2 w-17.5 border-none bg-transparent shadow-none data-[state=open]:bg-transparent rounded-xs">
|
||||||
|
{proxyTarget.method || "http"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">http</SelectItem>
|
||||||
|
<SelectItem value="https">https</SelectItem>
|
||||||
|
<SelectItem value="h2c">h2c</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isHttp && (
|
||||||
|
<div className="flex items-center justify-center px-2 h-9">
|
||||||
|
{"://"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={proxyTarget.ip}
|
||||||
|
placeholder="Host"
|
||||||
|
className="flex-1 min-w-30 px-2 border-none placeholder-gray-400 rounded-xs"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const input = e.target.value.trim();
|
||||||
|
const hasProtocol = /^(https?|h2c):\/\//.test(input);
|
||||||
|
const hasPort = /:\d+(?:\/|$)/.test(input);
|
||||||
|
|
||||||
|
if (hasProtocol || hasPort) {
|
||||||
|
const parsed = parseHostTarget(input);
|
||||||
|
if (parsed) {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
method: hasProtocol
|
||||||
|
? parsed.protocol
|
||||||
|
: proxyTarget.method,
|
||||||
|
ip: parsed.host,
|
||||||
|
port: hasPort
|
||||||
|
? parsed.port
|
||||||
|
: proxyTarget.port
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
ip: input
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
ip: input
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center px-2 h-9">
|
||||||
|
{":"}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Port"
|
||||||
|
defaultValue={
|
||||||
|
proxyTarget.port === 0 ? "" : proxyTarget.port
|
||||||
|
}
|
||||||
|
className="w-18.75 px-2 border-none placeholder-gray-400 rounded-l-xs"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = parseInt(e.target.value, 10);
|
||||||
|
if (!isNaN(value) && value > 0) {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
port: value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateTarget(proxyTarget.targetId, {
|
||||||
|
...proxyTarget,
|
||||||
|
port: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -44,8 +44,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
|
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
|
||||||
|
// "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -60,7 +62,7 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-sm",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-sm",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className
|
className
|
||||||
@@ -73,7 +75,7 @@ function SelectContent({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user