mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 06:10:47 +00:00
Compare commits
35 Commits
crowdin_de
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3a2c660e | ||
|
|
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 |
60
.github/workflows/cicd.yml
vendored
60
.github/workflows/cicd.yml
vendored
@@ -339,37 +339,37 @@ jobs:
|
||||
TAG=${{ env.TAG }}
|
||||
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||
|
||||
|
||||
echo "Waiting for multi-arch manifests to be ready..."
|
||||
sleep 30
|
||||
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
|
||||
|
||||
if [ "$IS_RC" = "true" ]; then
|
||||
echo "RC release detected - copying version-specific tags only"
|
||||
|
||||
|
||||
# SQLite OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
|
||||
|
||||
# PostgreSQL OSS
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG
|
||||
|
||||
|
||||
# SQLite Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG \
|
||||
docker://$GHCR_IMAGE:ee-$TAG
|
||||
|
||||
|
||||
# PostgreSQL Enterprise
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG
|
||||
else
|
||||
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
|
||||
|
||||
|
||||
# SQLite OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
|
||||
@@ -385,7 +385,7 @@ jobs:
|
||||
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:$TAG_SUFFIX
|
||||
done
|
||||
|
||||
|
||||
# PostgreSQL OSS - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
|
||||
@@ -393,7 +393,7 @@ jobs:
|
||||
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
|
||||
# SQLite Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
|
||||
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
|
||||
done
|
||||
|
||||
|
||||
# PostgreSQL Enterprise - all tags
|
||||
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
|
||||
@@ -410,7 +410,7 @@ jobs:
|
||||
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
echo "All images copied successfully to GHCR!"
|
||||
shell: bash
|
||||
|
||||
@@ -442,7 +442,7 @@ jobs:
|
||||
|
||||
# Determine if this is an RC release
|
||||
IS_RC="false"
|
||||
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
|
||||
if [[ "$TAG" == *"-rc."* ]]; then
|
||||
IS_RC="true"
|
||||
fi
|
||||
|
||||
@@ -482,19 +482,37 @@ jobs:
|
||||
echo "==> cosign sign (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}"
|
||||
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}"
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer "${issuer}" \
|
||||
--certificate-identity-regexp "${id_regex}" \
|
||||
"${REF}" -o text
|
||||
|
||||
retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"
|
||||
|
||||
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||
done
|
||||
done
|
||||
|
||||
|
||||
echo "All images signed and verified successfully!"
|
||||
shell: bash
|
||||
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -4,13 +4,13 @@
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
@@ -19,4 +19,4 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,12 @@
|
||||
|
||||
</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">
|
||||
<strong>
|
||||
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)
|
||||
- [Windows](https://pangolin.net/downloads/windows)
|
||||
- [Linux](https://pangolin.net/downloads/linux)
|
||||
- [iOS](https://pangolin.net/downloads/ios)
|
||||
- [Android](https://pangolin.net/downloads/android)
|
||||
|
||||
## Get Started
|
||||
|
||||
|
||||
72
blueprint.py
72
blueprint.py
@@ -1,72 +0,0 @@
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
import base64
|
||||
|
||||
# The file path for the YAML file to be read
|
||||
# You can change this to the path of your YAML file
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://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
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Активирана защитна стена.",
|
||||
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
|
||||
"tpmAvailable": "TPM е на разположение.",
|
||||
"windowsDefenderEnabled": "Windows Defender е активиран.",
|
||||
"macosSipEnabled": "Protection на системната цялост (SIP).",
|
||||
"macosGatekeeperEnabled": "Gatekeeper.",
|
||||
"macosFirewallStealthMode": "Скрит режим на защитната стена.",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Firewall povolen",
|
||||
"autoUpdatesEnabled": "Automatické aktualizace povoleny",
|
||||
"tpmAvailable": "TPM k dispozici",
|
||||
"windowsDefenderEnabled": "Okna byla povolena",
|
||||
"macosSipEnabled": "Ochrana systémové integrity (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Režim neviditelnosti firewallu",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Firewall aktiviert",
|
||||
"autoUpdatesEnabled": "Automatische Updates aktiviert",
|
||||
"tpmAvailable": "TPM verfügbar",
|
||||
"windowsDefenderEnabled": "Windows Defender aktiviert",
|
||||
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth-Modus",
|
||||
|
||||
@@ -2510,7 +2510,7 @@
|
||||
"firewallEnabled": "Firewall Enabled",
|
||||
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||
"tpmAvailable": "TPM Available",
|
||||
"windowsDefenderEnabled": "Windows Defender Enabled",
|
||||
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Cortafuegos activado",
|
||||
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
|
||||
"tpmAvailable": "TPM disponible",
|
||||
"windowsDefenderEnabled": "Windows Defender activado",
|
||||
"macosSipEnabled": "Protección de integridad del sistema (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Sigilo Firewall",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Pare-feu activé",
|
||||
"autoUpdatesEnabled": "Mises à jour automatiques activées",
|
||||
"tpmAvailable": "TPM disponible",
|
||||
"windowsDefenderEnabled": "Windows Defender activé",
|
||||
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Mode furtif du pare-feu",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Firewall Abilitato",
|
||||
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
|
||||
"tpmAvailable": "TPM Disponibile",
|
||||
"windowsDefenderEnabled": "Windows Defender Abilitato",
|
||||
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Furtivo Del Firewall",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "방화벽 활성화",
|
||||
"autoUpdatesEnabled": "자동 업데이트 활성화",
|
||||
"tpmAvailable": "TPM 사용 가능",
|
||||
"windowsDefenderEnabled": "Windows Defender 활성화",
|
||||
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "방화벽 스텔스 모드",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Brannmur aktivert",
|
||||
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
|
||||
"tpmAvailable": "TPM tilgjengelig",
|
||||
"windowsDefenderEnabled": "Windows svarer aktivert",
|
||||
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Brannmur Usynlig Modus",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Firewall ingeschakeld",
|
||||
"autoUpdatesEnabled": "Auto Updates Ingeschakeld",
|
||||
"tpmAvailable": "TPM beschikbaar",
|
||||
"windowsDefenderEnabled": "Windows Verdediger ingeschakeld",
|
||||
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Verberg Modus",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Zapora włączona",
|
||||
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
|
||||
"tpmAvailable": "TPM dostępne",
|
||||
"windowsDefenderEnabled": "Obrońca Windows włączony",
|
||||
"macosSipEnabled": "Ochrona integralności systemu (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Tryb Stealth zapory",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Firewall habilitado",
|
||||
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
|
||||
"tpmAvailable": "TPM disponível",
|
||||
"windowsDefenderEnabled": "Defensor do Windows habilitado",
|
||||
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Furtivo do Firewall",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Брандмауэр включен",
|
||||
"autoUpdatesEnabled": "Автоматические обновления включены",
|
||||
"tpmAvailable": "Доступно TPM",
|
||||
"windowsDefenderEnabled": "Защитник Windows включен",
|
||||
"macosSipEnabled": "Защита целостности системы (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Стилс-режим брандмауэра",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "Güvenlik Duvarı Etkin",
|
||||
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
|
||||
"tpmAvailable": "TPM Mevcut",
|
||||
"windowsDefenderEnabled": "Windows Defender Etkin",
|
||||
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",
|
||||
|
||||
@@ -2510,7 +2510,6 @@
|
||||
"firewallEnabled": "防火墙已启用",
|
||||
"autoUpdatesEnabled": "启用自动更新",
|
||||
"tpmAvailable": "TPM 可用",
|
||||
"windowsDefenderEnabled": "Windows Defender 已启用",
|
||||
"macosSipEnabled": "系统完整性保护 (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "防火墙隐形模式",
|
||||
|
||||
147
package-lock.json
generated
147
package-lock.json
generated
@@ -82,7 +82,7 @@
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.11",
|
||||
"npm": "11.7.0",
|
||||
"npm": "11.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.17.1",
|
||||
@@ -13944,7 +13944,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -17048,9 +17047,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm": {
|
||||
"version": "11.7.0",
|
||||
"resolved": "https://registry.npmjs.org/npm/-/npm-11.7.0.tgz",
|
||||
"integrity": "sha512-wiCZpv/41bIobCoJ31NStIWKfAxxYyD1iYnWCtiyns8s5v3+l8y0HCP/sScuH6B5+GhIfda4HQKiqeGZwJWhFw==",
|
||||
"version": "11.8.0",
|
||||
"resolved": "https://registry.npmjs.org/npm/-/npm-11.8.0.tgz",
|
||||
"integrity": "sha512-n19sJeW+RGKdkHo8SCc5xhSwkKhQUFfZaFzSc+EsYXLjSqIV0tl72aDYQVuzVvfrbysGwdaQsNLNy58J10EBSQ==",
|
||||
"bundleDependencies": [
|
||||
"@isaacs/string-locale-compare",
|
||||
"@npmcli/arborist",
|
||||
@@ -17129,8 +17128,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@isaacs/string-locale-compare": "^1.1.0",
|
||||
"@npmcli/arborist": "^9.1.9",
|
||||
"@npmcli/config": "^10.4.5",
|
||||
"@npmcli/arborist": "^9.1.10",
|
||||
"@npmcli/config": "^10.5.0",
|
||||
"@npmcli/fs": "^5.0.0",
|
||||
"@npmcli/map-workspaces": "^5.0.3",
|
||||
"@npmcli/metavuln-calculator": "^9.0.3",
|
||||
@@ -17138,7 +17137,7 @@
|
||||
"@npmcli/promise-spawn": "^9.0.1",
|
||||
"@npmcli/redact": "^4.0.0",
|
||||
"@npmcli/run-script": "^10.0.3",
|
||||
"@sigstore/tuf": "^4.0.0",
|
||||
"@sigstore/tuf": "^4.0.1",
|
||||
"abbrev": "^4.0.0",
|
||||
"archy": "~1.0.0",
|
||||
"cacache": "^20.0.3",
|
||||
@@ -17155,11 +17154,11 @@
|
||||
"is-cidr": "^6.0.1",
|
||||
"json-parse-even-better-errors": "^5.0.0",
|
||||
"libnpmaccess": "^10.0.3",
|
||||
"libnpmdiff": "^8.0.12",
|
||||
"libnpmexec": "^10.1.11",
|
||||
"libnpmfund": "^7.0.12",
|
||||
"libnpmdiff": "^8.0.13",
|
||||
"libnpmexec": "^10.1.12",
|
||||
"libnpmfund": "^7.0.13",
|
||||
"libnpmorg": "^8.0.1",
|
||||
"libnpmpack": "^9.0.12",
|
||||
"libnpmpack": "^9.0.13",
|
||||
"libnpmpublish": "^11.1.3",
|
||||
"libnpmsearch": "^9.0.1",
|
||||
"libnpmteam": "^8.0.2",
|
||||
@@ -17188,11 +17187,11 @@
|
||||
"spdx-expression-parse": "^4.0.0",
|
||||
"ssri": "^13.0.0",
|
||||
"supports-color": "^10.2.2",
|
||||
"tar": "^7.5.2",
|
||||
"tar": "^7.5.4",
|
||||
"text-table": "~0.2.0",
|
||||
"tiny-relative-date": "^2.0.2",
|
||||
"treeverse": "^3.0.0",
|
||||
"validate-npm-package-name": "^7.0.0",
|
||||
"validate-npm-package-name": "^7.0.2",
|
||||
"which": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -17267,7 +17266,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@npmcli/arborist": {
|
||||
"version": "9.1.9",
|
||||
"version": "9.1.10",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -17284,7 +17283,7 @@
|
||||
"@npmcli/run-script": "^10.0.0",
|
||||
"bin-links": "^6.0.0",
|
||||
"cacache": "^20.0.1",
|
||||
"common-ancestor-path": "^1.0.1",
|
||||
"common-ancestor-path": "^2.0.0",
|
||||
"hosted-git-info": "^9.0.0",
|
||||
"json-stringify-nice": "^1.1.4",
|
||||
"lru-cache": "^11.2.1",
|
||||
@@ -17313,7 +17312,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@npmcli/config": {
|
||||
"version": "10.4.5",
|
||||
"version": "10.5.0",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -17494,7 +17493,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/core": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -17510,48 +17509,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/sign": {
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.0",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/protobuf-specs": "^0.5.0",
|
||||
"make-fetch-happen": "^15.0.2",
|
||||
"proc-log": "^5.0.0",
|
||||
"make-fetch-happen": "^15.0.3",
|
||||
"proc-log": "^6.1.0",
|
||||
"promise-retry": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": {
|
||||
"version": "5.0.0",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/tuf": {
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/protobuf-specs": "^0.5.0",
|
||||
"tuf-js": "^4.0.0"
|
||||
"tuf-js": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/verify": {
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/protobuf-specs": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -17567,31 +17558,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@tufjs/models": {
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tufjs/canonical-json": "2.0.0",
|
||||
"minimatch": "^9.0.5"
|
||||
"minimatch": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/abbrev": {
|
||||
"version": "4.0.0",
|
||||
"inBundle": true,
|
||||
@@ -17626,11 +17603,6 @@
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm/node_modules/bin-links": {
|
||||
"version": "6.0.0",
|
||||
"inBundle": true,
|
||||
@@ -17657,14 +17629,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/cacache": {
|
||||
"version": "20.0.3",
|
||||
"inBundle": true,
|
||||
@@ -17751,9 +17715,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/common-ancestor-path": {
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.0",
|
||||
"inBundle": true,
|
||||
"license": "ISC"
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
@@ -17783,7 +17750,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/diff": {
|
||||
"version": "8.0.2",
|
||||
"version": "8.0.3",
|
||||
"inBundle": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -17959,7 +17926,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"version": "10.1.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -18051,11 +18018,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmdiff": {
|
||||
"version": "8.0.12",
|
||||
"version": "8.0.13",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.1.9",
|
||||
"@npmcli/arborist": "^9.1.10",
|
||||
"@npmcli/installed-package-contents": "^4.0.0",
|
||||
"binary-extensions": "^3.0.0",
|
||||
"diff": "^8.0.2",
|
||||
@@ -18069,11 +18036,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmexec": {
|
||||
"version": "10.1.11",
|
||||
"version": "10.1.12",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.1.9",
|
||||
"@npmcli/arborist": "^9.1.10",
|
||||
"@npmcli/package-json": "^7.0.0",
|
||||
"@npmcli/run-script": "^10.0.0",
|
||||
"ci-info": "^4.0.0",
|
||||
@@ -18091,11 +18058,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmfund": {
|
||||
"version": "7.0.12",
|
||||
"version": "7.0.13",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.1.9"
|
||||
"@npmcli/arborist": "^9.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -18114,11 +18081,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmpack": {
|
||||
"version": "9.0.12",
|
||||
"version": "9.0.13",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.1.9",
|
||||
"@npmcli/arborist": "^9.1.10",
|
||||
"@npmcli/run-script": "^10.0.0",
|
||||
"npm-package-arg": "^13.0.0",
|
||||
"pacote": "^21.0.2"
|
||||
@@ -18184,9 +18151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/lru-cache": {
|
||||
"version": "11.2.2",
|
||||
"version": "11.2.4",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
@@ -18567,7 +18534,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -18582,7 +18549,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.0",
|
||||
"version": "7.1.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -18711,16 +18678,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/sigstore": {
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/protobuf-specs": "^0.5.0",
|
||||
"@sigstore/sign": "^4.0.0",
|
||||
"@sigstore/tuf": "^4.0.0",
|
||||
"@sigstore/verify": "^3.0.0"
|
||||
"@sigstore/sign": "^4.1.0",
|
||||
"@sigstore/tuf": "^4.0.1",
|
||||
"@sigstore/verify": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -18845,7 +18812,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/tar": {
|
||||
"version": "7.5.2",
|
||||
"version": "7.5.4",
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -18928,13 +18895,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/tuf-js": {
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tufjs/models": "4.0.0",
|
||||
"debug": "^4.4.1",
|
||||
"make-fetch-happen": "^15.0.0"
|
||||
"@tufjs/models": "4.1.0",
|
||||
"debug": "^4.4.3",
|
||||
"make-fetch-happen": "^15.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -18986,7 +18953,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/validate-npm-package-name": {
|
||||
"version": "7.0.0",
|
||||
"version": "7.0.2",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.11",
|
||||
"npm": "11.7.0",
|
||||
"npm": "11.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.17.1",
|
||||
|
||||
@@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
|
||||
@@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers";
|
||||
import { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export type ProxyResourcesResults = {
|
||||
@@ -213,11 +213,7 @@ export async function updateProxyResources(
|
||||
// Update existing resource
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
logger.warn(
|
||||
"Server is not licensed! Clearing set maintenance screen values"
|
||||
);
|
||||
// null the maintenance mode fields if not licensed
|
||||
if (!isLicensed) {
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
@@ -594,7 +590,7 @@ export async function updateProxyResources(
|
||||
existingRule.action !== getRuleAction(rule.action) ||
|
||||
existingRule.match !== rule.match.toUpperCase() ||
|
||||
existingRule.value !==
|
||||
getRuleValue(rule.match.toUpperCase(), rule.value) ||
|
||||
getRuleValue(rule.match.toUpperCase(), rule.value) ||
|
||||
existingRule.priority !== intendedPriority
|
||||
) {
|
||||
validateRule(rule);
|
||||
@@ -653,11 +649,7 @@ export async function updateProxyResources(
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
logger.warn(
|
||||
"Server is not licensed! Clearing set maintenance screen values"
|
||||
);
|
||||
// null the maintenance mode fields if not licensed
|
||||
if (!isLicensed) {
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@server/db";
|
||||
import { getUniqueClientName } from "@server/db/names";
|
||||
import { getNextAvailableClientSubnet } from "@server/lib/ip";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import logger from "@server/logger";
|
||||
import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
|
||||
|
||||
@@ -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> {
|
||||
if (build === "enterprise") {
|
||||
return await license.isUnlocked();
|
||||
}
|
||||
|
||||
if (build === "saas") {
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
return tier === TierId.STANDARD;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
30
server/private/lib/isLicencedOrSubscribed.ts
Normal file
30
server/private/lib/isLicencedOrSubscribed.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
|
||||
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
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 {
|
||||
approvals,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
|
||||
|
||||
import { build } from "@server/build";
|
||||
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 response from "@server/lib/response";
|
||||
import { and, eq, type InferInsertModel } from "drizzle-orm";
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function upsertLoginPageBranding(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
const parsedBody = await bodySchema.safeParseAsync(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -26,7 +26,8 @@ const applyBlueprintSchema = z
|
||||
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
})
|
||||
}),
|
||||
source: z.enum(["API", "UI", "CLI"]).optional()
|
||||
})
|
||||
.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);
|
||||
|
||||
@@ -107,7 +108,7 @@ export async function applyYAMLBlueprint(
|
||||
blueprint = await applyBlueprint({
|
||||
orgId,
|
||||
name,
|
||||
source: "UI",
|
||||
source,
|
||||
configData: parsedConfig
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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"> & {
|
||||
source: BlueprintSource;
|
||||
|
||||
@@ -9,9 +9,6 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
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({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -77,9 +74,6 @@ export async function archiveClient(
|
||||
.update(clients)
|
||||
.set({ archived: true })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
// Rebuild associations to clean up related data
|
||||
await rebuildClientAssociationsFromClient(client, trx);
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
|
||||
const getClientSchema = z.strictObject({
|
||||
clientId: z
|
||||
@@ -58,7 +59,7 @@ type PostureData = {
|
||||
firewallEnabled?: boolean | null;
|
||||
autoUpdatesEnabled?: boolean | null;
|
||||
tpmAvailable?: boolean | null;
|
||||
windowsDefenderEnabled?: boolean | null;
|
||||
windowsAntivirusEnabled?: boolean | null;
|
||||
macosSipEnabled?: boolean | null;
|
||||
macosGatekeeperEnabled?: boolean | null;
|
||||
macosFirewallStealthMode?: boolean | null;
|
||||
@@ -75,78 +76,123 @@ function getPlatformPostureData(
|
||||
const normalizedPlatform = platform?.toLowerCase() || "unknown";
|
||||
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 (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
if (
|
||||
fingerprint.diskEncrypted !== null &&
|
||||
fingerprint.diskEncrypted !== undefined
|
||||
) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.firewallEnabled !== null &&
|
||||
fingerprint.firewallEnabled !== undefined
|
||||
) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
|
||||
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
||||
}
|
||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
||||
if (
|
||||
fingerprint.tpmAvailable !== null &&
|
||||
fingerprint.tpmAvailable !== undefined
|
||||
) {
|
||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||
}
|
||||
if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) {
|
||||
posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled;
|
||||
if (
|
||||
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
|
||||
else if (normalizedPlatform === "macos") {
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
if (
|
||||
fingerprint.diskEncrypted !== null &&
|
||||
fingerprint.diskEncrypted !== undefined
|
||||
) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.biometricsEnabled !== null &&
|
||||
fingerprint.biometricsEnabled !== undefined
|
||||
) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.firewallEnabled !== null &&
|
||||
fingerprint.firewallEnabled !== undefined
|
||||
) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.macosSipEnabled !== null &&
|
||||
fingerprint.macosSipEnabled !== undefined
|
||||
) {
|
||||
posture.macosSipEnabled = fingerprint.macosSipEnabled;
|
||||
}
|
||||
if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.macosGatekeeperEnabled !== null &&
|
||||
fingerprint.macosGatekeeperEnabled !== undefined
|
||||
) {
|
||||
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
|
||||
}
|
||||
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
|
||||
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
|
||||
if (
|
||||
fingerprint.macosFirewallStealthMode !== null &&
|
||||
fingerprint.macosFirewallStealthMode !== undefined
|
||||
) {
|
||||
posture.macosFirewallStealthMode =
|
||||
fingerprint.macosFirewallStealthMode;
|
||||
}
|
||||
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.autoUpdatesEnabled !== null &&
|
||||
fingerprint.autoUpdatesEnabled !== undefined
|
||||
) {
|
||||
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
||||
}
|
||||
}
|
||||
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
|
||||
else if (normalizedPlatform === "linux") {
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
if (
|
||||
fingerprint.diskEncrypted !== null &&
|
||||
fingerprint.diskEncrypted !== undefined
|
||||
) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.firewallEnabled !== null &&
|
||||
fingerprint.firewallEnabled !== undefined
|
||||
) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.linuxAppArmorEnabled !== null &&
|
||||
fingerprint.linuxAppArmorEnabled !== undefined
|
||||
) {
|
||||
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
|
||||
}
|
||||
if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) {
|
||||
if (
|
||||
fingerprint.linuxSELinuxEnabled !== null &&
|
||||
fingerprint.linuxSELinuxEnabled !== undefined
|
||||
) {
|
||||
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
|
||||
}
|
||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
||||
if (
|
||||
fingerprint.tpmAvailable !== null &&
|
||||
fingerprint.tpmAvailable !== undefined
|
||||
) {
|
||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||
}
|
||||
}
|
||||
// iOS: Biometric configuration
|
||||
else if (normalizedPlatform === "ios") {
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
// none supported yet
|
||||
}
|
||||
// Android: Screen lock, Biometric configuration, Hard drive encryption
|
||||
else if (normalizedPlatform === "android") {
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
if (
|
||||
fingerprint.diskEncrypted !== null &&
|
||||
fingerprint.diskEncrypted !== undefined
|
||||
) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
}
|
||||
@@ -243,23 +289,27 @@ export async function getClient(
|
||||
// Build fingerprint data if available
|
||||
const fingerprintData = client.currentFingerprint
|
||||
? {
|
||||
username: client.currentFingerprint.username || null,
|
||||
hostname: client.currentFingerprint.hostname || null,
|
||||
platform: client.currentFingerprint.platform || null,
|
||||
osVersion: client.currentFingerprint.osVersion || null,
|
||||
kernelVersion:
|
||||
client.currentFingerprint.kernelVersion || null,
|
||||
arch: client.currentFingerprint.arch || null,
|
||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||
lastSeen: client.currentFingerprint.lastSeen || null
|
||||
}
|
||||
username: client.currentFingerprint.username || null,
|
||||
hostname: client.currentFingerprint.hostname || null,
|
||||
platform: client.currentFingerprint.platform || null,
|
||||
osVersion: client.currentFingerprint.osVersion || null,
|
||||
kernelVersion:
|
||||
client.currentFingerprint.kernelVersion || null,
|
||||
arch: client.currentFingerprint.arch || null,
|
||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||
lastSeen: client.currentFingerprint.lastSeen || null
|
||||
}
|
||||
: null;
|
||||
|
||||
// Build posture data if available (platform-specific)
|
||||
// Only return posture data if org is licensed/subscribed
|
||||
let postureData: PostureData | null = null;
|
||||
if (build !== "oss") {
|
||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||
client.clients.orgId
|
||||
);
|
||||
if (isOrgLicensed) {
|
||||
postureData = getPlatformPostureData(
|
||||
client.currentFingerprint?.platform || null,
|
||||
client.currentFingerprint
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { olms, clients } from "@server/db";
|
||||
import { olms } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -8,9 +8,6 @@ import response from "@server/lib/response";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import { sendTerminateClient } from "../client/terminate";
|
||||
import { OlmErrorCodes } from "./error";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -37,26 +34,7 @@ export async function archiveUserOlm(
|
||||
|
||||
const { olmId } = parsedParams.data;
|
||||
|
||||
// Archive the OLM and disconnect associated clients in a transaction
|
||||
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
|
||||
.update(olms)
|
||||
.set({ archived: true })
|
||||
|
||||
@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
|
||||
tpmAvailable: postures.tpmAvailable ?? false,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled ?? false,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
|
||||
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
|
||||
@@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Handling fingerprint insertion for olm register...", {
|
||||
olmId: olm.olmId,
|
||||
fingerprint,
|
||||
postures
|
||||
});
|
||||
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
|
||||
if (
|
||||
@@ -143,7 +149,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!policyCheck.policies?.passwordAge?.compliant === false) {
|
||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
||||
);
|
||||
@@ -153,7 +159,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
);
|
||||
return;
|
||||
} else if (
|
||||
!policyCheck.policies?.maxSessionLength?.compliant === false
|
||||
policyCheck.policies?.maxSessionLength?.compliant === false
|
||||
) {
|
||||
logger.warn(
|
||||
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
||||
|
||||
@@ -13,7 +13,7 @@ import { build } from "@server/build";
|
||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { cache } from "@server/lib/cache";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
|
||||
const updateOrgParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -89,7 +89,7 @@ export async function updateOrg(
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
if (!isLicensed) {
|
||||
parsedBody.data.requireTwoFactor = undefined;
|
||||
parsedBody.data.maxSessionLengthHours = undefined;
|
||||
parsedBody.data.passwordExpiryDays = undefined;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { OpenAPITags } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
|
||||
const updateResourceParamsSchema = z.strictObject({
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -342,11 +342,7 @@ async function updateHttpResource(
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
||||
if (build == "enterprise" && !isLicensed) {
|
||||
logger.warn(
|
||||
"Server is not licensed! Clearing set maintenance screen values"
|
||||
);
|
||||
// null the maintenance mode fields if not licensed
|
||||
if (!isLicensed) {
|
||||
updateData.maintenanceModeEnabled = undefined;
|
||||
updateData.maintenanceModeType = undefined;
|
||||
updateData.maintenanceTitle = undefined;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ActionsEnum } from "@server/auth/actions";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
|
||||
const createRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -101,7 +101,7 @@ export async function createRole(
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build === "oss" || !isLicensed) {
|
||||
if (!isLicensed) {
|
||||
roleData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const updateRoleParamsSchema = z.strictObject({
|
||||
@@ -112,7 +111,7 @@ export async function updateRole(
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build === "oss" || !isLicensed) {
|
||||
if (!isLicensed) {
|
||||
updateData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export default async function migration() {
|
||||
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
||||
"autoUpdatesEnabled" 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,
|
||||
"macosGatekeeperEnabled" 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,
|
||||
"autoUpdatesEnabled" 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,
|
||||
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
||||
|
||||
@@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' (
|
||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||
'autoUpdatesEnabled' 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,
|
||||
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
||||
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
|
||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||
'autoUpdatesEnabled' 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,
|
||||
'macosGatekeeperEnabled' 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) {
|
||||
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;
|
||||
const orgRes = await getCachedOrg(params.orgId);
|
||||
|
||||
|
||||
@@ -656,17 +656,17 @@ export default function GeneralPage() {
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.windowsDefenderEnabled !== null &&
|
||||
client.posture.windowsDefenderEnabled !== undefined && (
|
||||
{client.posture.windowsAntivirusEnabled !== null &&
|
||||
client.posture.windowsAntivirusEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("windowsDefenderEnabled")}
|
||||
{t("windowsAntivirusEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.windowsDefenderEnabled
|
||||
.windowsAntivirusEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ContainersSelector } from "@app/components/ContainersSelector";
|
||||
import { HeadersInput } from "@app/components/HeadersInput";
|
||||
import {
|
||||
PathMatchDisplay,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
PathRewriteDisplay,
|
||||
PathRewriteModal
|
||||
} from "@app/components/PathMatchRenameModal";
|
||||
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
@@ -30,15 +30,6 @@ import {
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
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 {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -48,11 +39,6 @@ import {
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -73,12 +59,9 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import { tlsNameSchema } from "@server/lib/schemas";
|
||||
import { type GetResourceResponse } from "@server/routers/resource";
|
||||
import type { ListSitesResponse } from "@server/routers/site";
|
||||
@@ -98,7 +81,6 @@ import {
|
||||
import { AxiosResponse } from "axios";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckIcon,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Info,
|
||||
@@ -107,7 +89,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 { z } from "zod";
|
||||
|
||||
@@ -202,7 +184,7 @@ function ProxyResourceTargetsForm({
|
||||
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 containers = await dockerManager.fetchContainers();
|
||||
|
||||
@@ -214,9 +196,9 @@ function ProxyResourceTargetsForm({
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
const getDockerStateForSite = (siteId: number): DockerState => {
|
||||
const getDockerStateForSite = useCallback((siteId: number): DockerState => {
|
||||
return (
|
||||
dockerStates.get(siteId) || {
|
||||
isEnabled: false,
|
||||
@@ -224,7 +206,7 @@ function ProxyResourceTargetsForm({
|
||||
containers: []
|
||||
}
|
||||
);
|
||||
};
|
||||
}, [dockerStates]);
|
||||
|
||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -234,8 +216,40 @@ function ProxyResourceTargetsForm({
|
||||
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> = {
|
||||
id: "priority",
|
||||
@@ -419,213 +433,15 @@ function ProxyResourceTargetsForm({
|
||||
accessorKey: "address",
|
||||
header: () => <span className="p-3">{t("address")}</span>,
|
||||
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 (
|
||||
<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>
|
||||
|
||||
{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>
|
||||
<ResourceTargetAddressItem
|
||||
isHttp={isHttp}
|
||||
sites={sites}
|
||||
getDockerStateForSite={getDockerStateForSite}
|
||||
proxyTarget={row.original}
|
||||
refreshContainersForSite={refreshContainersForSite}
|
||||
updateTarget={updateTarget}
|
||||
/>
|
||||
);
|
||||
},
|
||||
size: 400,
|
||||
@@ -765,7 +581,7 @@ function ProxyResourceTargetsForm({
|
||||
actionsColumn
|
||||
];
|
||||
}
|
||||
};
|
||||
}, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]);
|
||||
|
||||
function addNewTarget() {
|
||||
const isHttp = resource.http;
|
||||
@@ -806,32 +622,6 @@ function ProxyResourceTargetsForm({
|
||||
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) {
|
||||
setTargets(
|
||||
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({
|
||||
data: targets,
|
||||
columns,
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
"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 {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
@@ -9,6 +18,10 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} 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 {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -18,22 +31,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} 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 { 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -41,48 +39,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
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 { Switch } from "@app/components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -91,30 +48,49 @@ import {
|
||||
TableHeader,
|
||||
TableRow
|
||||
} 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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} 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 {
|
||||
PathMatchDisplay,
|
||||
PathMatchModal,
|
||||
PathRewriteDisplay,
|
||||
PathRewriteModal
|
||||
} from "@app/components/PathMatchRenameModal";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} 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({
|
||||
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";
|
||||
|
||||
interface ResourceTypeOption {
|
||||
@@ -217,7 +189,7 @@ interface ResourceTypeOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type LocalTarget = Omit<
|
||||
export type LocalTarget = Omit<
|
||||
ArrayElement<ListTargetsResponse["targets"]> & {
|
||||
new?: boolean;
|
||||
updated?: boolean;
|
||||
@@ -233,18 +205,16 @@ export default function Page() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||
const [baseDomains, setBaseDomains] = useState<
|
||||
{ domainId: string; baseDomain: string }[]
|
||||
>([]);
|
||||
const { data: sites = [], isLoading: loadingPage } = useQuery(
|
||||
orgQueries.sites({ orgId: orgId as string })
|
||||
);
|
||||
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [showSnippets, setShowSnippets] = useState(false);
|
||||
const [niceId, setNiceId] = useState<string>("");
|
||||
|
||||
// Target management state
|
||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||
new Map()
|
||||
);
|
||||
@@ -405,102 +375,60 @@ export default function Page() {
|
||||
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||
};
|
||||
|
||||
const refreshContainersForSite = async (siteId: number) => {
|
||||
const dockerManager = new DockerManager(api, siteId);
|
||||
const containers = await dockerManager.fetchContainers();
|
||||
const refreshContainersForSite = useCallback(
|
||||
async (siteId: number) => {
|
||||
const dockerManager = new DockerManager(api, siteId);
|
||||
const containers = await dockerManager.fetchContainers();
|
||||
|
||||
setDockerStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existingState = newMap.get(siteId);
|
||||
if (existingState) {
|
||||
newMap.set(siteId, { ...existingState, containers });
|
||||
}
|
||||
return newMap;
|
||||
setDockerStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existingState = newMap.get(siteId);
|
||||
if (existingState) {
|
||||
newMap.set(siteId, { ...existingState, containers });
|
||||
}
|
||||
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 => {
|
||||
return (
|
||||
dockerStates.get(siteId) || {
|
||||
isEnabled: false,
|
||||
isAvailable: false,
|
||||
containers: []
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
||||
const site = sites.find((site) => site.siteId === data.siteId);
|
||||
|
||||
const isHttp = baseForm.watch("http");
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
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]
|
||||
);
|
||||
|
||||
async function onSubmit() {
|
||||
setCreateLoading(true);
|
||||
@@ -638,82 +566,18 @@ export default function Page() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoadingPage(true);
|
||||
// Initialize Docker for newt sites
|
||||
for (const site of sites) {
|
||||
if (site.type === "newt") {
|
||||
initializeDockerForSite(site.siteId);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSites = async () => {
|
||||
const res = await api
|
||||
.get<
|
||||
AxiosResponse<ListSitesResponse>
|
||||
>(`/org/${orgId}/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();
|
||||
}, []);
|
||||
// If there's at least one site, set it as the default in the form
|
||||
if (sites.length > 0) {
|
||||
addTargetForm.setValue("siteId", sites[0].siteId);
|
||||
}
|
||||
}, [sites]);
|
||||
|
||||
function TargetHealthCheck(targetId: number, config: any) {
|
||||
setTargets(
|
||||
@@ -729,16 +593,15 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
const openHealthCheckDialog = (target: LocalTarget) => {
|
||||
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
||||
console.log(target);
|
||||
setSelectedTargetForHealthCheck(target);
|
||||
setHealthCheckDialogOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
||||
const isHttp = baseForm.watch("http");
|
||||
const isHttp = baseForm.watch("http");
|
||||
|
||||
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
header: () => (
|
||||
@@ -875,7 +738,7 @@ export default function Page() {
|
||||
trigger={
|
||||
<Button
|
||||
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
|
||||
value={{
|
||||
@@ -899,7 +762,7 @@ export default function Page() {
|
||||
trigger={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full max-w-[200px]"
|
||||
className="w-full max-w-50"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("matchPath")}
|
||||
@@ -918,216 +781,16 @@ export default function Page() {
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: () => <span className="p-3">{t("address")}</span>,
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<ResourceTargetAddressItem
|
||||
isHttp={isHttp}
|
||||
sites={sites}
|
||||
getDockerStateForSite={getDockerStateForSite}
|
||||
proxyTarget={row.original}
|
||||
refreshContainersForSite={refreshContainersForSite}
|
||||
updateTarget={updateTarget}
|
||||
/>
|
||||
),
|
||||
size: 400,
|
||||
minSize: 350,
|
||||
maxSize: 500
|
||||
@@ -1186,7 +849,7 @@ export default function Page() {
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={noPathMatch}
|
||||
className="w-full max-w-[200px]"
|
||||
className="w-full max-w-50"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("rewritePath")}
|
||||
@@ -1265,9 +928,17 @@ export default function Page() {
|
||||
actionsColumn
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const columns = getColumns();
|
||||
}, [
|
||||
isAdvancedMode,
|
||||
isHttp,
|
||||
sites,
|
||||
updateTarget,
|
||||
getDockerStateForSite,
|
||||
refreshContainersForSite,
|
||||
openHealthCheckDialog,
|
||||
removeTarget,
|
||||
t
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: targets,
|
||||
@@ -1649,9 +1320,6 @@ export default function Page() {
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
{/* <TableCaption> */}
|
||||
{/* {t('targetNoOneDescription')} */}
|
||||
{/* </TableCaption> */}
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -110,6 +110,15 @@ export default function BlueprintDetailsForm({
|
||||
Dashboard
|
||||
</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>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
|
||||
@@ -128,6 +128,19 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
</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 { Download } from "lucide-react";
|
||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||
import { SiAndroid } from "react-icons/si";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import DismissableBanner from "./DismissableBanner";
|
||||
@@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => {
|
||||
Linux
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -94,12 +94,6 @@ export default function DomainPicker({
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
|
||||
console.log({
|
||||
defaultFullDomain,
|
||||
defaultSubdomain,
|
||||
defaultDomainId
|
||||
});
|
||||
|
||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||
orgQueries.domains({ orgId })
|
||||
);
|
||||
@@ -369,9 +363,6 @@ export default function DomainPicker({
|
||||
setSelectedProvidedDomain(null);
|
||||
}
|
||||
|
||||
console.log({
|
||||
setSelectedBaseDomain: option
|
||||
});
|
||||
setSelectedBaseDomain(option);
|
||||
setOpen(false);
|
||||
|
||||
@@ -442,9 +433,6 @@ export default function DomainPicker({
|
||||
0,
|
||||
providedDomainsShown
|
||||
);
|
||||
console.log({
|
||||
displayedProvidedOptions
|
||||
});
|
||||
|
||||
const selectedDomainNamespaceId =
|
||||
selectedProvidedDomain?.domainNamespaceId ??
|
||||
|
||||
@@ -143,7 +143,6 @@ export default function LoginOrgSelector({
|
||||
<IdpLoginButtons
|
||||
idps={idps}
|
||||
redirect={redirect}
|
||||
orgId={org.orgId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
241
src/components/resource-target-address-item.tsx
Normal file
241
src/components/resource-target-address-item.tsx
Normal file
@@ -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"
|
||||
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",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50",
|
||||
"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
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -36,7 +36,9 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -60,7 +62,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
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" &&
|
||||
"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
|
||||
@@ -73,7 +75,7 @@ function SelectContent({
|
||||
className={cn(
|
||||
"p-1",
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user