From 6a45151741526e23d656cf4c8a5d3844d2645756 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 18 Jan 2026 11:55:24 -0800 Subject: [PATCH] show fingerprint popup and fix policy check errors --- server/routers/client/listClients.ts | 9 +- .../routers/olm/handleOlmRegisterMessage.ts | 17 +- .../clients/user/[niceId]/general/page.tsx | 278 ++++++++++-------- .../[orgId]/settings/clients/user/page.tsx | 29 +- src/components/UserDevicesTable.tsx | 113 +++++-- src/components/ui/info-popup.tsx | 48 ++- 6 files changed, 344 insertions(+), 150 deletions(-) diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 99857261..31f75d68 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -143,7 +143,14 @@ function queryClients( olmArchived: olms.archived, archived: clients.archived, blocked: clients.blocked, - deviceModel: fingerprints.deviceModel + deviceModel: fingerprints.deviceModel, + fingerprintPlatform: fingerprints.platform, + fingerprintOsVersion: fingerprints.osVersion, + fingerprintKernelVersion: fingerprints.kernelVersion, + fingerprintArch: fingerprints.arch, + fingerprintSerialNumber: fingerprints.serialNumber, + fingerprintUsername: fingerprints.username, + fingerprintHostname: fingerprints.hostname }) .from(clients) .leftJoin(orgs, eq(clients.orgId, orgs.orgId)) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index c8a8d3f7..19c240ad 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -115,6 +115,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { sessionId // this is the user token passed in the message }); + logger.debug("Policy check result:", policyCheck); + if (policyCheck?.error) { logger.error( `Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}` @@ -123,7 +125,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - if (policyCheck?.policies?.passwordAge?.compliant) { + if ( + policyCheck?.policies?.passwordAge && + !policyCheck.policies.passwordAge.compliant + ) { logger.warn( `Olm user ${olm.userId} has non-compliant password age for org ${orgId}` ); @@ -132,7 +137,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { olm.olmId ); return; - } else if (policyCheck?.policies?.maxSessionLength?.compliant) { + } else if ( + policyCheck?.policies?.maxSessionLength && + !policyCheck.policies.maxSessionLength.compliant + ) { logger.warn( `Olm user ${olm.userId} has non-compliant session length for org ${orgId}` ); @@ -141,7 +149,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { olm.olmId ); return; - } else if (policyCheck?.policies?.requiredTwoFactor) { + } else if ( + policyCheck?.policies && + !policyCheck.policies.requiredTwoFactor + ) { logger.warn( `Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}` ); 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 179756e1..daa668e6 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx @@ -149,12 +149,16 @@ export default function GeneralPage() { const [approvalId, setApprovalId] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const [, startTransition] = useTransition(); - + const showApprovalFeatures = build !== "oss" && isPaidUser; // Fetch approval ID for this client if pending useEffect(() => { - if (showApprovalFeatures && client.approvalState === "pending" && client.clientId) { + if ( + showApprovalFeatures && + client.approvalState === "pending" && + client.clientId + ) { api.get(`/org/${orgId}/approvals?approvalState=pending`) .then((res) => { const approval = res.data.data.approvals.find( @@ -168,7 +172,13 @@ export default function GeneralPage() { // Silently fail - approval might not exist }); } - }, [showApprovalFeatures, client.approvalState, client.clientId, orgId, api]); + }, [ + showApprovalFeatures, + client.approvalState, + client.clientId, + orgId, + api + ]); const handleApprove = async () => { if (!approvalId) return; @@ -280,7 +290,6 @@ export default function GeneralPage() { } }; - return ( {/* Pending Approval Banner */} @@ -296,6 +305,7 @@ export default function GeneralPage() { onClick={handleApprove} disabled={isRefreshing || !approvalId} loading={isRefreshing} + variant="outline" className="gap-2" > @@ -305,7 +315,7 @@ export default function GeneralPage() { onClick={handleDeny} disabled={isRefreshing || !approvalId} loading={isRefreshing} - variant="destructive" + variant="outline" className="gap-2" > @@ -339,8 +349,7 @@ export default function GeneralPage() { )} {/* Device Information Section */} - {(client.fingerprint || - (client.agent && client.olmVersion)) && ( + {(client.fingerprint || (client.agent && client.olmVersion)) && ( @@ -360,145 +369,182 @@ export default function GeneralPage() { - {client.agent + " v" + client.olmVersion} + {client.agent + + " v" + + client.olmVersion} )} - {client.fingerprint && (() => { - const platform = client.fingerprint.platform; - const fieldConfig = getPlatformFieldConfig(platform); - - return ( - - {platform && ( - - - {t("platform")} - - -
- {getPlatformIcon(platform)} - {formatPlatform(platform)} -
-
-
- )} + {client.fingerprint && + (() => { + const platform = client.fingerprint.platform; + const fieldConfig = + getPlatformFieldConfig(platform); - {client.fingerprint.osVersion && - fieldConfig.osVersion.show && ( + return ( + + {platform && ( - {t(fieldConfig.osVersion.labelKey)} + {t("platform")} - {client.fingerprint.osVersion} +
+ {getPlatformIcon( + platform + )} + + {formatPlatform( + platform + )} + +
)} - {client.fingerprint.kernelVersion && - fieldConfig.kernelVersion.show && ( + {client.fingerprint.osVersion && + fieldConfig.osVersion.show && ( + + + {t( + fieldConfig + .osVersion + .labelKey + )} + + + { + client.fingerprint + .osVersion + } + + + )} + + {client.fingerprint.kernelVersion && + fieldConfig.kernelVersion.show && ( + + + {t("kernelVersion")} + + + { + client.fingerprint + .kernelVersion + } + + + )} + + {client.fingerprint.arch && + fieldConfig.arch.show && ( + + + {t("architecture")} + + + { + client.fingerprint + .arch + } + + + )} + + {client.fingerprint.deviceModel && + fieldConfig.deviceModel.show && ( + + + {t("deviceModel")} + + + { + client.fingerprint + .deviceModel + } + + + )} + + {client.fingerprint.serialNumber && + fieldConfig.serialNumber.show && ( + + + {t("serialNumber")} + + + { + client.fingerprint + .serialNumber + } + + + )} + + {client.fingerprint.username && + fieldConfig.username.show && ( + + + {t("username")} + + + { + client.fingerprint + .username + } + + + )} + + {client.fingerprint.hostname && + fieldConfig.hostname.show && ( + + + {t("hostname")} + + + { + client.fingerprint + .hostname + } + + + )} + + {client.fingerprint.firstSeen && ( - {t("kernelVersion")} + {t("firstSeen")} - {client.fingerprint.kernelVersion} + {formatTimestamp( + client.fingerprint + .firstSeen + )} )} - {client.fingerprint.arch && - fieldConfig.arch.show && ( + {client.fingerprint.lastSeen && ( - {t("architecture")} + {t("lastSeen")} - {client.fingerprint.arch} + {formatTimestamp( + client.fingerprint + .lastSeen + )} )} - - {client.fingerprint.deviceModel && - fieldConfig.deviceModel.show && ( - - - {t("deviceModel")} - - - {client.fingerprint.deviceModel} - - - )} - - {client.fingerprint.serialNumber && - fieldConfig.serialNumber.show && ( - - - {t("serialNumber")} - - - {client.fingerprint.serialNumber} - - - )} - - {client.fingerprint.username && - fieldConfig.username.show && ( - - - {t("username")} - - - {client.fingerprint.username} - - - )} - - {client.fingerprint.hostname && - fieldConfig.hostname.show && ( - - - {t("hostname")} - - - {client.fingerprint.hostname} - - - )} - - {client.fingerprint.firstSeen && ( - - - {t("firstSeen")} - - - {formatTimestamp( - client.fingerprint.firstSeen - )} - - - )} - - {client.fingerprint.lastSeen && ( - - - {t("lastSeen")} - - - {formatTimestamp( - client.fingerprint.lastSeen - )} - - - )} -
- ); - })()} +
+ ); + })()}
)} diff --git a/src/app/[orgId]/settings/clients/user/page.tsx b/src/app/[orgId]/settings/clients/user/page.tsx index dee24532..35a2b2e3 100644 --- a/src/app/[orgId]/settings/clients/user/page.tsx +++ b/src/app/[orgId]/settings/clients/user/page.tsx @@ -41,6 +41,32 @@ export default async function ClientsPage(props: ClientsPageProps) { const mapClientToRow = ( client: ListClientsResponse["clients"][0] ): ClientRow => { + // Build fingerprint object if any fingerprint data exists + const hasFingerprintData = + (client as any).fingerprintPlatform || + (client as any).fingerprintOsVersion || + (client as any).fingerprintKernelVersion || + (client as any).fingerprintArch || + (client as any).fingerprintSerialNumber || + (client as any).fingerprintUsername || + (client as any).fingerprintHostname || + (client as any).deviceModel; + + const fingerprint = hasFingerprintData + ? { + platform: (client as any).fingerprintPlatform || null, + osVersion: (client as any).fingerprintOsVersion || null, + kernelVersion: + (client as any).fingerprintKernelVersion || null, + arch: (client as any).fingerprintArch || null, + deviceModel: (client as any).deviceModel || null, + serialNumber: + (client as any).fingerprintSerialNumber || null, + username: (client as any).fingerprintUsername || null, + hostname: (client as any).fingerprintHostname || null + } + : null; + return { name: client.name, id: client.clientId, @@ -58,7 +84,8 @@ export default async function ClientsPage(props: ClientsPageProps) { agent: client.agent, archived: client.archived || false, blocked: client.blocked || false, - approvalState: client.approvalState + approvalState: client.approvalState, + fingerprint }; }; diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index d85adae2..26d6fab1 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -27,7 +27,7 @@ import ClientDownloadBanner from "./ClientDownloadBanner"; import { Badge } from "./ui/badge"; import { build } from "@server/build"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { t } from "@faker-js/faker/dist/airline-DF6RqYmq"; +import { InfoPopup } from "@app/components/ui/info-popup"; export type ClientRow = { id: number; @@ -48,6 +48,16 @@ export type ClientRow = { approvalState: "approved" | "pending" | "denied" | null; archived?: boolean; blocked?: boolean; + fingerprint?: { + platform: string | null; + osVersion: string | null; + kernelVersion: string | null; + arch: string | null; + deviceModel: string | null; + serialNumber: string | null; + username: string | null; + hostname: string | null; + } | null; }; type ClientTableProps = { @@ -55,10 +65,52 @@ type ClientTableProps = { orgId: string; }; +function formatPlatform(platform: string | null | undefined): string { + if (!platform) return "-"; + const platformMap: Record = { + macos: "macOS", + windows: "Windows", + linux: "Linux", + ios: "iOS", + android: "Android", + unknown: "Unknown" + }; + return platformMap[platform.toLowerCase()] || platform; +} + export default function UserDevicesTable({ userClients }: ClientTableProps) { const router = useRouter(); const t = useTranslations(); + const formatFingerprintInfo = (fingerprint: ClientRow["fingerprint"]): string => { + if (!fingerprint) return ""; + const parts: string[] = []; + + if (fingerprint.platform) { + parts.push(`${t("platform")}: ${formatPlatform(fingerprint.platform)}`); + } + if (fingerprint.deviceModel) { + parts.push(`${t("deviceModel")}: ${fingerprint.deviceModel}`); + } + if (fingerprint.osVersion) { + parts.push(`${t("osVersion")}: ${fingerprint.osVersion}`); + } + if (fingerprint.arch) { + parts.push(`${t("architecture")}: ${fingerprint.arch}`); + } + if (fingerprint.hostname) { + parts.push(`${t("hostname")}: ${fingerprint.hostname}`); + } + if (fingerprint.username) { + parts.push(`${t("username")}: ${fingerprint.username}`); + } + if (fingerprint.serialNumber) { + parts.push(`${t("serialNumber")}: ${fingerprint.serialNumber}`); + } + + return parts.join("\n"); + }; + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedClient, setSelectedClient] = useState( null @@ -182,7 +234,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { { accessorKey: "name", enableHiding: false, - friendlyName: "Name", + friendlyName: t("name"), header: ({ column }) => { return ( ); }, cell: ({ row }) => { const r = row.original; + const fingerprintInfo = r.fingerprint + ? formatFingerprintInfo(r.fingerprint) + : null; return (
{r.name} + {fingerprintInfo && ( + +
+
+ {t("deviceInformation")} +
+
+ {fingerprintInfo} +
+
+
+ )} {r.archived && ( {t("archived")} @@ -250,7 +317,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }, { accessorKey: "userEmail", - friendlyName: "User", + friendlyName: t("users"), header: ({ column }) => { return ( ); @@ -284,7 +351,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }, { accessorKey: "online", - friendlyName: "Connectivity", + friendlyName: t("online"), header: ({ column }) => { return ( ); @@ -306,14 +373,14 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { return (
- Connected + {t("online")}
); } else { return (
- Disconnected + {t("offline")}
); } @@ -321,7 +388,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }, { accessorKey: "mbIn", - friendlyName: "Data In", + friendlyName: t("dataIn"), header: ({ column }) => { return ( ); @@ -340,7 +407,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }, { accessorKey: "mbOut", - friendlyName: "Data Out", + friendlyName: t("dataOut"), header: ({ column }) => { return ( ); @@ -399,7 +466,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }, { accessorKey: "subnet", - friendlyName: "Address", + friendlyName: t("address"), header: ({ column }) => { return ( ); @@ -445,8 +512,8 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { > {clientRow.archived - ? "Unarchive" - : "Archive"} + ? t("actionUnarchiveClient") + : t("actionArchiveClient")} {clientRow.blocked - ? "Unblock" - : "Block"} + ? t("actionUnblockClient") + : t("actionBlockClient")} {!clientRow.userId && ( @@ -473,7 +540,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { }} > - Delete + {t("delete")} )} @@ -483,7 +550,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { href={`/${clientRow.orgId}/settings/clients/user/${clientRow.niceId}`} > @@ -510,10 +577,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {

{t("clientMessageRemove")}

} - buttonText="Confirm Delete Client" + buttonText={t("actionDeleteClient")} onConfirm={async () => deleteClient(selectedClient!.id)} string={selectedClient.name} - title="Delete Client" + title={t("actionDeleteClient")} /> )} diff --git a/src/components/ui/info-popup.tsx b/src/components/ui/info-popup.tsx index cff1cce4..b7c0f55e 100644 --- a/src/components/ui/info-popup.tsx +++ b/src/components/ui/info-popup.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Info } from "lucide-react"; import { Popover, @@ -17,25 +17,61 @@ interface InfoPopupProps { } export function InfoPopup({ text, info, trigger, children }: InfoPopupProps) { + const [open, setOpen] = useState(false); + const timeoutRef = useRef(null); + + const handleMouseEnter = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setOpen(true); + }; + + const handleMouseLeave = () => { + // Add a small delay to prevent flickering when moving between trigger and content + timeoutRef.current = setTimeout(() => { + setOpen(false); + }, 100); + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + const defaultTrigger = ( ); + const triggerElement = trigger ?? defaultTrigger; + return (
{text && {text}} - - - {trigger ?? defaultTrigger} + + + {triggerElement} - + {children || (info && (