From 4abe83f8a9b44a0161352b114df94ab9698a7d9b Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 21 Jan 2026 16:36:35 -0800 Subject: [PATCH 01/14] Dont show bio info on android --- server/routers/client/getClient.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 01cb867c..18e99819 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -143,9 +143,6 @@ function getPlatformPostureData( } // Android: Screen lock, Biometric configuration, Hard drive encryption else if (normalizedPlatform === "android") { - if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { - posture.biometricsEnabled = fingerprint.biometricsEnabled; - } if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { posture.diskEncrypted = fingerprint.diskEncrypted; } From 8fa1701e06ee014c8bdb60f89b222314da19f053 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 21 Jan 2026 17:57:15 -0800 Subject: [PATCH 02/14] rename windowsDefenderEnabled --- messages/bg-BG.json | 1 - messages/cs-CZ.json | 1 - messages/de-DE.json | 1 - messages/en-US.json | 2 +- messages/es-ES.json | 1 - messages/fr-FR.json | 1 - messages/it-IT.json | 1 - messages/ko-KR.json | 1 - messages/nb-NO.json | 1 - messages/nl-NL.json | 1 - messages/pl-PL.json | 1 - messages/pt-PT.json | 1 - messages/ru-RU.json | 1 - messages/tr-TR.json | 1 - messages/zh-CN.json | 1 - server/db/pg/schema/schema.ts | 4 ++-- server/db/sqlite/schema/schema.ts | 4 ++-- server/routers/client/getClient.ts | 11 ++++------- server/routers/olm/fingerprintingUtils.ts | 10 +++++----- server/routers/olm/handleOlmRegisterMessage.ts | 6 ++++++ server/setup/scriptsPg/1.15.0.ts | 4 ++-- server/setup/scriptsSqlite/1.15.0.ts | 4 ++-- .../settings/clients/user/[niceId]/general/page.tsx | 8 ++++---- 23 files changed, 28 insertions(+), 39 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 0f29538c..dacf36b5 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Активирана защитна стена.", "autoUpdatesEnabled": "Активирани автоматични актуализации.", "tpmAvailable": "TPM е на разположение.", - "windowsDefenderEnabled": "Windows Defender е активиран.", "macosSipEnabled": "Protection на системната цялост (SIP).", "macosGatekeeperEnabled": "Gatekeeper.", "macosFirewallStealthMode": "Скрит режим на защитната стена.", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index cff24737..567ab4d4 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Firewall povolen", "autoUpdatesEnabled": "Automatické aktualizace povoleny", "tpmAvailable": "TPM k dispozici", - "windowsDefenderEnabled": "Okna byla povolena", "macosSipEnabled": "Ochrana systémové integrity (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Režim neviditelnosti firewallu", diff --git a/messages/de-DE.json b/messages/de-DE.json index 8b5ece78..e0c85879 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Firewall aktiviert", "autoUpdatesEnabled": "Automatische Updates aktiviert", "tpmAvailable": "TPM verfügbar", - "windowsDefenderEnabled": "Windows Defender aktiviert", "macosSipEnabled": "Schutz der Systemintegrität (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Firewall Stealth-Modus", diff --git a/messages/en-US.json b/messages/en-US.json index 6d2386e6..f2affe11 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2510,7 +2510,7 @@ "firewallEnabled": "Firewall Enabled", "autoUpdatesEnabled": "Auto Updates Enabled", "tpmAvailable": "TPM Available", - "windowsDefenderEnabled": "Windows Defender Enabled", + "windowsAntivirusEnabled": "Antivirus Enabled", "macosSipEnabled": "System Integrity Protection (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Firewall Stealth Mode", diff --git a/messages/es-ES.json b/messages/es-ES.json index 4823e92e..b27d82ea 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Cortafuegos activado", "autoUpdatesEnabled": "Actualizaciones automáticas habilitadas", "tpmAvailable": "TPM disponible", - "windowsDefenderEnabled": "Windows Defender activado", "macosSipEnabled": "Protección de integridad del sistema (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Modo Sigilo Firewall", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 779e7338..ed1fbc57 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Pare-feu activé", "autoUpdatesEnabled": "Mises à jour automatiques activées", "tpmAvailable": "TPM disponible", - "windowsDefenderEnabled": "Windows Defender activé", "macosSipEnabled": "Protection contre l'intégrité du système (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Mode furtif du pare-feu", diff --git a/messages/it-IT.json b/messages/it-IT.json index f1029d76..cf26edd6 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Firewall Abilitato", "autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati", "tpmAvailable": "TPM Disponibile", - "windowsDefenderEnabled": "Windows Defender Abilitato", "macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Modo Furtivo Del Firewall", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 751e4325..7b97c7b0 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "방화벽 활성화", "autoUpdatesEnabled": "자동 업데이트 활성화", "tpmAvailable": "TPM 사용 가능", - "windowsDefenderEnabled": "Windows Defender 활성화", "macosSipEnabled": "시스템 무결성 보호 (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "방화벽 스텔스 모드", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index ef6d7516..33f8228b 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Brannmur aktivert", "autoUpdatesEnabled": "Automatiske oppdateringer aktivert", "tpmAvailable": "TPM tilgjengelig", - "windowsDefenderEnabled": "Windows svarer aktivert", "macosSipEnabled": "System Integritetsbeskyttelse (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Brannmur Usynlig Modus", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 7ddbd03e..fe8a327e 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Firewall ingeschakeld", "autoUpdatesEnabled": "Auto Updates Ingeschakeld", "tpmAvailable": "TPM beschikbaar", - "windowsDefenderEnabled": "Windows Verdediger ingeschakeld", "macosSipEnabled": "Systeemintegriteitsbescherming (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Firewall Verberg Modus", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index a43fe425..97fc10be 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Zapora włączona", "autoUpdatesEnabled": "Automatyczne aktualizacje włączone", "tpmAvailable": "TPM dostępne", - "windowsDefenderEnabled": "Obrońca Windows włączony", "macosSipEnabled": "Ochrona integralności systemu (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Tryb Stealth zapory", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 71f55640..ded18509 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Firewall habilitado", "autoUpdatesEnabled": "Atualizações Automáticas Habilitadas", "tpmAvailable": "TPM disponível", - "windowsDefenderEnabled": "Defensor do Windows habilitado", "macosSipEnabled": "Proteção da Integridade do Sistema (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Modo Furtivo do Firewall", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 451ba592..a8f18219 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Брандмауэр включен", "autoUpdatesEnabled": "Автоматические обновления включены", "tpmAvailable": "Доступно TPM", - "windowsDefenderEnabled": "Защитник Windows включен", "macosSipEnabled": "Защита целостности системы (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Стилс-режим брандмауэра", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 87b9a323..bd79b465 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "Güvenlik Duvarı Etkin", "autoUpdatesEnabled": "Otomatik Güncellemeler Etkin", "tpmAvailable": "TPM Mevcut", - "windowsDefenderEnabled": "Windows Defender Etkin", "macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index f1a3038e..87a57c63 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -2510,7 +2510,6 @@ "firewallEnabled": "防火墙已启用", "autoUpdatesEnabled": "启用自动更新", "tpmAvailable": "TPM 可用", - "windowsDefenderEnabled": "Windows Defender 已启用", "macosSipEnabled": "系统完整性保护 (SIP)", "macosGatekeeperEnabled": "Gatekeeper", "macosFirewallStealthMode": "防火墙隐形模式", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 001e54cb..3c957470 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", { // Windows-specific posture check information - windowsDefenderEnabled: boolean("windowsDefenderEnabled") + windowsAntivirusEnabled: boolean("windowsAntivirusEnabled") .notNull() .default(false), @@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", { // Windows-specific posture check information - windowsDefenderEnabled: boolean("windowsDefenderEnabled") + windowsAntivirusEnabled: boolean("windowsAntivirusEnabled") .notNull() .default(false), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index e4e6c6d7..4137db3c 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", { // Windows-specific posture check information - windowsDefenderEnabled: integer("windowsDefenderEnabled", { + windowsAntivirusEnabled: integer("windowsAntivirusEnabled", { mode: "boolean" }) .notNull() @@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", { // Windows-specific posture check information - windowsDefenderEnabled: integer("windowsDefenderEnabled", { + windowsAntivirusEnabled: integer("windowsAntivirusEnabled", { mode: "boolean" }) .notNull() diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 18e99819..6bbf91b0 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -58,7 +58,7 @@ type PostureData = { firewallEnabled?: boolean | null; autoUpdatesEnabled?: boolean | null; tpmAvailable?: boolean | null; - windowsDefenderEnabled?: boolean | null; + windowsAntivirusEnabled?: boolean | null; macosSipEnabled?: boolean | null; macosGatekeeperEnabled?: boolean | null; macosFirewallStealthMode?: boolean | null; @@ -75,7 +75,7 @@ function getPlatformPostureData( const normalizedPlatform = platform?.toLowerCase() || "unknown"; const posture: PostureData = {}; - // Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender + // Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status if (normalizedPlatform === "windows") { if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { posture.diskEncrypted = fingerprint.diskEncrypted; @@ -83,14 +83,11 @@ function getPlatformPostureData( if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { posture.firewallEnabled = fingerprint.firewallEnabled; } - if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) { - posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled; - } if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { posture.tpmAvailable = fingerprint.tpmAvailable; } - if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) { - posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled; + if (fingerprint.windowsAntivirusEnabled !== null && fingerprint.windowsAntivirusEnabled !== undefined) { + posture.windowsAntivirusEnabled = fingerprint.windowsAntivirusEnabled; } } // macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode diff --git a/server/routers/olm/fingerprintingUtils.ts b/server/routers/olm/fingerprintingUtils.ts index 3fe445f1..90fafd3c 100644 --- a/server/routers/olm/fingerprintingUtils.ts +++ b/server/routers/olm/fingerprintingUtils.ts @@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string { autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false, tpmAvailable: postures.tpmAvailable ?? false, - windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false, + windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false, macosSipEnabled: postures.macosSipEnabled ?? false, macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false, @@ -87,7 +87,7 @@ export async function handleFingerprintInsertion( autoUpdatesEnabled: postures.autoUpdatesEnabled, tpmAvailable: postures.tpmAvailable, - windowsDefenderEnabled: postures.windowsDefenderEnabled, + windowsAntivirusEnabled: postures.windowsAntivirusEnabled, macosSipEnabled: postures.macosSipEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled, @@ -117,7 +117,7 @@ export async function handleFingerprintInsertion( autoUpdatesEnabled: postures.autoUpdatesEnabled, tpmAvailable: postures.tpmAvailable, - windowsDefenderEnabled: postures.windowsDefenderEnabled, + windowsAntivirusEnabled: postures.windowsAntivirusEnabled, macosSipEnabled: postures.macosSipEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled, @@ -162,7 +162,7 @@ export async function handleFingerprintInsertion( autoUpdatesEnabled: postures.autoUpdatesEnabled, tpmAvailable: postures.tpmAvailable, - windowsDefenderEnabled: postures.windowsDefenderEnabled, + windowsAntivirusEnabled: postures.windowsAntivirusEnabled, macosSipEnabled: postures.macosSipEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled, @@ -197,7 +197,7 @@ export async function handleFingerprintInsertion( autoUpdatesEnabled: postures.autoUpdatesEnabled, tpmAvailable: postures.tpmAvailable, - windowsDefenderEnabled: postures.windowsDefenderEnabled, + windowsAntivirusEnabled: postures.windowsAntivirusEnabled, macosSipEnabled: postures.macosSipEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled, diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 958c4568..b8d4dc01 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } + logger.debug("Handling fingerprint insertion for olm register...", { + olmId: olm.olmId, + fingerprint, + postures + }); + await handleFingerprintInsertion(olm, fingerprint, postures); if ( diff --git a/server/setup/scriptsPg/1.15.0.ts b/server/setup/scriptsPg/1.15.0.ts index 1ccf001b..0b96345b 100644 --- a/server/setup/scriptsPg/1.15.0.ts +++ b/server/setup/scriptsPg/1.15.0.ts @@ -49,7 +49,7 @@ export default async function migration() { "firewallEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "tpmAvailable" boolean DEFAULT false NOT NULL, - "windowsDefenderEnabled" boolean DEFAULT false NOT NULL, + "windowsAntivirusEnabled" boolean DEFAULT false NOT NULL, "macosSipEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosFirewallStealthMode" boolean DEFAULT false NOT NULL, @@ -75,7 +75,7 @@ export default async function migration() { "firewallEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "tpmAvailable" boolean DEFAULT false NOT NULL, - "windowsDefenderEnabled" boolean DEFAULT false NOT NULL, + "windowsAntivirusEnabled" boolean DEFAULT false NOT NULL, "macosSipEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosFirewallStealthMode" boolean DEFAULT false NOT NULL, diff --git a/server/setup/scriptsSqlite/1.15.0.ts b/server/setup/scriptsSqlite/1.15.0.ts index c8a3a221..dc0638d4 100644 --- a/server/setup/scriptsSqlite/1.15.0.ts +++ b/server/setup/scriptsSqlite/1.15.0.ts @@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' ( 'firewallEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'tpmAvailable' integer DEFAULT false NOT NULL, - 'windowsDefenderEnabled' integer DEFAULT false NOT NULL, + 'windowsAntivirusEnabled' integer DEFAULT false NOT NULL, 'macosSipEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosFirewallStealthMode' integer DEFAULT false NOT NULL, @@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' ( 'firewallEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'tpmAvailable' integer DEFAULT false NOT NULL, - 'windowsDefenderEnabled' integer DEFAULT false NOT NULL, + 'windowsAntivirusEnabled' integer DEFAULT false NOT NULL, 'macosSipEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosFirewallStealthMode' integer DEFAULT false NOT NULL, diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx index ed9a5f49..0cd86bd8 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx @@ -656,17 +656,17 @@ export default function GeneralPage() { )} - {client.posture.windowsDefenderEnabled !== null && - client.posture.windowsDefenderEnabled !== undefined && ( + {client.posture.windowsAntivirusEnabled !== null && + client.posture.windowsAntivirusEnabled !== undefined && ( - {t("windowsDefenderEnabled")} + {t("windowsAntivirusEnabled")} {isPaidUser ? formatPostureValue( client.posture - .windowsDefenderEnabled + .windowsAntivirusEnabled ) : "-"} From fd9fdf6399325975feda077c7a63342a99b18077 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 21 Jan 2026 18:13:12 -0800 Subject: [PATCH 03/14] remove biometric support from ios --- server/routers/client/getClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 6bbf91b0..e5b1c03e 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -134,9 +134,7 @@ function getPlatformPostureData( } // iOS: Biometric configuration else if (normalizedPlatform === "ios") { - if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { - posture.biometricsEnabled = fingerprint.biometricsEnabled; - } + // none supported yet } // Android: Screen lock, Biometric configuration, Hard drive encryption else if (normalizedPlatform === "android") { From 9ef93df54fd42df8f3ee4c5ff59c3fcac5691e91 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 21 Jan 2026 18:16:16 -0800 Subject: [PATCH 04/14] add mobile links to download banner --- src/components/ClientDownloadBanner.tsx | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/components/ClientDownloadBanner.tsx b/src/components/ClientDownloadBanner.tsx index 5f1dfbe6..e4076c31 100644 --- a/src/components/ClientDownloadBanner.tsx +++ b/src/components/ClientDownloadBanner.tsx @@ -4,6 +4,7 @@ import React from "react"; import { Button } from "@app/components/ui/button"; import { Download } from "lucide-react"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; +import { SiAndroid } from "react-icons/si"; import { useTranslations } from "next-intl"; import Link from "next/link"; import DismissableBanner from "./DismissableBanner"; @@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => { Linux + + + + + + ); }; From 00fc1da33c682b17cf99be6ee8874316c9081bab Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 22 Jan 2026 10:36:52 -0800 Subject: [PATCH 05/14] dont include posture in repsonse if not licensed or subscribed --- server/routers/client/getClient.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index e5b1c03e..1171430f 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { getUserDeviceName } from "@server/db/names"; import { build } from "@server/build"; +import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; const getClientSchema = z.strictObject({ clientId: z @@ -250,12 +251,18 @@ export async function getClient( : null; // Build posture data if available (platform-specific) + // Only return posture data if org is licensed/subscribed let postureData: PostureData | null = null; if (build !== "oss") { - postureData = getPlatformPostureData( - client.currentFingerprint?.platform || null, - client.currentFingerprint + const isOrgLicensed = await isLicensedOrSubscribed( + client.clients.orgId ); + if (isOrgLicensed) { + postureData = getPlatformPostureData( + client.currentFingerprint?.platform || null, + client.currentFingerprint + ); + } } const data: GetClientResponse = { From 316b7e56533837a7cae1ef2bfafe8cbfc58ccaef Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 22 Jan 2026 10:37:55 -0800 Subject: [PATCH 06/14] Hiring --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 27105c70..1432f2ab 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ +

+ + We're Hiring! + +

+

Start testing Pangolin at app.pangolin.net From 068b2a0dcdc7ff2a08be8dff99b916faf8c34975 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 22 Jan 2026 11:16:16 -0800 Subject: [PATCH 07/14] clean up paid features check --- .vscode/settings.json | 6 +- server/lib/blueprints/proxyResources.ts | 16 +-- server/lib/calculateUserClientsForOrgs.ts | 2 +- server/lib/isLicencedOrSubscribed.ts | 18 +-- server/private/lib/isLicencedOrSubscribed.ts | 30 +++++ server/routers/client/getClient.ts | 133 +++++++++++++------ server/routers/org/updateOrg.ts | 4 +- server/routers/resource/updateResource.ts | 8 +- server/routers/role/createRole.ts | 4 +- server/routers/role/updateRole.ts | 5 +- 10 files changed, 140 insertions(+), 86 deletions(-) create mode 100644 server/private/lib/isLicencedOrSubscribed.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 77440d96..767e57b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,13 +4,13 @@ }, "editor.defaultFormatter": "esbenp.prettier-vscode", "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.json-language-features" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" @@ -19,4 +19,4 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.formatOnSave": true -} +} \ No newline at end of file diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 0ae4c529..c0faad63 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers"; import { resourcePassword } from "@server/db"; import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; -import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { build } from "@server/build"; export type ProxyResourcesResults = { @@ -213,11 +213,7 @@ export async function updateProxyResources( // Update existing resource const isLicensed = await isLicensedOrSubscribed(orgId); - if (build == "enterprise" && !isLicensed) { - logger.warn( - "Server is not licensed! Clearing set maintenance screen values" - ); - // null the maintenance mode fields if not licensed + if (!isLicensed) { resourceData.maintenance = undefined; } @@ -594,7 +590,7 @@ export async function updateProxyResources( existingRule.action !== getRuleAction(rule.action) || existingRule.match !== rule.match.toUpperCase() || existingRule.value !== - getRuleValue(rule.match.toUpperCase(), rule.value) || + getRuleValue(rule.match.toUpperCase(), rule.value) || existingRule.priority !== intendedPriority ) { validateRule(rule); @@ -653,11 +649,7 @@ export async function updateProxyResources( } const isLicensed = await isLicensedOrSubscribed(orgId); - if (build == "enterprise" && !isLicensed) { - logger.warn( - "Server is not licensed! Clearing set maintenance screen values" - ); - // null the maintenance mode fields if not licensed + if (!isLicensed) { resourceData.maintenance = undefined; } diff --git a/server/lib/calculateUserClientsForOrgs.ts b/server/lib/calculateUserClientsForOrgs.ts index b2ea08a3..15837890 100644 --- a/server/lib/calculateUserClientsForOrgs.ts +++ b/server/lib/calculateUserClientsForOrgs.ts @@ -14,7 +14,7 @@ import { } from "@server/db"; import { getUniqueClientName } from "@server/db/names"; import { getNextAvailableClientSubnet } from "@server/lib/ip"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import logger from "@server/logger"; import { sendTerminateClient } from "@server/routers/client/terminate"; import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; diff --git a/server/lib/isLicencedOrSubscribed.ts b/server/lib/isLicencedOrSubscribed.ts index 3de3a915..748bb1b1 100644 --- a/server/lib/isLicencedOrSubscribed.ts +++ b/server/lib/isLicencedOrSubscribed.ts @@ -1,17 +1,3 @@ -import { build } from "@server/build"; -import license from "#dynamic/license/license"; -import { getOrgTierData } from "#dynamic/lib/billing"; -import { TierId } from "@server/lib/billing/tiers"; - export async function isLicensedOrSubscribed(orgId: string): Promise { - if (build === "enterprise") { - return await license.isUnlocked(); - } - - if (build === "saas") { - const { tier } = await getOrgTierData(orgId); - return tier === TierId.STANDARD; - } - - return true; -} + return false; +} \ No newline at end of file diff --git a/server/private/lib/isLicencedOrSubscribed.ts b/server/private/lib/isLicencedOrSubscribed.ts new file mode 100644 index 00000000..494deb7a --- /dev/null +++ b/server/private/lib/isLicencedOrSubscribed.ts @@ -0,0 +1,30 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { build } from "@server/build"; +import license from "#private/license/license"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; + +export async function isLicensedOrSubscribed(orgId: string): Promise { + if (build === "enterprise") { + return await license.isUnlocked(); + } + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + return tier === TierId.STANDARD; + } + + return false; +} \ No newline at end of file diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 1171430f..138a286c 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -12,7 +12,7 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { getUserDeviceName } from "@server/db/names"; import { build } from "@server/build"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; const getClientSchema = z.strictObject({ clientId: z @@ -78,58 +78,108 @@ function getPlatformPostureData( // Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status if (normalizedPlatform === "windows") { - if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + if ( + fingerprint.diskEncrypted !== null && + fingerprint.diskEncrypted !== undefined + ) { posture.diskEncrypted = fingerprint.diskEncrypted; } - if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + if ( + fingerprint.firewallEnabled !== null && + fingerprint.firewallEnabled !== undefined + ) { posture.firewallEnabled = fingerprint.firewallEnabled; } - if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { + if ( + fingerprint.tpmAvailable !== null && + fingerprint.tpmAvailable !== undefined + ) { posture.tpmAvailable = fingerprint.tpmAvailable; } - if (fingerprint.windowsAntivirusEnabled !== null && fingerprint.windowsAntivirusEnabled !== undefined) { - posture.windowsAntivirusEnabled = fingerprint.windowsAntivirusEnabled; + if ( + fingerprint.windowsAntivirusEnabled !== null && + fingerprint.windowsAntivirusEnabled !== undefined + ) { + posture.windowsAntivirusEnabled = + fingerprint.windowsAntivirusEnabled; } } // macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode else if (normalizedPlatform === "macos") { - if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + if ( + fingerprint.diskEncrypted !== null && + fingerprint.diskEncrypted !== undefined + ) { posture.diskEncrypted = fingerprint.diskEncrypted; } - if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { + if ( + fingerprint.biometricsEnabled !== null && + fingerprint.biometricsEnabled !== undefined + ) { posture.biometricsEnabled = fingerprint.biometricsEnabled; } - if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + if ( + fingerprint.firewallEnabled !== null && + fingerprint.firewallEnabled !== undefined + ) { posture.firewallEnabled = fingerprint.firewallEnabled; } - if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) { + if ( + fingerprint.macosSipEnabled !== null && + fingerprint.macosSipEnabled !== undefined + ) { posture.macosSipEnabled = fingerprint.macosSipEnabled; } - if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) { + if ( + fingerprint.macosGatekeeperEnabled !== null && + fingerprint.macosGatekeeperEnabled !== undefined + ) { posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled; } - if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) { - posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode; + if ( + fingerprint.macosFirewallStealthMode !== null && + fingerprint.macosFirewallStealthMode !== undefined + ) { + posture.macosFirewallStealthMode = + fingerprint.macosFirewallStealthMode; } - if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) { + if ( + fingerprint.autoUpdatesEnabled !== null && + fingerprint.autoUpdatesEnabled !== undefined + ) { posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled; } } // Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability else if (normalizedPlatform === "linux") { - if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + if ( + fingerprint.diskEncrypted !== null && + fingerprint.diskEncrypted !== undefined + ) { posture.diskEncrypted = fingerprint.diskEncrypted; } - if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + if ( + fingerprint.firewallEnabled !== null && + fingerprint.firewallEnabled !== undefined + ) { posture.firewallEnabled = fingerprint.firewallEnabled; } - if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) { + if ( + fingerprint.linuxAppArmorEnabled !== null && + fingerprint.linuxAppArmorEnabled !== undefined + ) { posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled; } - if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) { + if ( + fingerprint.linuxSELinuxEnabled !== null && + fingerprint.linuxSELinuxEnabled !== undefined + ) { posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled; } - if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { + if ( + fingerprint.tpmAvailable !== null && + fingerprint.tpmAvailable !== undefined + ) { posture.tpmAvailable = fingerprint.tpmAvailable; } } @@ -139,7 +189,10 @@ function getPlatformPostureData( } // Android: Screen lock, Biometric configuration, Hard drive encryption else if (normalizedPlatform === "android") { - if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + if ( + fingerprint.diskEncrypted !== null && + fingerprint.diskEncrypted !== undefined + ) { posture.diskEncrypted = fingerprint.diskEncrypted; } } @@ -236,33 +289,31 @@ export async function getClient( // Build fingerprint data if available const fingerprintData = client.currentFingerprint ? { - username: client.currentFingerprint.username || null, - hostname: client.currentFingerprint.hostname || null, - platform: client.currentFingerprint.platform || null, - osVersion: client.currentFingerprint.osVersion || null, - kernelVersion: - client.currentFingerprint.kernelVersion || null, - arch: client.currentFingerprint.arch || null, - deviceModel: client.currentFingerprint.deviceModel || null, - serialNumber: client.currentFingerprint.serialNumber || null, - firstSeen: client.currentFingerprint.firstSeen || null, - lastSeen: client.currentFingerprint.lastSeen || null - } + username: client.currentFingerprint.username || null, + hostname: client.currentFingerprint.hostname || null, + platform: client.currentFingerprint.platform || null, + osVersion: client.currentFingerprint.osVersion || null, + kernelVersion: + client.currentFingerprint.kernelVersion || null, + arch: client.currentFingerprint.arch || null, + deviceModel: client.currentFingerprint.deviceModel || null, + serialNumber: client.currentFingerprint.serialNumber || null, + firstSeen: client.currentFingerprint.firstSeen || null, + lastSeen: client.currentFingerprint.lastSeen || null + } : null; // Build posture data if available (platform-specific) // Only return posture data if org is licensed/subscribed let postureData: PostureData | null = null; - if (build !== "oss") { - const isOrgLicensed = await isLicensedOrSubscribed( - client.clients.orgId + const isOrgLicensed = await isLicensedOrSubscribed( + client.clients.orgId + ); + if (isOrgLicensed) { + postureData = getPlatformPostureData( + client.currentFingerprint?.platform || null, + client.currentFingerprint ); - if (isOrgLicensed) { - postureData = getPlatformPostureData( - client.currentFingerprint?.platform || null, - client.currentFingerprint - ); - } } const data: GetClientResponse = { diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 844ca200..44ff9190 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -13,7 +13,7 @@ import { build } from "@server/build"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { cache } from "@server/lib/cache"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; const updateOrgParamsSchema = z.strictObject({ orgId: z.string() @@ -89,7 +89,7 @@ export async function updateOrg( const { orgId } = parsedParams.data; const isLicensed = await isLicensedOrSubscribed(orgId); - if (build == "enterprise" && !isLicensed) { + if (!isLicensed) { parsedBody.data.requireTwoFactor = undefined; parsedBody.data.maxSessionLengthHours = undefined; parsedBody.data.passwordExpiryDays = undefined; diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 80b7a00a..62a466d7 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -23,7 +23,7 @@ import { OpenAPITags } from "@server/openApi"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { build } from "@server/build"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; const updateResourceParamsSchema = z.strictObject({ resourceId: z.string().transform(Number).pipe(z.int().positive()) @@ -342,11 +342,7 @@ async function updateHttpResource( } const isLicensed = await isLicensedOrSubscribed(resource.orgId); - if (build == "enterprise" && !isLicensed) { - logger.warn( - "Server is not licensed! Clearing set maintenance screen values" - ); - // null the maintenance mode fields if not licensed + if (!isLicensed) { updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeType = undefined; updateData.maintenanceTitle = undefined; diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index a1e21d7a..666eb756 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -11,7 +11,7 @@ import { ActionsEnum } from "@server/auth/actions"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; const createRoleParamsSchema = z.strictObject({ orgId: z.string() @@ -101,7 +101,7 @@ export async function createRole( } const isLicensed = await isLicensedOrSubscribed(orgId); - if (build === "oss" || !isLicensed) { + if (!isLicensed) { roleData.requireDeviceApproval = undefined; } diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts index 03034ea1..6724d622 100644 --- a/server/routers/role/updateRole.ts +++ b/server/routers/role/updateRole.ts @@ -8,8 +8,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { build } from "@server/build"; -import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { OpenAPITags, registry } from "@server/openApi"; const updateRoleParamsSchema = z.strictObject({ @@ -112,7 +111,7 @@ export async function updateRole( } const isLicensed = await isLicensedOrSubscribed(orgId); - if (build === "oss" || !isLicensed) { + if (!isLicensed) { updateData.requireDeviceApproval = undefined; } From a76eec7bb70f9b1007c19229f84bd99c9a0d4801 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 22 Jan 2026 11:27:24 -0800 Subject: [PATCH 08/14] add ios and android to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1432f2ab..c566c867 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Download the Pangolin client for your platform: - [Mac](https://pangolin.net/downloads/mac) - [Windows](https://pangolin.net/downloads/windows) - [Linux](https://pangolin.net/downloads/linux) +- [iOS](https://pangolin.net/downloads/ios) +- [Android](https://pangolin.net/downloads/android) ## Get Started From 2959ad0e70e477974243fb66a0e0050a3f383fc1 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 22 Jan 2026 15:03:04 -0800 Subject: [PATCH 09/14] Fix the source of the cli blueprint --- blueprint.py | 72 ------------------- blueprint.yaml | 70 ------------------ .../routers/blueprints/applyYAMLBlueprint.ts | 7 +- server/routers/blueprints/types.ts | 2 +- 4 files changed, 5 insertions(+), 146 deletions(-) delete mode 100644 blueprint.py delete mode 100644 blueprint.yaml diff --git a/blueprint.py b/blueprint.py deleted file mode 100644 index 9fd76412..00000000 --- a/blueprint.py +++ /dev/null @@ -1,72 +0,0 @@ -import requests -import yaml -import json -import base64 - -# The file path for the YAML file to be read -# You can change this to the path of your YAML file -YAML_FILE_PATH = 'blueprint.yaml' - -# The API endpoint and headers from the curl request -API_URL = 'http://api.pangolin.net/v1/org/test/blueprint' -HEADERS = { - 'accept': '*/*', - 'Authorization': 'Bearer ', - '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) - diff --git a/blueprint.yaml b/blueprint.yaml deleted file mode 100644 index adc25055..00000000 --- a/blueprint.yaml +++ /dev/null @@ -1,70 +0,0 @@ -client-resources: - client-resource-nice-id-uno: - name: this is my resource - protocol: tcp - proxy-port: 3001 - hostname: localhost - internal-port: 3000 - site: lively-yosemite-toad - client-resource-nice-id-duce: - name: this is my resource - protocol: udp - proxy-port: 3000 - hostname: localhost - internal-port: 3000 - site: lively-yosemite-toad - -proxy-resources: - resource-nice-id-uno: - name: this is my resource - protocol: http - full-domain: duce.test.example.com - host-header: example.com - tls-server-name: example.com - # auth: - # pincode: 123456 - # password: sadfasdfadsf - # sso-enabled: true - # sso-roles: - # - Member - # sso-users: - # - owen@pangolin.net - # whitelist-users: - # - owen@pangolin.net - # auto-login-idp: 1 - headers: - - name: X-Example-Header - value: example-value - - name: X-Another-Header - value: another-value - rules: - - action: allow - match: ip - value: 1.1.1.1 - - action: deny - match: cidr - value: 2.2.2.2/32 - - action: pass - match: path - value: /admin - targets: - - site: lively-yosemite-toad - path: /path - pathMatchType: prefix - hostname: localhost - method: http - port: 8000 - - site: slim-alpine-chipmunk - hostname: localhost - path: /yoman - pathMatchType: exact - method: http - port: 8001 - resource-nice-id-duce: - name: this is other resource - protocol: tcp - proxy-port: 3000 - targets: - - site: lively-yosemite-toad - hostname: localhost - port: 3000 \ No newline at end of file diff --git a/server/routers/blueprints/applyYAMLBlueprint.ts b/server/routers/blueprints/applyYAMLBlueprint.ts index 21402cd0..19751e46 100644 --- a/server/routers/blueprints/applyYAMLBlueprint.ts +++ b/server/routers/blueprints/applyYAMLBlueprint.ts @@ -26,7 +26,8 @@ const applyBlueprintSchema = z message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}` }); } - }) + }), + source: z.enum(["API", "UI", "CLI"]).optional() }) .strict(); @@ -84,7 +85,7 @@ export async function applyYAMLBlueprint( ); } - const { blueprint: contents, name } = parsedBody.data; + const { blueprint: contents, name, source = "UI" } = parsedBody.data; logger.debug(`Received blueprint:`, contents); @@ -107,7 +108,7 @@ export async function applyYAMLBlueprint( blueprint = await applyBlueprint({ orgId, name, - source: "UI", + source, configData: parsedConfig }); } catch (err) { diff --git a/server/routers/blueprints/types.ts b/server/routers/blueprints/types.ts index 52d61300..9a188b2c 100644 --- a/server/routers/blueprints/types.ts +++ b/server/routers/blueprints/types.ts @@ -1,6 +1,6 @@ import type { Blueprint } from "@server/db"; -export type BlueprintSource = "API" | "UI" | "NEWT"; +export type BlueprintSource = "API" | "UI" | "NEWT" | "CLI"; export type BlueprintData = Omit & { source: BlueprintSource; From 5f19918ca02be413215ecc1ca8008c0a52796cb3 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 22 Jan 2026 15:16:41 -0800 Subject: [PATCH 10/14] Show the source in the UI --- src/components/BlueprintDetailsForm.tsx | 9 +++++++++ src/components/BlueprintsTable.tsx | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/components/BlueprintDetailsForm.tsx b/src/components/BlueprintDetailsForm.tsx index 92b6a304..2bbfaa60 100644 --- a/src/components/BlueprintDetailsForm.tsx +++ b/src/components/BlueprintDetailsForm.tsx @@ -110,6 +110,15 @@ export default function BlueprintDetailsForm({ Dashboard )}{" "} + {blueprint.source === "CLI" && ( + + + CLI + + )}{" "} diff --git a/src/components/BlueprintsTable.tsx b/src/components/BlueprintsTable.tsx index 8031e506..63cd3dce 100644 --- a/src/components/BlueprintsTable.tsx +++ b/src/components/BlueprintsTable.tsx @@ -128,6 +128,19 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { ); } + case "CLI": { + return ( + + + + CLI + + + ); + } } } }, From e3e4bdfe09a90cfd829fec0e25382fb28c6d849c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 23 Jan 2026 04:40:19 +0100 Subject: [PATCH 11/14] =?UTF-8?q?=F0=9F=9A=B8=20fix=20target=20item=20tabb?= =?UTF-8?q?ing=20by=20memoizing=20the=20`getColumns`=20(and=20its=20depend?= =?UTF-8?q?encies)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/proxy/page.tsx | 316 ++------- .../settings/resources/proxy/create/page.tsx | 622 ++++-------------- src/components/DomainPicker.tsx | 12 - .../resource-target-address-item.tsx | 256 +++++++ src/components/ui/input.tsx | 2 +- src/components/ui/select.tsx | 8 +- 6 files changed, 456 insertions(+), 760 deletions(-) create mode 100644 src/components/resource-target-address-item.tsx diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index fd4eb33a..e7e64ae9 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -11,7 +11,6 @@ import { SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { ContainersSelector } from "@app/components/ContainersSelector"; import { HeadersInput } from "@app/components/HeadersInput"; import { PathMatchDisplay, @@ -19,6 +18,7 @@ import { PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; +import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item"; import { SettingsContainer, SettingsSection, @@ -30,15 +30,6 @@ import { } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { Badge } from "@app/components/ui/badge"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; import { Form, FormControl, @@ -48,11 +39,6 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; import { Table, TableBody, @@ -73,12 +59,9 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; -import { cn } from "@app/lib/cn"; import { DockerManager, DockerState } from "@app/lib/docker"; -import { parseHostTarget } from "@app/lib/parseHostTarget"; import { orgQueries, resourceQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CaretSortIcon } from "@radix-ui/react-icons"; import { tlsNameSchema } from "@server/lib/schemas"; import { type GetResourceResponse } from "@server/routers/resource"; import type { ListSitesResponse } from "@server/routers/site"; @@ -98,7 +81,6 @@ import { import { AxiosResponse } from "axios"; import { AlertTriangle, - CheckIcon, CircleCheck, CircleX, Info, @@ -107,7 +89,7 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { use, useActionState, useEffect, useState } from "react"; +import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -202,7 +184,7 @@ function ProxyResourceTargetsForm({ setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; - const refreshContainersForSite = async (siteId: number) => { + const refreshContainersForSite = useCallback(async (siteId: number) => { const dockerManager = new DockerManager(api, siteId); const containers = await dockerManager.fetchContainers(); @@ -214,9 +196,9 @@ function ProxyResourceTargetsForm({ } return newMap; }); - }; + }, [api]); - const getDockerStateForSite = (siteId: number): DockerState => { + const getDockerStateForSite = useCallback((siteId: number): DockerState => { return ( dockerStates.get(siteId) || { isEnabled: false, @@ -224,7 +206,7 @@ function ProxyResourceTargetsForm({ containers: [] } ); - }; + }, [dockerStates]); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { @@ -234,8 +216,40 @@ function ProxyResourceTargetsForm({ return false; }); - const getColumns = (): ColumnDef[] => { - const isHttp = resource.http; + const isHttp = resource.http; + + const removeTarget = useCallback((targetId: number) => { + setTargets((prevTargets) => { + const targetToRemove = prevTargets.find((target) => target.targetId === targetId); + if (targetToRemove && !targetToRemove.new) { + setTargetsToRemove((prev) => [...prev, targetId]); + } + return prevTargets.filter((target) => target.targetId !== targetId); + }); + }, []); + + const updateTarget = useCallback((targetId: number, data: Partial) => { + 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[] => { const priorityColumn: ColumnDef = { id: "priority", @@ -419,213 +433,15 @@ function ProxyResourceTargetsForm({ accessorKey: "address", header: () => {t("address")}, 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 ( -

-
- {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} - - - - - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ) - } - > - - {site.name} - - ))} - - - - - - - {resource.http && ( - - )} - - {resource.http && ( -
- {"://"} -
- )} - - { - 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 - }); - } - }} - /> -
- {":"} -
- { - 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 - }); - } - }} - /> -
-
+ ); }, size: 400, @@ -765,7 +581,7 @@ function ProxyResourceTargetsForm({ actionsColumn ]; } - }; + }, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]); function addNewTarget() { const isHttp = resource.http; @@ -806,32 +622,6 @@ function ProxyResourceTargetsForm({ setTargets((prev) => [...prev, newTarget]); } - const removeTarget = (targetId: number) => { - setTargets([ - ...targets.filter((target) => target.targetId !== targetId) - ]); - - if (!targets.find((target) => target.targetId === targetId)?.new) { - setTargetsToRemove([...targetsToRemove, targetId]); - } - }; - - async function updateTarget(targetId: number, data: Partial) { - const site = sites.find((site) => site.siteId === data.siteId); - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ) - ); - } - function updateTargetHealthCheck(targetId: number, config: any) { setTargets( targets.map((target) => @@ -846,14 +636,6 @@ function ProxyResourceTargetsForm({ ); } - const openHealthCheckDialog = (target: LocalTarget) => { - console.log(target); - setSelectedTargetForHealthCheck(target); - setHealthCheckDialogOpen(true); - }; - - const columns = getColumns(); - const table = useReactTable({ data: targets, columns, diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index 1d6212bf..47f24cd9 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -1,5 +1,14 @@ "use client"; +import CopyTextBox from "@app/components/CopyTextBox"; +import DomainPicker from "@app/components/DomainPicker"; +import HealthCheckDialog from "@app/components/HealthCheckDialog"; +import { + PathMatchDisplay, + PathMatchModal, + PathRewriteDisplay, + PathRewriteModal +} from "@app/components/PathMatchRenameModal"; import { SettingsContainer, SettingsSection, @@ -9,6 +18,10 @@ import { SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; +import { StrategySelect } from "@app/components/StrategySelect"; +import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item"; +import { Button } from "@app/components/ui/button"; import { Form, FormControl, @@ -18,22 +31,7 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import HeaderTitle from "@app/components/SettingsSectionTitle"; -import { z } from "zod"; -import { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; -import { Button } from "@app/components/ui/button"; -import { useParams, useRouter } from "next/navigation"; -import { ListSitesResponse } from "@server/routers/site"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; -import { StrategySelect } from "@app/components/StrategySelect"; import { Select, SelectContent, @@ -41,48 +39,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { ListDomainsResponse } from "@server/routers/domain"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; -import { cn } from "@app/lib/cn"; -import { - ArrowRight, - CircleCheck, - CircleX, - Info, - MoveRight, - Plus, - Settings, - SquareArrowOutUpRight -} from "lucide-react"; -import CopyTextBox from "@app/components/CopyTextBox"; -import Link from "next/link"; -import { useTranslations } from "next-intl"; -import DomainPicker from "@app/components/DomainPicker"; -import { build } from "@server/build"; -import { ContainersSelector } from "@app/components/ContainersSelector"; -import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender, - Row -} from "@tanstack/react-table"; +import { Switch } from "@app/components/ui/switch"; import { Table, TableBody, @@ -91,30 +48,49 @@ import { TableHeader, TableRow } from "@app/components/ui/table"; -import { Switch } from "@app/components/ui/switch"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { isTargetValid } from "@server/lib/validators"; -import { ListTargetsResponse } from "@server/routers/target"; -import { DockerManager, DockerState } from "@app/lib/docker"; -import { parseHostTarget } from "@app/lib/parseHostTarget"; -import { toASCII, toUnicode } from "punycode"; -import { DomainRow } from "@app/components/DomainsTable"; -import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { DockerManager, DockerState } from "@app/lib/docker"; +import { orgQueries } from "@app/lib/queries"; +import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Resource } from "@server/db"; +import { isTargetValid } from "@server/lib/validators"; +import { ListTargetsResponse } from "@server/routers/target"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { useQuery } from "@tanstack/react-query"; import { - PathMatchDisplay, - PathMatchModal, - PathRewriteDisplay, - PathRewriteModal -} from "@app/components/PathMatchRenameModal"; -import { Badge } from "@app/components/ui/badge"; -import HealthCheckDialog from "@app/components/HealthCheckDialog"; -import { SwitchInput } from "@app/components/SwitchInput"; + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { AxiosResponse } from "axios"; +import { + CircleCheck, + CircleX, + Info, + Plus, + Settings, + SquareArrowOutUpRight +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; +import { toASCII } from "punycode"; +import { useEffect, useMemo, useState, useCallback } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { z } from "zod"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -204,10 +180,6 @@ const addTargetSchema = z } ); -type BaseResourceFormValues = z.infer; -type HttpResourceFormValues = z.infer; -type TcpUdpResourceFormValues = z.infer; - type ResourceType = "http" | "raw"; interface ResourceTypeOption { @@ -217,7 +189,7 @@ interface ResourceTypeOption { disabled?: boolean; } -type LocalTarget = Omit< +export type LocalTarget = Omit< ArrayElement & { new?: boolean; updated?: boolean; @@ -233,18 +205,16 @@ export default function Page() { const router = useRouter(); const t = useTranslations(); - const [loadingPage, setLoadingPage] = useState(true); - const [sites, setSites] = useState([]); - const [baseDomains, setBaseDomains] = useState< - { domainId: string; baseDomain: string }[] - >([]); + const { data: sites = [], isLoading: loadingPage } = useQuery( + orgQueries.sites({ orgId: orgId as string }) + ); + const [createLoading, setCreateLoading] = useState(false); const [showSnippets, setShowSnippets] = useState(false); const [niceId, setNiceId] = useState(""); // Target management state const [targets, setTargets] = useState([]); - const [targetsToRemove, setTargetsToRemove] = useState([]); const [dockerStates, setDockerStates] = useState>( new Map() ); @@ -405,102 +375,60 @@ export default function Page() { setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; - const refreshContainersForSite = async (siteId: number) => { - const dockerManager = new DockerManager(api, siteId); - const containers = await dockerManager.fetchContainers(); + const refreshContainersForSite = useCallback( + async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); - setDockerStates((prev) => { - const newMap = new Map(prev); - const existingState = newMap.get(siteId); - if (existingState) { - newMap.set(siteId, { ...existingState, containers }); - } - return newMap; + setDockerStates((prev) => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }, + [api] + ); + + const getDockerStateForSite = useCallback( + (siteId: number): DockerState => { + return ( + dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + } + ); + }, + [dockerStates] + ); + + const removeTarget = useCallback((targetId: number) => { + setTargets((prevTargets) => { + return prevTargets.filter((target) => target.targetId !== targetId); }); - }; + }, []); - const getDockerStateForSite = (siteId: number): DockerState => { - return ( - dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - } - ); - }; - - async function addTarget(data: z.infer) { - const site = sites.find((site) => site.siteId === data.siteId); - - const isHttp = baseForm.watch("http"); - - const newTarget: LocalTarget = { - ...data, - path: isHttp ? data.path || null : null, - pathMatchType: isHttp ? data.pathMatchType || null : null, - rewritePath: isHttp ? data.rewritePath || null : null, - rewritePathType: isHttp ? data.rewritePathType || null : null, - siteType: site?.type || null, - enabled: true, - targetId: new Date().getTime(), - new: true, - resourceId: 0, // Will be set when resource is created - priority: isHttp ? data.priority || 100 : 100, // Default priority - hcEnabled: false, - hcPath: null, - hcMethod: null, - hcInterval: null, - hcTimeout: null, - hcHeaders: null, - hcScheme: null, - hcHostname: null, - hcPort: null, - hcFollowRedirects: null, - hcHealth: "unknown", - hcStatus: null, - hcMode: null, - hcUnhealthyInterval: null, - hcTlsServerName: null - }; - - setTargets([...targets, newTarget]); - addTargetForm.reset({ - ip: "", - method: baseForm.watch("http") ? "http" : null, - port: "" as any as number, - path: null, - pathMatchType: null, - rewritePath: null, - rewritePathType: null, - priority: isHttp ? 100 : undefined - }); - } - - const removeTarget = (targetId: number) => { - setTargets([ - ...targets.filter((target) => target.targetId !== targetId) - ]); - - if (!targets.find((target) => target.targetId === targetId)?.new) { - setTargetsToRemove([...targetsToRemove, targetId]); - } - }; - - async function updateTarget(targetId: number, data: Partial) { - const site = sites.find((site) => site.siteId === data.siteId); - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ) - ); - } + const updateTarget = useCallback( + (targetId: number, data: Partial) => { + setTargets((prevTargets) => { + const site = sites.find((site) => site.siteId === data.siteId); + return prevTargets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } + : target + ); + }); + }, + [sites] + ); async function onSubmit() { setCreateLoading(true); @@ -638,82 +566,18 @@ export default function Page() { } useEffect(() => { - const load = async () => { - setLoadingPage(true); + // Initialize Docker for newt sites + for (const site of sites) { + if (site.type === "newt") { + initializeDockerForSite(site.siteId); + } + } - const fetchSites = async () => { - const res = await api - .get< - AxiosResponse - >(`/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 - >(`/org/${orgId}/domains/`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("domainsErrorFetch"), - description: formatAxiosError( - e, - t("domainsErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain) - })); - setBaseDomains(domains); - // if (domains.length) { - // httpForm.setValue("domainId", domains[0].domainId); - // } - } - }; - - await fetchSites(); - await fetchDomains(); - - setLoadingPage(false); - }; - - load(); - }, []); + // If there's at least one site, set it as the default in the form + if (sites.length > 0) { + addTargetForm.setValue("siteId", sites[0].siteId); + } + }, [sites]); function TargetHealthCheck(targetId: number, config: any) { setTargets( @@ -729,16 +593,15 @@ export default function Page() { ); } - const openHealthCheckDialog = (target: LocalTarget) => { + const openHealthCheckDialog = useCallback((target: LocalTarget) => { console.log(target); setSelectedTargetForHealthCheck(target); setHealthCheckDialogOpen(true); - }; + }, []); - const getColumns = (): ColumnDef[] => { - const baseColumns: ColumnDef[] = []; - const isHttp = baseForm.watch("http"); + const isHttp = baseForm.watch("http"); + const columns = useMemo((): ColumnDef[] => { const priorityColumn: ColumnDef = { id: "priority", header: () => ( @@ -875,7 +738,7 @@ export default function Page() { trigger={ - - - - - - - {t("siteNotFound")} - - - {sites.map((site) => ( - - updateTarget( - row.original - .targetId, - { - siteId: site.siteId - } - ) - } - > - - {site.name} - - ))} - - - - - - - {isHttp && ( - - )} - - {isHttp && ( -
- {"://"} -
- )} - - { - 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 - }); - } - }} - /> -
- {":"} -
- { - 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 - }); - } - }} - /> - - - ); - }, + cell: ({ row }) => ( + + ), size: 400, minSize: 350, maxSize: 500 @@ -1186,7 +849,7 @@ export default function Page() { + + + + + + {t("siteNotFound")} + + {sites.map((site) => ( + + updateTarget( + proxyTarget.targetId, + { + siteId: site.siteId + } + ) + } + > + + {site.name} + + ))} + + + + + + + {isHttp && ( + + )} + + {isHttp && ( +
+ {"://"} +
+ )} + + { + 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 + }); + } + }} + /> +
+ {":"} +
+ { + 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 + }); + } + }} + /> + + + ); +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index aec536e2..0c48f1f2 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -44,8 +44,8 @@ const Input = React.forwardRef( data-slot="input" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0", className )} ref={ref} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 39e79da6..6d065e7a 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -36,7 +36,9 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", + "border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0", + // "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0", className )} {...props} @@ -60,7 +62,7 @@ function SelectContent({ {children} From c1b473294e5738ea827e47b68a09c0dc09eebe52 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 23 Jan 2026 04:54:24 +0100 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=94=A5=20remove=20useless=20`useEff?= =?UTF-8?q?ect`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-target-address-item.tsx | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/components/resource-target-address-item.tsx b/src/components/resource-target-address-item.tsx index 16b07974..3c4cb927 100644 --- a/src/components/resource-target-address-item.tsx +++ b/src/components/resource-target-address-item.tsx @@ -1,26 +1,25 @@ 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 { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; -import { SelectTrigger, SelectContent, SelectItem, Select } from "./ui/select"; +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, - CommandInput, - CommandList, CommandEmpty, CommandGroup, - CommandItem + CommandInput, + CommandItem, + CommandList } from "./ui/command"; -import { CheckIcon } from "lucide-react"; -import { ContainersSelector } from "./ContainersSelector"; -import type { ListSitesResponse } from "@server/routers/site"; -import type { DockerState } from "@app/lib/docker"; -import { useTranslations } from "next-intl"; -import type { ArrayElement } from "@server/types/ArrayElement"; -import { Button } from "./ui/button"; import { Input } from "./ui/input"; -import { useEffect } from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; type SiteWithUpdateAvailable = ListSitesResponse["sites"][number]; @@ -67,20 +66,6 @@ export function ResourceTargetAddressItem({ }); }; - useEffect(() => { - console.log("onMount"); - return () => console.log("onUnMount"); - }, []); - - useEffect(() => { - console.log("onChange [sites]", { sites }); - // return () => console.log("onUnMount"); - }, [sites]); - - useEffect(() => { - console.log("onChange [proxyTarget]", { proxyTarget }); - }, [proxyTarget]); - return (
From f378d6f04033ed810c60f8db606bf6303b230c32 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 22 Jan 2026 21:24:28 -0800 Subject: [PATCH 13/14] fix input border --- src/components/ui/input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 0c48f1f2..fe87fb0d 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -45,7 +45,7 @@ const Input = React.forwardRef( 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", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0", + "focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0", className )} ref={ref} From 643d56958d5ac51da281227cac2e105d6bbdf626 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 23 Jan 2026 10:07:05 -0800 Subject: [PATCH 14/14] fix saas private import --- server/private/routers/approvals/processPendingApproval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/private/routers/approvals/processPendingApproval.ts b/server/private/routers/approvals/processPendingApproval.ts index 43639dd7..d4988ac5 100644 --- a/server/private/routers/approvals/processPendingApproval.ts +++ b/server/private/routers/approvals/processPendingApproval.ts @@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error"; import { build } from "@server/build"; import { approvals, clients, db, orgs, type Approval } from "@server/db"; -import { getOrgTierData } from "@server/lib/billing"; +import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import response from "@server/lib/response"; import { and, eq, type InferInsertModel } from "drizzle-orm";