mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
add display info for device posture
This commit is contained in:
@@ -2489,6 +2489,8 @@
|
||||
"logIn": "Log In",
|
||||
"deviceInformation": "Device Information",
|
||||
"deviceInformationDescription": "Information about the device and agent",
|
||||
"deviceSecurity": "Device Security",
|
||||
"deviceSecurityDescription": "Device security posture information",
|
||||
"platform": "Platform",
|
||||
"macosVersion": "macOS Version",
|
||||
"windowsVersion": "Windows Version",
|
||||
@@ -2501,6 +2503,17 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "First Seen",
|
||||
"lastSeen": "Last Seen",
|
||||
"biometricsEnabled": "Biometrics Enabled",
|
||||
"diskEncrypted": "Disk Encrypted",
|
||||
"firewallEnabled": "Firewall Enabled",
|
||||
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||
"tpmAvailable": "TPM Available",
|
||||
"windowsDefenderEnabled": "Windows Defender Enabled",
|
||||
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "View device information and settings",
|
||||
"devicePendingApprovalDescription": "This device is waiting for approval",
|
||||
"deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.",
|
||||
|
||||
51
server/private/middlewares/verifyValidSubscription.ts
Normal file
51
server/private/middlewares/verifyValidSubscription.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { build } from "@server/build";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
|
||||
export async function verifyValidLicense(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
if (build != "saas") {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { tier, active } = await getOrgTierData(orgId);
|
||||
const subscribed = tier === TierId.STANDARD;
|
||||
if (!subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"This organization's current plan does not support this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error verifying subscription"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
import { build } from "@server/build";
|
||||
|
||||
const getClientSchema = z.strictObject({
|
||||
clientId: z
|
||||
@@ -51,6 +52,106 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
type PostureData = {
|
||||
biometricsEnabled?: boolean | null;
|
||||
diskEncrypted?: boolean | null;
|
||||
firewallEnabled?: boolean | null;
|
||||
autoUpdatesEnabled?: boolean | null;
|
||||
tpmAvailable?: boolean | null;
|
||||
windowsDefenderEnabled?: boolean | null;
|
||||
macosSipEnabled?: boolean | null;
|
||||
macosGatekeeperEnabled?: boolean | null;
|
||||
macosFirewallStealthMode?: boolean | null;
|
||||
linuxAppArmorEnabled?: boolean | null;
|
||||
linuxSELinuxEnabled?: boolean | null;
|
||||
};
|
||||
|
||||
function getPlatformPostureData(
|
||||
platform: string | null | undefined,
|
||||
fingerprint: typeof currentFingerprint.$inferSelect | null
|
||||
): PostureData | null {
|
||||
if (!fingerprint) return null;
|
||||
|
||||
const normalizedPlatform = platform?.toLowerCase() || "unknown";
|
||||
const posture: PostureData = {};
|
||||
|
||||
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender
|
||||
if (normalizedPlatform === "windows") {
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) {
|
||||
posture.macosSipEnabled = fingerprint.macosSipEnabled;
|
||||
}
|
||||
if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) {
|
||||
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
|
||||
}
|
||||
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
|
||||
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
|
||||
}
|
||||
}
|
||||
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
|
||||
else if (normalizedPlatform === "linux") {
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) {
|
||||
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
|
||||
}
|
||||
if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) {
|
||||
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
|
||||
}
|
||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||
}
|
||||
}
|
||||
// iOS: Biometric configuration
|
||||
else if (normalizedPlatform === "ios") {
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Only return if we have at least one posture field
|
||||
return Object.keys(posture).length > 0 ? posture : null;
|
||||
}
|
||||
|
||||
export type GetClientResponse = NonNullable<
|
||||
Awaited<ReturnType<typeof query>>
|
||||
>["clients"] & {
|
||||
@@ -69,6 +170,7 @@ export type GetClientResponse = NonNullable<
|
||||
firstSeen: number | null;
|
||||
lastSeen: number | null;
|
||||
} | null;
|
||||
posture: PostureData | null;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
@@ -152,13 +254,23 @@ export async function getClient(
|
||||
}
|
||||
: null;
|
||||
|
||||
// Build posture data if available (platform-specific)
|
||||
let postureData: PostureData | null = null;
|
||||
if (build !== "oss") {
|
||||
postureData = getPlatformPostureData(
|
||||
client.currentFingerprint?.platform || null,
|
||||
client.currentFingerprint
|
||||
);
|
||||
}
|
||||
|
||||
const data: GetClientResponse = {
|
||||
...client.clients,
|
||||
name: clientName,
|
||||
olmId: client.olms ? client.olms.olmId : null,
|
||||
agent: client.olms?.agent || null,
|
||||
olmVersion: client.olms?.version || null,
|
||||
fingerprint: fingerprintData
|
||||
fingerprint: fingerprintData,
|
||||
posture: postureData
|
||||
};
|
||||
|
||||
return response<GetClientResponse>(res, {
|
||||
|
||||
@@ -22,12 +22,13 @@ import {
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import ActionBanner from "@app/components/ActionBanner";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { Check, Ban, Shield, ShieldOff, Clock } from "lucide-react";
|
||||
import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||
import { SiAndroid } from "react-icons/si";
|
||||
@@ -111,18 +112,12 @@ function getPlatformFieldConfig(
|
||||
kernelVersion: { show: false, labelKey: "kernelVersion" },
|
||||
arch: { show: true, labelKey: "architecture" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||
serialNumber: { show: true, labelKey: "serialNumber" },
|
||||
username: { show: true, labelKey: "username" },
|
||||
hostname: { show: true, labelKey: "hostname" }
|
||||
},
|
||||
android: {
|
||||
osVersion: { show: true, labelKey: "androidVersion" },
|
||||
kernelVersion: { show: true, labelKey: "kernelVersion" },
|
||||
arch: { show: true, labelKey: "architecture" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||
serialNumber: { show: true, labelKey: "serialNumber" },
|
||||
username: { show: true, labelKey: "username" },
|
||||
hostname: { show: true, labelKey: "hostname" }
|
||||
},
|
||||
unknown: {
|
||||
osVersion: { show: true, labelKey: "osVersion" },
|
||||
@@ -138,6 +133,7 @@ function getPlatformFieldConfig(
|
||||
return configs[normalizedPlatform] || configs.unknown;
|
||||
}
|
||||
|
||||
|
||||
export default function GeneralPage() {
|
||||
const { client, updateClient } = useClientContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
@@ -152,6 +148,20 @@ export default function GeneralPage() {
|
||||
|
||||
const showApprovalFeatures = build !== "oss" && isPaidUser;
|
||||
|
||||
const formatPostureValue = (value: boolean | null | undefined) => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{value ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-600" />
|
||||
)}
|
||||
<span>{value ? t("enabled") : t("disabled")}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch approval ID for this client if pending
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -407,13 +417,13 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.fingerprint.osVersion &&
|
||||
fieldConfig.osVersion.show && (
|
||||
fieldConfig.osVersion?.show && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t(
|
||||
fieldConfig
|
||||
.osVersion
|
||||
.labelKey
|
||||
?.labelKey || "osVersion"
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
@@ -426,7 +436,7 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.fingerprint.kernelVersion &&
|
||||
fieldConfig.kernelVersion.show && (
|
||||
fieldConfig.kernelVersion?.show && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("kernelVersion")}
|
||||
@@ -456,7 +466,7 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.fingerprint.deviceModel &&
|
||||
fieldConfig.deviceModel.show && (
|
||||
fieldConfig.deviceModel?.show && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("deviceModel")}
|
||||
@@ -486,7 +496,7 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.fingerprint.username &&
|
||||
fieldConfig.username.show && (
|
||||
fieldConfig.username?.show && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("username")}
|
||||
@@ -501,7 +511,7 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.fingerprint.hostname &&
|
||||
fieldConfig.hostname.show && (
|
||||
fieldConfig.hostname?.show && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("hostname")}
|
||||
@@ -548,6 +558,218 @@ export default function GeneralPage() {
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Device Security Section */}
|
||||
{build !== "oss" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("deviceSecurity")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("deviceSecurityDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
{client.posture && Object.keys(client.posture).length > 0 ? (
|
||||
<>
|
||||
{!isPaidUser && <PaidFeaturesAlert />}
|
||||
<InfoSections cols={3}>
|
||||
{client.posture.biometricsEnabled !== null &&
|
||||
client.posture.biometricsEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("biometricsEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.biometricsEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.diskEncrypted !== null &&
|
||||
client.posture.diskEncrypted !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("diskEncrypted")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.diskEncrypted
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.firewallEnabled !== null &&
|
||||
client.posture.firewallEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("firewallEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.firewallEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.autoUpdatesEnabled !== null &&
|
||||
client.posture.autoUpdatesEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("autoUpdatesEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.autoUpdatesEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.tpmAvailable !== null &&
|
||||
client.posture.tpmAvailable !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("tpmAvailable")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.tpmAvailable
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.windowsDefenderEnabled !== null &&
|
||||
client.posture.windowsDefenderEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("windowsDefenderEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.windowsDefenderEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.macosSipEnabled !== null &&
|
||||
client.posture.macosSipEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosSipEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.macosSipEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.macosGatekeeperEnabled !== null &&
|
||||
client.posture.macosGatekeeperEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosGatekeeperEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.macosGatekeeperEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.macosFirewallStealthMode !== null &&
|
||||
client.posture.macosFirewallStealthMode !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosFirewallStealthMode")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.macosFirewallStealthMode
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.linuxAppArmorEnabled !== null &&
|
||||
client.posture.linuxAppArmorEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("linuxAppArmorEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.linuxAppArmorEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.linuxSELinuxEnabled !== null &&
|
||||
client.posture.linuxSELinuxEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("linuxSELinuxEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.linuxSELinuxEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSections>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted-foreground">
|
||||
{t("noData")}
|
||||
</div>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,10 +145,10 @@ function CollapsibleNavItem({
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
|
||||
{notificationCount !== undefined &&
|
||||
notificationCount > 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
>
|
||||
{notificationCount > 99 ? "99+" : notificationCount}
|
||||
<Badge variant="secondary">
|
||||
{notificationCount > 99
|
||||
? "99+"
|
||||
: notificationCount}
|
||||
</Badge>
|
||||
)}
|
||||
{build === "enterprise" &&
|
||||
@@ -321,9 +321,7 @@ export function SidebarNav({
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
{notificationCount !== undefined &&
|
||||
notificationCount > 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
>
|
||||
<Badge variant="secondary">
|
||||
{notificationCount > 99
|
||||
? "99+"
|
||||
: notificationCount}
|
||||
@@ -346,8 +344,8 @@ export function SidebarNav({
|
||||
notificationCount !== undefined &&
|
||||
notificationCount > 0 && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="absolute -top-1 -right-1 h-5 min-w-5 px-1.5 flex items-center justify-center text-xs bg-primary text-primary-foreground"
|
||||
variant="secondary"
|
||||
className="absolute -top-1 -right-1 h-5 min-w-5 px-1.5 flex items-center justify-center text-xs"
|
||||
>
|
||||
{notificationCount > 99 ? "99+" : notificationCount}
|
||||
</Badge>
|
||||
@@ -379,7 +377,7 @@ export function SidebarNav({
|
||||
{notificationCount !== undefined &&
|
||||
notificationCount > 0 && (
|
||||
<Badge
|
||||
variant="default"
|
||||
variant="secondary"
|
||||
className="flex-shrink-0 bg-primary text-primary-foreground"
|
||||
>
|
||||
{notificationCount > 99
|
||||
|
||||
@@ -38,6 +38,7 @@ export function formatFingerprintInfo(
|
||||
): string {
|
||||
if (!fingerprint) return "";
|
||||
const parts: string[] = [];
|
||||
const normalizedPlatform = fingerprint.platform?.toLowerCase() || "unknown";
|
||||
|
||||
if (fingerprint.platform) {
|
||||
parts.push(
|
||||
@@ -53,14 +54,17 @@ export function formatFingerprintInfo(
|
||||
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}`);
|
||||
|
||||
if (normalizedPlatform !== "ios" && normalizedPlatform !== "android") {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user