format device approval message

This commit is contained in:
miloschwartz
2026-01-19 21:00:41 -08:00
parent b299f3d6aa
commit 7305c721a6
5 changed files with 199 additions and 66 deletions

View File

@@ -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 (
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="inline-flex items-start md:items-center gap-2">
<LaptopMinimal className="size-4 text-muted-foreground flex-none relative top-2 sm:top-0" />
<span>
<span className="text-primary">
<Link
href={`/${orgId}/settings/access/users/${approval.user.userId}/access-controls`}
className="text-primary hover:underline cursor-pointer"
>
{getUserDisplayName({
email: approval.user.email,
name: approval.user.name,
username: approval.user.username
})}
</span>
</Link>
&nbsp;
{approval.type === "user_device" && (
<span>{t("requestingNewDeviceApproval")}</span>
<span className="inline-flex items-center gap-1">
{approval.deviceName ? (
<>
{t("requestingNewDeviceApproval")}:{" "}
{approval.clientId ? (
<Link
href={`/${orgId}/settings/clients/user/${approval.clientId}/general`}
className="text-primary hover:underline cursor-pointer"
>
{approval.deviceName}
</Link>
) : (
<span>{approval.deviceName}</span>
)}
{approval.fingerprint && (
<InfoPopup>
<div className="space-y-1 text-sm">
<div className="font-semibold mb-2">
{t("deviceInformation")}
</div>
<div className="text-muted-foreground whitespace-pre-line">
{formatFingerprintInfo(approval.fingerprint, t)}
</div>
</div>
</InfoPopup>
)}
</>
) : (
<span>{t("requestingNewDeviceApproval")}</span>
)}
</span>
)}
</span>
</div>
@@ -231,17 +266,20 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
<Badge variant="red">{t("denied")}</Badge>
)}
<Button
variant="outline"
onClick={() => {}}
className="gap-2"
asChild
>
<Link href={"#"}>
{t("viewDetails")}
<ArrowRight className="size-4 flex-none" />
</Link>
</Button>
{approval.clientId && (
<Button
variant="outline"
className="gap-2"
asChild
>
<Link
href={`/${orgId}/settings/clients/user/${approval.clientId}/general`}
>
{t("viewDetails")}
<ArrowRight className="size-4 flex-none" />
</Link>
</Button>
)}
</div>
</div>
);

View File

@@ -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<string, string> = {
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<ClientRow | null>(
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 (
<div className="flex items-center gap-2">

View File

@@ -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<string, string> = {
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");
}

View File

@@ -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 = {