From 7305c721a6555f89f9b9f9039403d3a3d07c0589 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 19 Jan 2026 21:00:41 -0800 Subject: [PATCH] format device approval message --- .../routers/approvals/listApprovals.ts | 68 +++++++++++++++++- src/components/ApprovalFeed.tsx | 70 ++++++++++++++----- src/components/UserDevicesTable.tsx | 49 +------------ src/lib/formatDeviceFingerprint.ts | 67 ++++++++++++++++++ src/lib/queries.ts | 11 +++ 5 files changed, 199 insertions(+), 66 deletions(-) create mode 100644 src/lib/formatDeviceFingerprint.ts diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts index 76a895a6..d518555f 100644 --- a/server/private/routers/approvals/listApprovals.ts +++ b/server/private/routers/approvals/listApprovals.ts @@ -21,9 +21,10 @@ import type { Request, Response, NextFunction } from "express"; import { build } from "@server/build"; import { getOrgTierData } from "@server/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; -import { approvals, clients, db, users, type Approval } from "@server/db"; +import { approvals, clients, db, users, olms, fingerprints, type Approval } from "@server/db"; import { eq, isNull, sql, not, and, desc } from "drizzle-orm"; import response from "@server/lib/response"; +import { getUserDeviceName } from "@server/db/names"; const paramsSchema = z.strictObject({ orgId: z.string() @@ -82,7 +83,16 @@ async function queryApprovals( userId: users.userId, username: users.username, email: users.email - } + }, + clientName: clients.name, + 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(approvals) .innerJoin(users, and(eq(approvals.userId, users.userId))) @@ -93,6 +103,8 @@ async function queryApprovals( not(isNull(clients.userId)) // only user devices ) ) + .leftJoin(olms, eq(clients.clientId, olms.clientId)) + .leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId)) .where( and( eq(approvals.orgId, orgId), @@ -105,7 +117,57 @@ async function queryApprovals( ) .limit(limit) .offset(offset); - return res; + + // Process results to format device names and build fingerprint objects + return res.map((approval) => { + const model = approval.deviceModel || null; + const deviceName = approval.clientName + ? getUserDeviceName(model, approval.clientName) + : null; + + // Build fingerprint object if any fingerprint data exists + const hasFingerprintData = + approval.fingerprintPlatform || + approval.fingerprintOsVersion || + approval.fingerprintKernelVersion || + approval.fingerprintArch || + approval.fingerprintSerialNumber || + approval.fingerprintUsername || + approval.fingerprintHostname || + approval.deviceModel; + + const fingerprint = hasFingerprintData + ? { + platform: approval.fingerprintPlatform || null, + osVersion: approval.fingerprintOsVersion || null, + kernelVersion: approval.fingerprintKernelVersion || null, + arch: approval.fingerprintArch || null, + deviceModel: approval.deviceModel || null, + serialNumber: approval.fingerprintSerialNumber || null, + username: approval.fingerprintUsername || null, + hostname: approval.fingerprintHostname || null + } + : null; + + const { + clientName, + deviceModel, + fingerprintPlatform, + fingerprintOsVersion, + fingerprintKernelVersion, + fingerprintArch, + fingerprintSerialNumber, + fingerprintUsername, + fingerprintHostname, + ...rest + } = approval; + + return { + ...rest, + deviceName, + fingerprint + }; + }); } export type ListApprovalsResponse = { diff --git a/src/components/ApprovalFeed.tsx b/src/components/ApprovalFeed.tsx index 974a02b2..fdc3c1aa 100644 --- a/src/components/ApprovalFeed.tsx +++ b/src/components/ApprovalFeed.tsx @@ -4,17 +4,19 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { cn } from "@app/lib/cn"; +import { formatFingerprintInfo, formatPlatform } from "@app/lib/formatDeviceFingerprint"; import { approvalFiltersSchema, approvalQueries, type ApprovalItem } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; -import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react"; +import { ArrowRight, Ban, Check, Laptop, Smartphone, RefreshCw } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Fragment, useActionState } from "react"; +import type { LucideIcon } from "lucide-react"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { Card, CardHeader } from "./ui/card"; @@ -27,6 +29,7 @@ import { SelectValue } from "./ui/select"; import { Separator } from "./ui/separator"; +import { InfoPopup } from "./ui/info-popup"; export type ApprovalFeedProps = { orgId: string; @@ -183,18 +186,50 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) { return (
- - + {getUserDisplayName({ email: approval.user.email, name: approval.user.name, username: approval.user.username })} - +   {approval.type === "user_device" && ( - {t("requestingNewDeviceApproval")} + + {approval.deviceName ? ( + <> + {t("requestingNewDeviceApproval")}:{" "} + {approval.clientId ? ( + + {approval.deviceName} + + ) : ( + {approval.deviceName} + )} + {approval.fingerprint && ( + +
+
+ {t("deviceInformation")} +
+
+ {formatFingerprintInfo(approval.fingerprint, t)} +
+
+
+ )} + + ) : ( + {t("requestingNewDeviceApproval")} + )} +
)}
@@ -231,17 +266,20 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) { {t("denied")} )} - + {approval.clientId && ( + + )}
); diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 11e5bead..0e84f619 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -13,6 +13,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; +import { formatFingerprintInfo, formatPlatform } from "@app/lib/formatDeviceFingerprint"; import { ArrowRight, ArrowUpDown, @@ -66,56 +67,10 @@ 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 @@ -258,7 +213,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { cell: ({ row }) => { const r = row.original; const fingerprintInfo = r.fingerprint - ? formatFingerprintInfo(r.fingerprint) + ? formatFingerprintInfo(r.fingerprint, t) : null; return (
diff --git a/src/lib/formatDeviceFingerprint.ts b/src/lib/formatDeviceFingerprint.ts new file mode 100644 index 00000000..3bd4a99b --- /dev/null +++ b/src/lib/formatDeviceFingerprint.ts @@ -0,0 +1,67 @@ +type DeviceFingerprint = { + 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; + +/** + * Formats a platform string to a human-readable format + */ +export 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; +} + +/** + * Formats device fingerprint information into a human-readable string + * + * @param fingerprint - The device fingerprint object + * @param t - Translation function from next-intl + * @returns Formatted string with device information + */ +export function formatFingerprintInfo( + fingerprint: DeviceFingerprint, + t: (key: string) => string +): 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"); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index d016ec77..f471c5a2 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -342,6 +342,17 @@ export type ApprovalItem = { username: string; email: string | null; }; + deviceName: string | null; + 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; }; export const approvalQueries = {