Compare commits

..

1 Commits

Author SHA1 Message Date
Owen
49001f6dda Fix not detecting rc release in sign and package 2026-01-21 15:45:20 -08:00
51 changed files with 1043 additions and 675 deletions

View File

@@ -442,7 +442,7 @@ jobs:
# Determine if this is an RC release # Determine if this is an RC release
IS_RC="false" IS_RC="false"
if [[ "$TAG" == *"-rc."* ]]; then if echo "$TAG" | grep -qE "rc[0-9]+$"; then
IS_RC="true" IS_RC="true"
fi fi

View File

@@ -4,13 +4,13 @@
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -19,4 +19,4 @@
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.formatOnSave": true "editor.formatOnSave": true
} }

View File

@@ -35,12 +35,6 @@
</div> </div>
<p align="center">
<a href="https://docs.pangolin.net/careers/join-us">
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
</a>
</p>
<p align="center"> <p align="center">
<strong> <strong>
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a> Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
@@ -80,8 +74,6 @@ Download the Pangolin client for your platform:
- [Mac](https://pangolin.net/downloads/mac) - [Mac](https://pangolin.net/downloads/mac)
- [Windows](https://pangolin.net/downloads/windows) - [Windows](https://pangolin.net/downloads/windows)
- [Linux](https://pangolin.net/downloads/linux) - [Linux](https://pangolin.net/downloads/linux)
- [iOS](https://pangolin.net/downloads/ios)
- [Android](https://pangolin.net/downloads/android)
## Get Started ## Get Started

72
blueprint.py Normal file
View File

@@ -0,0 +1,72 @@
import requests
import yaml
import json
import base64
# The file path for the YAML file to be read
# You can change this to the path of your YAML file
YAML_FILE_PATH = 'blueprint.yaml'
# The API endpoint and headers from the curl request
API_URL = 'http://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)

70
blueprint.yaml Normal file
View File

@@ -0,0 +1,70 @@
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

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Активирана защитна стена.", "firewallEnabled": "Активирана защитна стена.",
"autoUpdatesEnabled": "Активирани автоматични актуализации.", "autoUpdatesEnabled": "Активирани автоматични актуализации.",
"tpmAvailable": "TPM е на разположение.", "tpmAvailable": "TPM е на разположение.",
"windowsDefenderEnabled": "Windows Defender е активиран.",
"macosSipEnabled": "Protection на системната цялост (SIP).", "macosSipEnabled": "Protection на системната цялост (SIP).",
"macosGatekeeperEnabled": "Gatekeeper.", "macosGatekeeperEnabled": "Gatekeeper.",
"macosFirewallStealthMode": "Скрит режим на защитната стена.", "macosFirewallStealthMode": "Скрит режим на защитната стена.",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Firewall povolen", "firewallEnabled": "Firewall povolen",
"autoUpdatesEnabled": "Automatické aktualizace povoleny", "autoUpdatesEnabled": "Automatické aktualizace povoleny",
"tpmAvailable": "TPM k dispozici", "tpmAvailable": "TPM k dispozici",
"windowsDefenderEnabled": "Okna byla povolena",
"macosSipEnabled": "Ochrana systémové integrity (SIP)", "macosSipEnabled": "Ochrana systémové integrity (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Režim neviditelnosti firewallu", "macosFirewallStealthMode": "Režim neviditelnosti firewallu",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Firewall aktiviert", "firewallEnabled": "Firewall aktiviert",
"autoUpdatesEnabled": "Automatische Updates aktiviert", "autoUpdatesEnabled": "Automatische Updates aktiviert",
"tpmAvailable": "TPM verfügbar", "tpmAvailable": "TPM verfügbar",
"windowsDefenderEnabled": "Windows Defender aktiviert",
"macosSipEnabled": "Schutz der Systemintegrität (SIP)", "macosSipEnabled": "Schutz der Systemintegrität (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth-Modus", "macosFirewallStealthMode": "Firewall Stealth-Modus",

View File

@@ -2510,7 +2510,7 @@
"firewallEnabled": "Firewall Enabled", "firewallEnabled": "Firewall Enabled",
"autoUpdatesEnabled": "Auto Updates Enabled", "autoUpdatesEnabled": "Auto Updates Enabled",
"tpmAvailable": "TPM Available", "tpmAvailable": "TPM Available",
"windowsAntivirusEnabled": "Antivirus Enabled", "windowsDefenderEnabled": "Windows Defender Enabled",
"macosSipEnabled": "System Integrity Protection (SIP)", "macosSipEnabled": "System Integrity Protection (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth Mode", "macosFirewallStealthMode": "Firewall Stealth Mode",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Cortafuegos activado", "firewallEnabled": "Cortafuegos activado",
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas", "autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
"tpmAvailable": "TPM disponible", "tpmAvailable": "TPM disponible",
"windowsDefenderEnabled": "Windows Defender activado",
"macosSipEnabled": "Protección de integridad del sistema (SIP)", "macosSipEnabled": "Protección de integridad del sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Sigilo Firewall", "macosFirewallStealthMode": "Modo Sigilo Firewall",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Pare-feu activé", "firewallEnabled": "Pare-feu activé",
"autoUpdatesEnabled": "Mises à jour automatiques activées", "autoUpdatesEnabled": "Mises à jour automatiques activées",
"tpmAvailable": "TPM disponible", "tpmAvailable": "TPM disponible",
"windowsDefenderEnabled": "Windows Defender activé",
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)", "macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Mode furtif du pare-feu", "macosFirewallStealthMode": "Mode furtif du pare-feu",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Firewall Abilitato", "firewallEnabled": "Firewall Abilitato",
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati", "autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
"tpmAvailable": "TPM Disponibile", "tpmAvailable": "TPM Disponibile",
"windowsDefenderEnabled": "Windows Defender Abilitato",
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)", "macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo Del Firewall", "macosFirewallStealthMode": "Modo Furtivo Del Firewall",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "방화벽 활성화", "firewallEnabled": "방화벽 활성화",
"autoUpdatesEnabled": "자동 업데이트 활성화", "autoUpdatesEnabled": "자동 업데이트 활성화",
"tpmAvailable": "TPM 사용 가능", "tpmAvailable": "TPM 사용 가능",
"windowsDefenderEnabled": "Windows Defender 활성화",
"macosSipEnabled": "시스템 무결성 보호 (SIP)", "macosSipEnabled": "시스템 무결성 보호 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "방화벽 스텔스 모드", "macosFirewallStealthMode": "방화벽 스텔스 모드",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Brannmur aktivert", "firewallEnabled": "Brannmur aktivert",
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert", "autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
"tpmAvailable": "TPM tilgjengelig", "tpmAvailable": "TPM tilgjengelig",
"windowsDefenderEnabled": "Windows svarer aktivert",
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)", "macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Brannmur Usynlig Modus", "macosFirewallStealthMode": "Brannmur Usynlig Modus",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Firewall ingeschakeld", "firewallEnabled": "Firewall ingeschakeld",
"autoUpdatesEnabled": "Auto Updates Ingeschakeld", "autoUpdatesEnabled": "Auto Updates Ingeschakeld",
"tpmAvailable": "TPM beschikbaar", "tpmAvailable": "TPM beschikbaar",
"windowsDefenderEnabled": "Windows Verdediger ingeschakeld",
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)", "macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Verberg Modus", "macosFirewallStealthMode": "Firewall Verberg Modus",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Zapora włączona", "firewallEnabled": "Zapora włączona",
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone", "autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
"tpmAvailable": "TPM dostępne", "tpmAvailable": "TPM dostępne",
"windowsDefenderEnabled": "Obrońca Windows włączony",
"macosSipEnabled": "Ochrona integralności systemu (SIP)", "macosSipEnabled": "Ochrona integralności systemu (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Tryb Stealth zapory", "macosFirewallStealthMode": "Tryb Stealth zapory",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Firewall habilitado", "firewallEnabled": "Firewall habilitado",
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas", "autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
"tpmAvailable": "TPM disponível", "tpmAvailable": "TPM disponível",
"windowsDefenderEnabled": "Defensor do Windows habilitado",
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)", "macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo do Firewall", "macosFirewallStealthMode": "Modo Furtivo do Firewall",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Брандмауэр включен", "firewallEnabled": "Брандмауэр включен",
"autoUpdatesEnabled": "Автоматические обновления включены", "autoUpdatesEnabled": "Автоматические обновления включены",
"tpmAvailable": "Доступно TPM", "tpmAvailable": "Доступно TPM",
"windowsDefenderEnabled": "Защитник Windows включен",
"macosSipEnabled": "Защита целостности системы (SIP)", "macosSipEnabled": "Защита целостности системы (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Стилс-режим брандмауэра", "macosFirewallStealthMode": "Стилс-режим брандмауэра",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "Güvenlik Duvarı Etkin", "firewallEnabled": "Güvenlik Duvarı Etkin",
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin", "autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
"tpmAvailable": "TPM Mevcut", "tpmAvailable": "TPM Mevcut",
"windowsDefenderEnabled": "Windows Defender Etkin",
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)", "macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu", "macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",

View File

@@ -2510,6 +2510,7 @@
"firewallEnabled": "防火墙已启用", "firewallEnabled": "防火墙已启用",
"autoUpdatesEnabled": "启用自动更新", "autoUpdatesEnabled": "启用自动更新",
"tpmAvailable": "TPM 可用", "tpmAvailable": "TPM 可用",
"windowsDefenderEnabled": "Windows Defender 已启用",
"macosSipEnabled": "系统完整性保护 (SIP)", "macosSipEnabled": "系统完整性保护 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "防火墙隐形模式", "macosFirewallStealthMode": "防火墙隐形模式",

View File

@@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", {
// Windows-specific posture check information // Windows-specific posture check information
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled") windowsDefenderEnabled: boolean("windowsDefenderEnabled")
.notNull() .notNull()
.default(false), .default(false),
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
// Windows-specific posture check information // Windows-specific posture check information
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled") windowsDefenderEnabled: boolean("windowsDefenderEnabled")
.notNull() .notNull()
.default(false), .default(false),

View File

@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
// Windows-specific posture check information // Windows-specific posture check information
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", { windowsDefenderEnabled: integer("windowsDefenderEnabled", {
mode: "boolean" mode: "boolean"
}) })
.notNull() .notNull()
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
// Windows-specific posture check information // Windows-specific posture check information
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", { windowsDefenderEnabled: integer("windowsDefenderEnabled", {
mode: "boolean" mode: "boolean"
}) })
.notNull() .notNull()

View File

@@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db"; import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
import { build } from "@server/build"; import { build } from "@server/build";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
@@ -213,7 +213,11 @@ export async function updateProxyResources(
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) { if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }
@@ -590,7 +594,7 @@ export async function updateProxyResources(
existingRule.action !== getRuleAction(rule.action) || existingRule.action !== getRuleAction(rule.action) ||
existingRule.match !== rule.match.toUpperCase() || existingRule.match !== rule.match.toUpperCase() ||
existingRule.value !== existingRule.value !==
getRuleValue(rule.match.toUpperCase(), rule.value) || getRuleValue(rule.match.toUpperCase(), rule.value) ||
existingRule.priority !== intendedPriority existingRule.priority !== intendedPriority
) { ) {
validateRule(rule); validateRule(rule);
@@ -649,7 +653,11 @@ export async function updateProxyResources(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) { if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }

View File

@@ -14,7 +14,7 @@ import {
} from "@server/db"; } from "@server/db";
import { getUniqueClientName } from "@server/db/names"; import { getUniqueClientName } from "@server/db/names";
import { getNextAvailableClientSubnet } from "@server/lib/ip"; import { getNextAvailableClientSubnet } from "@server/lib/ip";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import logger from "@server/logger"; import logger from "@server/logger";
import { sendTerminateClient } from "@server/routers/client/terminate"; import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";

View File

@@ -1,3 +1,17 @@
import { build } from "@server/build";
import license from "#dynamic/license/license";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> { export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
return false; if (build === "enterprise") {
} return await license.isUnlocked();
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
return tier === TierId.STANDARD;
}
return true;
}

View File

@@ -1,30 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { 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;
}

View File

@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "@server/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { import {
approvals, approvals,

View File

@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import { build } from "@server/build"; import { build } from "@server/build";
import { approvals, clients, db, orgs, type Approval } from "@server/db"; import { approvals, clients, db, orgs, type Approval } from "@server/db";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "@server/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { and, eq, type InferInsertModel } from "drizzle-orm"; import { and, eq, type InferInsertModel } from "drizzle-orm";

View File

@@ -26,8 +26,7 @@ const applyBlueprintSchema = z
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}` message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
}); });
} }
}), })
source: z.enum(["API", "UI", "CLI"]).optional()
}) })
.strict(); .strict();
@@ -85,7 +84,7 @@ export async function applyYAMLBlueprint(
); );
} }
const { blueprint: contents, name, source = "UI" } = parsedBody.data; const { blueprint: contents, name } = parsedBody.data;
logger.debug(`Received blueprint:`, contents); logger.debug(`Received blueprint:`, contents);
@@ -108,7 +107,7 @@ export async function applyYAMLBlueprint(
blueprint = await applyBlueprint({ blueprint = await applyBlueprint({
orgId, orgId,
name, name,
source, source: "UI",
configData: parsedConfig configData: parsedConfig
}); });
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,6 @@
import type { Blueprint } from "@server/db"; import type { Blueprint } from "@server/db";
export type BlueprintSource = "API" | "UI" | "NEWT" | "CLI"; export type BlueprintSource = "API" | "UI" | "NEWT";
export type BlueprintData = Omit<Blueprint, "source"> & { export type BlueprintData = Omit<Blueprint, "source"> & {
source: BlueprintSource; source: BlueprintSource;

View File

@@ -12,7 +12,6 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names"; import { getUserDeviceName } from "@server/db/names";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const getClientSchema = z.strictObject({ const getClientSchema = z.strictObject({
clientId: z clientId: z
@@ -59,7 +58,7 @@ type PostureData = {
firewallEnabled?: boolean | null; firewallEnabled?: boolean | null;
autoUpdatesEnabled?: boolean | null; autoUpdatesEnabled?: boolean | null;
tpmAvailable?: boolean | null; tpmAvailable?: boolean | null;
windowsAntivirusEnabled?: boolean | null; windowsDefenderEnabled?: boolean | null;
macosSipEnabled?: boolean | null; macosSipEnabled?: boolean | null;
macosGatekeeperEnabled?: boolean | null; macosGatekeeperEnabled?: boolean | null;
macosFirewallStealthMode?: boolean | null; macosFirewallStealthMode?: boolean | null;
@@ -76,123 +75,78 @@ function getPlatformPostureData(
const normalizedPlatform = platform?.toLowerCase() || "unknown"; const normalizedPlatform = platform?.toLowerCase() || "unknown";
const posture: PostureData = {}; const posture: PostureData = {};
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status // Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender
if (normalizedPlatform === "windows") { if (normalizedPlatform === "windows") {
if ( if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if ( if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if ( if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
fingerprint.tpmAvailable !== null && posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
fingerprint.tpmAvailable !== undefined }
) { if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
posture.tpmAvailable = fingerprint.tpmAvailable; posture.tpmAvailable = fingerprint.tpmAvailable;
} }
if ( if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) {
fingerprint.windowsAntivirusEnabled !== null && posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled;
fingerprint.windowsAntivirusEnabled !== undefined
) {
posture.windowsAntivirusEnabled =
fingerprint.windowsAntivirusEnabled;
} }
} }
// macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode // macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
else if (normalizedPlatform === "macos") { else if (normalizedPlatform === "macos") {
if ( if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if ( if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
fingerprint.biometricsEnabled !== null &&
fingerprint.biometricsEnabled !== undefined
) {
posture.biometricsEnabled = fingerprint.biometricsEnabled; posture.biometricsEnabled = fingerprint.biometricsEnabled;
} }
if ( if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if ( if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) {
fingerprint.macosSipEnabled !== null &&
fingerprint.macosSipEnabled !== undefined
) {
posture.macosSipEnabled = fingerprint.macosSipEnabled; posture.macosSipEnabled = fingerprint.macosSipEnabled;
} }
if ( if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) {
fingerprint.macosGatekeeperEnabled !== null &&
fingerprint.macosGatekeeperEnabled !== undefined
) {
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled; posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
} }
if ( if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
fingerprint.macosFirewallStealthMode !== null && posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
fingerprint.macosFirewallStealthMode !== undefined
) {
posture.macosFirewallStealthMode =
fingerprint.macosFirewallStealthMode;
} }
if ( if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
fingerprint.autoUpdatesEnabled !== null &&
fingerprint.autoUpdatesEnabled !== undefined
) {
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled; posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
} }
} }
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability // Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
else if (normalizedPlatform === "linux") { else if (normalizedPlatform === "linux") {
if ( if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if ( if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if ( if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) {
fingerprint.linuxAppArmorEnabled !== null &&
fingerprint.linuxAppArmorEnabled !== undefined
) {
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled; posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
} }
if ( if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) {
fingerprint.linuxSELinuxEnabled !== null &&
fingerprint.linuxSELinuxEnabled !== undefined
) {
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled; posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
} }
if ( if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
fingerprint.tpmAvailable !== null &&
fingerprint.tpmAvailable !== undefined
) {
posture.tpmAvailable = fingerprint.tpmAvailable; posture.tpmAvailable = fingerprint.tpmAvailable;
} }
} }
// iOS: Biometric configuration // iOS: Biometric configuration
else if (normalizedPlatform === "ios") { else if (normalizedPlatform === "ios") {
// none supported yet if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
posture.biometricsEnabled = fingerprint.biometricsEnabled;
}
} }
// Android: Screen lock, Biometric configuration, Hard drive encryption // Android: Screen lock, Biometric configuration, Hard drive encryption
else if (normalizedPlatform === "android") { else if (normalizedPlatform === "android") {
if ( if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
fingerprint.diskEncrypted !== null && posture.biometricsEnabled = fingerprint.biometricsEnabled;
fingerprint.diskEncrypted !== undefined }
) { if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
} }
@@ -289,27 +243,23 @@ export async function getClient(
// Build fingerprint data if available // Build fingerprint data if available
const fingerprintData = client.currentFingerprint const fingerprintData = client.currentFingerprint
? { ? {
username: client.currentFingerprint.username || null, username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null, hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null, platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null, osVersion: client.currentFingerprint.osVersion || null,
kernelVersion: kernelVersion:
client.currentFingerprint.kernelVersion || null, client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null, arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null, deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null, serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null, firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null lastSeen: client.currentFingerprint.lastSeen || null
} }
: null; : null;
// Build posture data if available (platform-specific) // Build posture data if available (platform-specific)
// Only return posture data if org is licensed/subscribed
let postureData: PostureData | null = null; let postureData: PostureData | null = null;
const isOrgLicensed = await isLicensedOrSubscribed( if (build !== "oss") {
client.clients.orgId
);
if (isOrgLicensed) {
postureData = getPlatformPostureData( postureData = getPlatformPostureData(
client.currentFingerprint?.platform || null, client.currentFingerprint?.platform || null,
client.currentFingerprint client.currentFingerprint

View File

@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false, autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
tpmAvailable: postures.tpmAvailable ?? false, tpmAvailable: postures.tpmAvailable ?? false,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false, windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false,
macosSipEnabled: postures.macosSipEnabled ?? false, macosSipEnabled: postures.macosSipEnabled ?? false,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false, macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled, windowsDefenderEnabled: postures.windowsDefenderEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled, windowsDefenderEnabled: postures.windowsDefenderEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled, windowsDefenderEnabled: postures.windowsDefenderEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled, windowsDefenderEnabled: postures.windowsDefenderEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,

View File

@@ -46,12 +46,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
logger.debug("Handling fingerprint insertion for olm register...", {
olmId: olm.olmId,
fingerprint,
postures
});
await handleFingerprintInsertion(olm, fingerprint, postures); await handleFingerprintInsertion(olm, fingerprint, postures);
if ( if (
@@ -149,7 +143,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
if (policyCheck.policies?.passwordAge?.compliant === false) { if (!policyCheck.policies?.passwordAge?.compliant === false) {
logger.warn( logger.warn(
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}` `Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
); );
@@ -159,7 +153,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
); );
return; return;
} else if ( } else if (
policyCheck.policies?.maxSessionLength?.compliant === false !policyCheck.policies?.maxSessionLength?.compliant === false
) { ) {
logger.warn( logger.warn(
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}` `Olm user ${olm.userId} has non-compliant session length for org ${orgId}`

View File

@@ -13,7 +13,7 @@ import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing"; import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { cache } from "@server/lib/cache"; import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const updateOrgParamsSchema = z.strictObject({ const updateOrgParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -89,7 +89,7 @@ export async function updateOrg(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) { if (build == "enterprise" && !isLicensed) {
parsedBody.data.requireTwoFactor = undefined; parsedBody.data.requireTwoFactor = undefined;
parsedBody.data.maxSessionLengthHours = undefined; parsedBody.data.maxSessionLengthHours = undefined;
parsedBody.data.passwordExpiryDays = undefined; parsedBody.data.passwordExpiryDays = undefined;

View File

@@ -23,7 +23,7 @@ import { OpenAPITags } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const updateResourceParamsSchema = z.strictObject({ const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive()) resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -342,7 +342,11 @@ async function updateHttpResource(
} }
const isLicensed = await isLicensedOrSubscribed(resource.orgId); const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (!isLicensed) { if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined; updateData.maintenanceModeType = undefined;
updateData.maintenanceTitle = undefined; updateData.maintenanceTitle = undefined;

View File

@@ -11,7 +11,7 @@ import { ActionsEnum } from "@server/auth/actions";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
const createRoleParamsSchema = z.strictObject({ const createRoleParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -101,7 +101,7 @@ export async function createRole(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) { if (build === "oss" || !isLicensed) {
roleData.requireDeviceApproval = undefined; roleData.requireDeviceApproval = undefined;
} }

View File

@@ -8,7 +8,8 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const updateRoleParamsSchema = z.strictObject({ const updateRoleParamsSchema = z.strictObject({
@@ -111,7 +112,7 @@ export async function updateRole(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (!isLicensed) { if (build === "oss" || !isLicensed) {
updateData.requireDeviceApproval = undefined; updateData.requireDeviceApproval = undefined;
} }

View File

@@ -49,7 +49,7 @@ export default async function migration() {
"firewallEnabled" boolean DEFAULT false NOT NULL, "firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" boolean DEFAULT false NOT NULL, "tpmAvailable" boolean DEFAULT false NOT NULL,
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL, "windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
"macosSipEnabled" boolean DEFAULT false NOT NULL, "macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL, "macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
@@ -75,7 +75,7 @@ export default async function migration() {
"firewallEnabled" boolean DEFAULT false NOT NULL, "firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" boolean DEFAULT false NOT NULL, "tpmAvailable" boolean DEFAULT false NOT NULL,
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL, "windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
"macosSipEnabled" boolean DEFAULT false NOT NULL, "macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL, "macosFirewallStealthMode" boolean DEFAULT false NOT NULL,

View File

@@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' (
'firewallEnabled' integer DEFAULT false NOT NULL, 'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' integer DEFAULT false NOT NULL, 'tpmAvailable' integer DEFAULT false NOT NULL,
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL, 'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
'macosSipEnabled' integer DEFAULT false NOT NULL, 'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' integer DEFAULT false NOT NULL, 'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
'firewallEnabled' integer DEFAULT false NOT NULL, 'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' integer DEFAULT false NOT NULL, 'tpmAvailable' integer DEFAULT false NOT NULL,
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL, 'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
'macosSipEnabled' integer DEFAULT false NOT NULL, 'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' integer DEFAULT false NOT NULL, 'macosFirewallStealthMode' integer DEFAULT false NOT NULL,

View File

@@ -19,6 +19,17 @@ export interface ApprovalFeedPageProps {
export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) { export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
const params = await props.params; const params = await props.params;
let approvals: ApprovalItem[] = [];
const res = await internal
.get<
AxiosResponse<{ approvals: ApprovalItem[] }>
>(`/org/${params.orgId}/approvals`, await authCookieHeader())
.catch((e) => {});
if (res && res.status === 200) {
approvals = res.data.data.approvals;
}
let org: GetOrgResponse | null = null; let org: GetOrgResponse | null = null;
const orgRes = await getCachedOrg(params.orgId); const orgRes = await getCachedOrg(params.orgId);

View File

@@ -656,17 +656,17 @@ export default function GeneralPage() {
</InfoSection> </InfoSection>
)} )}
{client.posture.windowsAntivirusEnabled !== null && {client.posture.windowsDefenderEnabled !== null &&
client.posture.windowsAntivirusEnabled !== undefined && ( client.posture.windowsDefenderEnabled !== undefined && (
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("windowsAntivirusEnabled")} {t("windowsDefenderEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser {isPaidUser
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.windowsAntivirusEnabled .windowsDefenderEnabled
) )
: "-"} : "-"}
</InfoSectionContent> </InfoSectionContent>

View File

@@ -11,6 +11,7 @@ import {
SelectValue SelectValue
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { HeadersInput } from "@app/components/HeadersInput"; import { HeadersInput } from "@app/components/HeadersInput";
import { import {
PathMatchDisplay, PathMatchDisplay,
@@ -18,7 +19,6 @@ import {
PathRewriteDisplay, PathRewriteDisplay,
PathRewriteModal PathRewriteModal
} from "@app/components/PathMatchRenameModal"; } from "@app/components/PathMatchRenameModal";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
@@ -30,6 +30,15 @@ import {
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import { Badge } from "@app/components/ui/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import { import {
Form, Form,
FormControl, FormControl,
@@ -39,6 +48,11 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { import {
Table, Table,
TableBody, TableBody,
@@ -59,9 +73,12 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { cn } from "@app/lib/cn";
import { DockerManager, DockerState } from "@app/lib/docker"; import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { orgQueries, resourceQueries } from "@app/lib/queries"; import { orgQueries, resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { tlsNameSchema } from "@server/lib/schemas"; import { tlsNameSchema } from "@server/lib/schemas";
import { type GetResourceResponse } from "@server/routers/resource"; import { type GetResourceResponse } from "@server/routers/resource";
import type { ListSitesResponse } from "@server/routers/site"; import type { ListSitesResponse } from "@server/routers/site";
@@ -81,6 +98,7 @@ import {
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { import {
AlertTriangle, AlertTriangle,
CheckIcon,
CircleCheck, CircleCheck,
CircleX, CircleX,
Info, Info,
@@ -89,7 +107,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react"; import { use, useActionState, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -184,7 +202,7 @@ function ProxyResourceTargetsForm({
setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
}; };
const refreshContainersForSite = useCallback(async (siteId: number) => { const refreshContainersForSite = async (siteId: number) => {
const dockerManager = new DockerManager(api, siteId); const dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers(); const containers = await dockerManager.fetchContainers();
@@ -196,9 +214,9 @@ function ProxyResourceTargetsForm({
} }
return newMap; return newMap;
}); });
}, [api]); };
const getDockerStateForSite = useCallback((siteId: number): DockerState => { const getDockerStateForSite = (siteId: number): DockerState => {
return ( return (
dockerStates.get(siteId) || { dockerStates.get(siteId) || {
isEnabled: false, isEnabled: false,
@@ -206,7 +224,7 @@ function ProxyResourceTargetsForm({
containers: [] containers: []
} }
); );
}, [dockerStates]); };
const [isAdvancedMode, setIsAdvancedMode] = useState(() => { const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -216,40 +234,8 @@ function ProxyResourceTargetsForm({
return false; return false;
}); });
const isHttp = resource.http; const getColumns = (): ColumnDef<LocalTarget>[] => {
const isHttp = resource.http;
const removeTarget = useCallback((targetId: number) => {
setTargets((prevTargets) => {
const targetToRemove = prevTargets.find((target) => target.targetId === targetId);
if (targetToRemove && !targetToRemove.new) {
setTargetsToRemove((prev) => [...prev, targetId]);
}
return prevTargets.filter((target) => target.targetId !== targetId);
});
}, []);
const updateTarget = useCallback((targetId: number, data: Partial<LocalTarget>) => {
setTargets((prevTargets) => {
const site = sites.find((site) => site.siteId === data.siteId);
return prevTargets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
);
});
}, [sites]);
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
}, []);
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
const priorityColumn: ColumnDef<LocalTarget> = { const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority", id: "priority",
@@ -433,15 +419,213 @@ function ProxyResourceTargetsForm({
accessorKey: "address", accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>, header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => { cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
);
const handleContainerSelectForTarget = (
hostname: string,
port?: number
) => {
updateTarget(row.original.targetId, {
...row.original,
ip: hostname,
...(port && { port: port })
});
};
return ( return (
<ResourceTargetAddressItem <div className="flex items-center w-full">
isHttp={isHttp} <div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
sites={sites} {selectedSite &&
getDockerStateForSite={getDockerStateForSite} selectedSite.type === "newt" &&
proxyTarget={row.original} (() => {
refreshContainersForSite={refreshContainersForSite} const dockerState = getDockerStateForSite(
updateTarget={updateTarget} 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>
); );
}, },
size: 400, size: 400,
@@ -581,7 +765,7 @@ function ProxyResourceTargetsForm({
actionsColumn actionsColumn
]; ];
} }
}, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]); };
function addNewTarget() { function addNewTarget() {
const isHttp = resource.http; const isHttp = resource.http;
@@ -622,6 +806,32 @@ function ProxyResourceTargetsForm({
setTargets((prev) => [...prev, newTarget]); setTargets((prev) => [...prev, newTarget]);
} }
const removeTarget = (targetId: number) => {
setTargets([
...targets.filter((target) => target.targetId !== targetId)
]);
if (!targets.find((target) => target.targetId === targetId)?.new) {
setTargetsToRemove([...targetsToRemove, targetId]);
}
};
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
const site = sites.find((site) => site.siteId === data.siteId);
setTargets(
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
)
);
}
function updateTargetHealthCheck(targetId: number, config: any) { function updateTargetHealthCheck(targetId: number, config: any) {
setTargets( setTargets(
targets.map((target) => targets.map((target) =>
@@ -636,6 +846,14 @@ function ProxyResourceTargetsForm({
); );
} }
const openHealthCheckDialog = (target: LocalTarget) => {
console.log(target);
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
};
const columns = getColumns();
const table = useReactTable({ const table = useReactTable({
data: targets, data: targets,
columns, columns,

View File

@@ -1,14 +1,5 @@
"use client"; "use client";
import CopyTextBox from "@app/components/CopyTextBox";
import DomainPicker from "@app/components/DomainPicker";
import HealthCheckDialog from "@app/components/HealthCheckDialog";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
@@ -18,10 +9,6 @@ import {
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { StrategySelect } from "@app/components/StrategySelect";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@@ -31,7 +18,22 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { Button } from "@app/components/ui/button";
import { useParams, useRouter } from "next/navigation";
import { ListSitesResponse } from "@server/routers/site";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db";
import { StrategySelect } from "@app/components/StrategySelect";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -39,7 +41,48 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { Switch } from "@app/components/ui/switch"; 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 { import {
Table, Table,
TableBody, TableBody,
@@ -48,49 +91,30 @@ import {
TableHeader, TableHeader,
TableRow TableRow
} from "@app/components/ui/table"; } from "@app/components/ui/table";
import { Switch } from "@app/components/ui/switch";
import { ArrayElement } from "@server/types/ArrayElement";
import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { toASCII, toUnicode } from "punycode";
import { DomainRow } from "@app/components/DomainsTable";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger TooltipTrigger
} from "@app/components/ui/tooltip"; } from "@app/components/ui/tooltip";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { DockerManager, DockerState } from "@app/lib/docker";
import { orgQueries } from "@app/lib/queries";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { Resource } from "@server/db";
import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { ArrayElement } from "@server/types/ArrayElement";
import { useQuery } from "@tanstack/react-query";
import { import {
ColumnDef, PathMatchDisplay,
flexRender, PathMatchModal,
getCoreRowModel, PathRewriteDisplay,
getFilteredRowModel, PathRewriteModal
getPaginationRowModel, } from "@app/components/PathMatchRenameModal";
getSortedRowModel, import { Badge } from "@app/components/ui/badge";
useReactTable import HealthCheckDialog from "@app/components/HealthCheckDialog";
} from "@tanstack/react-table"; import { SwitchInput } from "@app/components/SwitchInput";
import { AxiosResponse } from "axios";
import {
CircleCheck,
CircleX,
Info,
Plus,
Settings,
SquareArrowOutUpRight
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { toASCII } from "punycode";
import { useEffect, useMemo, useState, useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
const baseResourceFormSchema = z.object({ const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
@@ -180,6 +204,10 @@ const addTargetSchema = z
} }
); );
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
type TcpUdpResourceFormValues = z.infer<typeof tcpUdpResourceFormSchema>;
type ResourceType = "http" | "raw"; type ResourceType = "http" | "raw";
interface ResourceTypeOption { interface ResourceTypeOption {
@@ -189,7 +217,7 @@ interface ResourceTypeOption {
disabled?: boolean; disabled?: boolean;
} }
export type LocalTarget = Omit< type LocalTarget = Omit<
ArrayElement<ListTargetsResponse["targets"]> & { ArrayElement<ListTargetsResponse["targets"]> & {
new?: boolean; new?: boolean;
updated?: boolean; updated?: boolean;
@@ -205,16 +233,18 @@ export default function Page() {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const { data: sites = [], isLoading: loadingPage } = useQuery( const [loadingPage, setLoadingPage] = useState(true);
orgQueries.sites({ orgId: orgId as string }) const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
); const [baseDomains, setBaseDomains] = useState<
{ domainId: string; baseDomain: string }[]
>([]);
const [createLoading, setCreateLoading] = useState(false); const [createLoading, setCreateLoading] = useState(false);
const [showSnippets, setShowSnippets] = useState(false); const [showSnippets, setShowSnippets] = useState(false);
const [niceId, setNiceId] = useState<string>(""); const [niceId, setNiceId] = useState<string>("");
// Target management state // Target management state
const [targets, setTargets] = useState<LocalTarget[]>([]); const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>( const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
new Map() new Map()
); );
@@ -375,60 +405,102 @@ export default function Page() {
setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
}; };
const refreshContainersForSite = useCallback( const refreshContainersForSite = async (siteId: number) => {
async (siteId: number) => { const dockerManager = new DockerManager(api, siteId);
const dockerManager = new DockerManager(api, siteId); const containers = await dockerManager.fetchContainers();
const containers = await dockerManager.fetchContainers();
setDockerStates((prev) => { setDockerStates((prev) => {
const newMap = new Map(prev); const newMap = new Map(prev);
const existingState = newMap.get(siteId); const existingState = newMap.get(siteId);
if (existingState) { if (existingState) {
newMap.set(siteId, { ...existingState, containers }); newMap.set(siteId, { ...existingState, containers });
} }
return newMap; return newMap;
});
},
[api]
);
const getDockerStateForSite = useCallback(
(siteId: number): DockerState => {
return (
dockerStates.get(siteId) || {
isEnabled: false,
isAvailable: false,
containers: []
}
);
},
[dockerStates]
);
const removeTarget = useCallback((targetId: number) => {
setTargets((prevTargets) => {
return prevTargets.filter((target) => target.targetId !== targetId);
}); });
}, []); };
const updateTarget = useCallback( const getDockerStateForSite = (siteId: number): DockerState => {
(targetId: number, data: Partial<LocalTarget>) => { return (
setTargets((prevTargets) => { dockerStates.get(siteId) || {
const site = sites.find((site) => site.siteId === data.siteId); isEnabled: false,
return prevTargets.map((target) => isAvailable: false,
target.targetId === targetId containers: []
? { }
...target, );
...data, };
updated: true,
siteType: site ? site.type : target.siteType async function addTarget(data: z.infer<typeof addTargetSchema>) {
} const site = sites.find((site) => site.siteId === data.siteId);
: target
); const isHttp = baseForm.watch("http");
});
}, const newTarget: LocalTarget = {
[sites] ...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
)
);
}
async function onSubmit() { async function onSubmit() {
setCreateLoading(true); setCreateLoading(true);
@@ -566,18 +638,82 @@ export default function Page() {
} }
useEffect(() => { useEffect(() => {
// Initialize Docker for newt sites const load = async () => {
for (const site of sites) { setLoadingPage(true);
if (site.type === "newt") {
initializeDockerForSite(site.siteId);
}
}
// If there's at least one site, set it as the default in the form const fetchSites = async () => {
if (sites.length > 0) { const res = await api
addTargetForm.setValue("siteId", sites[0].siteId); .get<
} AxiosResponse<ListSitesResponse>
}, [sites]); >(`/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();
}, []);
function TargetHealthCheck(targetId: number, config: any) { function TargetHealthCheck(targetId: number, config: any) {
setTargets( setTargets(
@@ -593,15 +729,16 @@ export default function Page() {
); );
} }
const openHealthCheckDialog = useCallback((target: LocalTarget) => { const openHealthCheckDialog = (target: LocalTarget) => {
console.log(target); console.log(target);
setSelectedTargetForHealthCheck(target); setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true); setHealthCheckDialogOpen(true);
}, []); };
const isHttp = baseForm.watch("http"); const getColumns = (): ColumnDef<LocalTarget>[] => {
const baseColumns: ColumnDef<LocalTarget>[] = [];
const isHttp = baseForm.watch("http");
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
const priorityColumn: ColumnDef<LocalTarget> = { const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority", id: "priority",
header: () => ( header: () => (
@@ -738,7 +875,7 @@ export default function Page() {
trigger={ trigger={
<Button <Button
variant="outline" variant="outline"
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-50" className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-[200px]"
> >
<PathMatchDisplay <PathMatchDisplay
value={{ value={{
@@ -762,7 +899,7 @@ export default function Page() {
trigger={ trigger={
<Button <Button
variant="outline" variant="outline"
className="w-full max-w-50" className="w-full max-w-[200px]"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
{t("matchPath")} {t("matchPath")}
@@ -781,16 +918,216 @@ export default function Page() {
const addressColumn: ColumnDef<LocalTarget> = { const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address", accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>, header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => ( cell: ({ row }) => {
<ResourceTargetAddressItem const selectedSite = sites.find(
isHttp={isHttp} (site) => site.siteId === row.original.siteId
sites={sites} );
getDockerStateForSite={getDockerStateForSite}
proxyTarget={row.original} const handleContainerSelectForTarget = (
refreshContainersForSite={refreshContainersForSite} hostname: string,
updateTarget={updateTarget} 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>
);
},
size: 400, size: 400,
minSize: 350, minSize: 350,
maxSize: 500 maxSize: 500
@@ -849,7 +1186,7 @@ export default function Page() {
<Button <Button
variant="outline" variant="outline"
disabled={noPathMatch} disabled={noPathMatch}
className="w-full max-w-50" className="w-full max-w-[200px]"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
{t("rewritePath")} {t("rewritePath")}
@@ -928,17 +1265,9 @@ export default function Page() {
actionsColumn actionsColumn
]; ];
} }
}, [ };
isAdvancedMode,
isHttp, const columns = getColumns();
sites,
updateTarget,
getDockerStateForSite,
refreshContainersForSite,
openHealthCheckDialog,
removeTarget,
t
]);
const table = useReactTable({ const table = useReactTable({
data: targets, data: targets,
@@ -1320,6 +1649,9 @@ export default function Page() {
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>
{/* <TableCaption> */}
{/* {t('targetNoOneDescription')} */}
{/* </TableCaption> */}
</Table> </Table>
</div> </div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

View File

@@ -110,15 +110,6 @@ export default function BlueprintDetailsForm({
Dashboard Dashboard
</Badge> </Badge>
)}{" "} )}{" "}
{blueprint.source === "CLI" && (
<Badge
variant="secondary"
className="inline-flex items-center gap-1 "
>
<Terminal className="w-3 h-3 flex-none" />
CLI
</Badge>
)}{" "}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>

View File

@@ -128,19 +128,6 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
</Badge> </Badge>
); );
} }
case "CLI": {
return (
<Badge
variant="secondary"
className="inline-flex items-center gap-1"
>
<span className="inline-flex items-center gap-1 ">
<Terminal className="w-3 h-3" />
CLI
</span>
</Badge>
);
}
} }
} }
}, },

View File

@@ -4,7 +4,6 @@ import React from "react";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import DismissableBanner from "./DismissableBanner"; import DismissableBanner from "./DismissableBanner";
@@ -62,34 +61,6 @@ export const ClientDownloadBanner = () => {
Linux Linux
</Button> </Button>
</Link> </Link>
<Link
href="https://pangolin.net/downloads/ios"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
<FaApple className="w-4 h-4" />
iOS
</Button>
</Link>
<Link
href="https://pangolin.net/downloads/android"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
<SiAndroid className="w-4 h-4" />
Android
</Button>
</Link>
</DismissableBanner> </DismissableBanner>
); );
}; };

View File

@@ -94,6 +94,12 @@ export default function DomainPicker({
const api = createApiClient({ env }); const api = createApiClient({ env });
const t = useTranslations(); const t = useTranslations();
console.log({
defaultFullDomain,
defaultSubdomain,
defaultDomainId
});
const { data = [], isLoading: loadingDomains } = useQuery( const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId }) orgQueries.domains({ orgId })
); );
@@ -363,6 +369,9 @@ export default function DomainPicker({
setSelectedProvidedDomain(null); setSelectedProvidedDomain(null);
} }
console.log({
setSelectedBaseDomain: option
});
setSelectedBaseDomain(option); setSelectedBaseDomain(option);
setOpen(false); setOpen(false);
@@ -433,6 +442,9 @@ export default function DomainPicker({
0, 0,
providedDomainsShown providedDomainsShown
); );
console.log({
displayedProvidedOptions
});
const selectedDomainNamespaceId = const selectedDomainNamespaceId =
selectedProvidedDomain?.domainNamespaceId ?? selectedProvidedDomain?.domainNamespaceId ??

View File

@@ -143,6 +143,7 @@ export default function LoginOrgSelector({
<IdpLoginButtons <IdpLoginButtons
idps={idps} idps={idps}
redirect={redirect} redirect={redirect}
orgId={org.orgId}
/> />
</div> </div>
</div> </div>

View File

@@ -1,241 +0,0 @@
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>
);
}

View File

@@ -44,8 +44,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0",
className className
)} )}
ref={ref} ref={ref}

View File

@@ -36,9 +36,7 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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", "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",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
// "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0",
className className
)} )}
{...props} {...props}
@@ -62,7 +60,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 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-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-sm",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
@@ -75,7 +73,7 @@ function SelectContent({
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)} )}
> >
{children} {children}