894 lines
25 KiB
TypeScript
894 lines
25 KiB
TypeScript
'use client';
|
|
|
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
import api from '@/lib/axios';
|
|
import type { AxiosError } from 'axios';
|
|
import { useAuth } from '@/context/AuthContext';
|
|
import { DetailedDevice } from '@/types/devices';
|
|
import { disableConsoleInProd } from '@/lib/disableConsole';
|
|
disableConsoleInProd();
|
|
|
|
interface DriveInfo {
|
|
name: string;
|
|
driveType: string;
|
|
totalSizeGB: number;
|
|
freeSpaceGB: number;
|
|
}
|
|
|
|
interface IpAddress {
|
|
interfaceName: string;
|
|
ipAddress: string;
|
|
}
|
|
|
|
interface MacAddress {
|
|
interfaceName: string;
|
|
macAddress: string;
|
|
}
|
|
|
|
interface InstalledApp {
|
|
app_name: string;
|
|
app_version: string;
|
|
publisher: string;
|
|
}
|
|
|
|
type RawRecord = Record<string, unknown>;
|
|
|
|
const normalizeKey = (key: string): string =>
|
|
key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
|
|
const getDirectValue = (source: RawRecord | null | undefined, keys: string[]): unknown => {
|
|
if (!source) return undefined;
|
|
for (const key of keys) {
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
const value = source[key];
|
|
if (value !== undefined && value !== null) {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const findValueDeep = (
|
|
source: unknown,
|
|
normalizedTargets: string[],
|
|
seen = new WeakSet<object>()
|
|
): unknown => {
|
|
if (!source || typeof source !== 'object') return undefined;
|
|
|
|
const stack: unknown[] = [source];
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
if (!current || typeof current !== 'object') continue;
|
|
|
|
if (seen.has(current as object)) continue;
|
|
seen.add(current as object);
|
|
|
|
if (Array.isArray(current)) {
|
|
for (const item of current) {
|
|
if (item && (typeof item === 'object' || typeof item === 'string')) {
|
|
stack.push(item);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(current as RawRecord)) {
|
|
const normalizedKey = normalizeKey(key);
|
|
if (normalizedTargets.includes(normalizedKey) && value !== undefined && value !== null) {
|
|
return value;
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
stack.push(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
const resolveValue = (
|
|
source: RawRecord | null | undefined,
|
|
keys: string[],
|
|
fallbackKeys: string[] = keys
|
|
): unknown => {
|
|
if (!source) return undefined;
|
|
|
|
const direct = getDirectValue(source, keys);
|
|
if (direct !== undefined && direct !== null) {
|
|
return direct;
|
|
}
|
|
|
|
const normalizedTargets = Array.from(new Set(fallbackKeys.map(normalizeKey))).filter(Boolean);
|
|
return findValueDeep(source, normalizedTargets);
|
|
};
|
|
|
|
const resolveArray = <T = unknown>(
|
|
source: RawRecord | null | undefined,
|
|
keys: string[],
|
|
fallbackKeys?: string[]
|
|
): T[] | undefined => {
|
|
const value = resolveValue(source, keys, fallbackKeys ?? keys);
|
|
if (Array.isArray(value)) {
|
|
return value as T[];
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const toOptionalString = (value: unknown): string | undefined => {
|
|
if (value === null || value === undefined) return undefined;
|
|
if (typeof value === 'string') return value;
|
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
return undefined;
|
|
};
|
|
|
|
const toNonEmptyString = (value: unknown, fallback = ''): string => {
|
|
const candidate = toOptionalString(value)?.trim();
|
|
return candidate && candidate.length > 0 ? candidate : fallback;
|
|
};
|
|
|
|
const parseSizeToGB = (value: unknown): number => {
|
|
if (value === null || value === undefined) return 0;
|
|
|
|
const normalizeNumeric = (num: number): number => {
|
|
if (!Number.isFinite(num)) return 0;
|
|
if (num > 1024 ** 3) return +(num / 1024 ** 3).toFixed(2); // bytes
|
|
if (num > 1024 ** 2) return +(num / 1024 ** 2).toFixed(2); // KB
|
|
if (num > 1024) return +(num / 1024).toFixed(2); // MB
|
|
return +num.toFixed(2); // already GB
|
|
};
|
|
|
|
if (typeof value === 'number') {
|
|
return normalizeNumeric(value);
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return 0;
|
|
|
|
const numeric = parseFloat(trimmed.replace(/[^\d.\-]/g, ''));
|
|
if (!Number.isFinite(numeric)) return 0;
|
|
|
|
if (/tb/i.test(trimmed)) return +(numeric * 1024).toFixed(2);
|
|
if (/gb/i.test(trimmed)) return +numeric.toFixed(2);
|
|
if (/mb/i.test(trimmed)) return +(numeric / 1024).toFixed(2);
|
|
if (/kb/i.test(trimmed)) return +(numeric / (1024 ** 2)).toFixed(2);
|
|
if (/bytes?/i.test(trimmed)) return +(numeric / (1024 ** 3)).toFixed(2);
|
|
|
|
return normalizeNumeric(numeric);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
const normalizeDrive = (driveInput: unknown, index: number): DriveInfo | null => {
|
|
if (!driveInput || typeof driveInput !== 'object') return null;
|
|
const drive = driveInput as RawRecord;
|
|
|
|
const name = toNonEmptyString(
|
|
resolveValue(drive, ['name', 'driveName', 'label', 'volume'], [
|
|
'name',
|
|
'driveName',
|
|
'label',
|
|
'volume',
|
|
'drive_letter',
|
|
'driveletter',
|
|
'mount_point',
|
|
'mountpoint',
|
|
'logicaldisk',
|
|
'deviceid',
|
|
]) ?? `Drive ${index + 1}`,
|
|
`Drive ${index + 1}`
|
|
);
|
|
const driveType = toNonEmptyString(
|
|
resolveValue(drive, ['driveType', 'drive_type', 'type'], [
|
|
'driveType',
|
|
'drive_type',
|
|
'type',
|
|
'drivetype',
|
|
'mediatype',
|
|
'busType',
|
|
]),
|
|
'Unknown'
|
|
);
|
|
const totalSizeGB = parseSizeToGB(
|
|
resolveValue(
|
|
drive,
|
|
['totalSizeGB', 'total_size_gb', 'totalSize', 'total_size', 'size', 'capacity', 'totalBytes', 'total_bytes'],
|
|
[
|
|
'totalSizeGB',
|
|
'total_size_gb',
|
|
'totalSize',
|
|
'total_size',
|
|
'size',
|
|
'capacity',
|
|
'totalBytes',
|
|
'total_bytes',
|
|
'size_bytes',
|
|
'capacity_bytes',
|
|
'totalsize',
|
|
'totalspace',
|
|
'totalspacebytes',
|
|
]
|
|
)
|
|
);
|
|
const freeSpaceGB = parseSizeToGB(
|
|
resolveValue(
|
|
drive,
|
|
['freeSpaceGB', 'free_space_gb', 'freeSpace', 'free_space', 'free', 'freeBytes', 'free_bytes', 'availableBytes'],
|
|
[
|
|
'freeSpaceGB',
|
|
'free_space_gb',
|
|
'freeSpace',
|
|
'free_space',
|
|
'free',
|
|
'freeBytes',
|
|
'free_bytes',
|
|
'availableBytes',
|
|
'available_bytes',
|
|
'freespace',
|
|
'freespacebytes',
|
|
'remaining',
|
|
]
|
|
)
|
|
);
|
|
|
|
if (!name && !totalSizeGB && !freeSpaceGB) return null;
|
|
|
|
return {
|
|
name,
|
|
driveType,
|
|
totalSizeGB,
|
|
freeSpaceGB,
|
|
};
|
|
};
|
|
|
|
const extractDrives = (rawInput: unknown): DriveInfo[] => {
|
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
|
const raw = rawInput as RawRecord;
|
|
|
|
const potentialArrays = [
|
|
resolveArray(raw, ['drives']),
|
|
resolveArray(raw, ['driveInfo']),
|
|
resolveArray(raw, ['drive_info']),
|
|
resolveArray(raw, ['storageDevices']),
|
|
resolveArray(raw, ['drive_details']),
|
|
resolveArray(raw, ['storage']),
|
|
resolveArray(raw, ['logicalDrives']),
|
|
resolveArray(raw, ['logical_drives']),
|
|
resolveArray(raw, ['volumes']),
|
|
resolveArray(raw, ['volume_info']),
|
|
resolveArray(raw, ['disks']),
|
|
];
|
|
|
|
for (const candidate of potentialArrays) {
|
|
if (Array.isArray(candidate)) {
|
|
return candidate
|
|
.map((drive, idx: number) => normalizeDrive(drive, idx))
|
|
.filter((drive): drive is DriveInfo => Boolean(drive));
|
|
}
|
|
}
|
|
|
|
const aggregatedDrive = {
|
|
totalSize: resolveValue(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage'], [
|
|
'total_disk_space',
|
|
'totalDiskSpace',
|
|
'total_storage',
|
|
'totalSpace',
|
|
'total_space',
|
|
'storage_total',
|
|
'disk_total',
|
|
]),
|
|
freeSpace: resolveValue(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage'], [
|
|
'free_disk_space',
|
|
'freeDiskSpace',
|
|
'available_storage',
|
|
'free_storage',
|
|
'freeSpace',
|
|
'free_space',
|
|
'availableSpace',
|
|
'available_space',
|
|
'storage_free',
|
|
]),
|
|
};
|
|
|
|
if (aggregatedDrive.totalSize || aggregatedDrive.freeSpace) {
|
|
const normalized = normalizeDrive(
|
|
{
|
|
name: 'System',
|
|
driveType: 'Unknown',
|
|
totalSizeGB: aggregatedDrive.totalSize,
|
|
freeSpaceGB: aggregatedDrive.freeSpace,
|
|
},
|
|
0
|
|
);
|
|
|
|
return normalized ? [normalized] : [];
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const normalizeIpAddress = (entryInput: unknown, index: number): IpAddress | null => {
|
|
if (!entryInput || typeof entryInput !== 'object') return null;
|
|
const entry = entryInput as RawRecord;
|
|
|
|
const ipAddress = toNonEmptyString(
|
|
resolveValue(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6'], [
|
|
'ipAddress',
|
|
'ip_address',
|
|
'address',
|
|
'ip',
|
|
'ipv4',
|
|
'ipv6',
|
|
'primary_ip',
|
|
'primaryIp',
|
|
'lan_ip',
|
|
]) ?? '',
|
|
''
|
|
);
|
|
|
|
const interfaceName = toNonEmptyString(
|
|
resolveValue(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic'], [
|
|
'interfaceName',
|
|
'interface_name',
|
|
'adapter',
|
|
'name',
|
|
'nic',
|
|
'network_adapter',
|
|
'interface',
|
|
'description',
|
|
]) ?? '',
|
|
ipAddress ? `Interface ${index + 1}` : ''
|
|
);
|
|
|
|
if (!ipAddress && !interfaceName) return null;
|
|
|
|
const macAddress = toOptionalString(
|
|
resolveValue(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress'], [
|
|
'macAddress',
|
|
'mac_address',
|
|
'mac',
|
|
'physicalAddress',
|
|
'physical_address',
|
|
'hw_address',
|
|
])
|
|
);
|
|
|
|
return {
|
|
interfaceName: interfaceName || `Interface ${index + 1}`,
|
|
ipAddress: ipAddress || 'Unknown',
|
|
macAddress: macAddress?.trim() || undefined,
|
|
};
|
|
};
|
|
|
|
const extractIpAddresses = (rawInput: unknown): IpAddress[] => {
|
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
|
const raw = rawInput as RawRecord;
|
|
|
|
const potentialArrays = [
|
|
resolveArray(raw, ['ipAddresses']),
|
|
resolveArray(raw, ['ip_addresses']),
|
|
resolveArray(raw, ['networkInterfaces']),
|
|
resolveArray(raw, ['network_interfaces']),
|
|
resolveArray(raw, ['interfaces']),
|
|
resolveArray(raw, ['networkAdapters']),
|
|
resolveArray(raw, ['network_adapters']),
|
|
resolveArray(raw, ['network']),
|
|
];
|
|
|
|
for (const candidate of potentialArrays) {
|
|
if (Array.isArray(candidate)) {
|
|
return candidate
|
|
.map((entry, idx: number) => normalizeIpAddress(entry, idx))
|
|
.filter((entry): entry is IpAddress => Boolean(entry));
|
|
}
|
|
}
|
|
|
|
const singleIp = resolveValue(raw, ['ipAddress', 'ip_address', 'primary_ip', 'primaryIp']);
|
|
if (singleIp) {
|
|
return [
|
|
normalizeIpAddress(
|
|
{
|
|
ipAddress: singleIp,
|
|
interfaceName: resolveValue(raw, ['interfaceName', 'interface_name', 'adapter']),
|
|
macAddress: resolveValue(raw, ['macAddress', 'mac_address']),
|
|
},
|
|
0
|
|
) as IpAddress,
|
|
].filter(Boolean);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const normalizeInstalledApp = (appInput: unknown): InstalledApp | null => {
|
|
if (!appInput || typeof appInput !== 'object') return null;
|
|
const app = appInput as RawRecord;
|
|
|
|
const appName = toNonEmptyString(
|
|
resolveValue(app, ['app_name', 'name', 'title'], [
|
|
'app_name',
|
|
'name',
|
|
'title',
|
|
'display_name',
|
|
'product_name',
|
|
]) ?? '',
|
|
''
|
|
);
|
|
if (!appName) return null;
|
|
|
|
return {
|
|
app_name: appName,
|
|
app_version: toNonEmptyString(
|
|
resolveValue(app, ['app_version', 'version'], ['app_version', 'version', 'display_version', 'product_version']) ?? '',
|
|
''
|
|
),
|
|
publisher: toNonEmptyString(
|
|
resolveValue(app, ['publisher', 'manufacturer', 'vendor'], [
|
|
'publisher',
|
|
'manufacturer',
|
|
'vendor',
|
|
'company',
|
|
'publisher_name',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
};
|
|
};
|
|
|
|
const extractInstalledApps = (rawInput: unknown): InstalledApp[] => {
|
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
|
const raw = rawInput as RawRecord;
|
|
|
|
const potentialArrays = [
|
|
resolveArray(raw, ['installedApplications']),
|
|
resolveArray(raw, ['installed_applications']),
|
|
resolveArray(raw, ['software']),
|
|
resolveArray(raw, ['applications']),
|
|
resolveArray(raw, ['apps']),
|
|
resolveArray(raw, ['installedPrograms']),
|
|
resolveArray(raw, ['installed_programs']),
|
|
resolveArray(raw, ['programs']),
|
|
];
|
|
|
|
for (const candidate of potentialArrays) {
|
|
if (Array.isArray(candidate)) {
|
|
return candidate
|
|
.map((app) => normalizeInstalledApp(app))
|
|
.filter((app): app is InstalledApp => Boolean(app));
|
|
}
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const extractMacAddresses = (rawInput: unknown, ipAddresses: IpAddress[]): MacAddress[] => {
|
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
|
const raw = rawInput as RawRecord;
|
|
const potentialArrays = resolveArray(raw, ['macAddresses', 'mac_addresses', 'macs', 'mac_list']);
|
|
|
|
if (Array.isArray(potentialArrays)) {
|
|
return potentialArrays
|
|
.map((entry, idx: number) => {
|
|
if (!entry || typeof entry !== 'object') return null;
|
|
const record = entry as RawRecord;
|
|
const macAddress = toOptionalString(
|
|
resolveValue(record, ['macAddress', 'mac_address', 'mac'], [
|
|
'macAddress',
|
|
'mac_address',
|
|
'mac',
|
|
'physicalAddress',
|
|
'hw_address',
|
|
])
|
|
);
|
|
if (!macAddress) return null;
|
|
return {
|
|
interfaceName: toNonEmptyString(
|
|
resolveValue(record, ['interfaceName', 'interface_name', 'adapter'], [
|
|
'interfaceName',
|
|
'interface_name',
|
|
'adapter',
|
|
'network_adapter',
|
|
'name',
|
|
]) ?? `Interface ${idx + 1}`,
|
|
`Interface ${idx + 1}`
|
|
),
|
|
macAddress: macAddress.trim(),
|
|
};
|
|
})
|
|
.filter((entry): entry is MacAddress => Boolean(entry));
|
|
}
|
|
|
|
return ipAddresses
|
|
.filter((entry) => Boolean(entry.macAddress))
|
|
.map((entry) => ({
|
|
interfaceName: entry.interfaceName,
|
|
macAddress: entry.macAddress as string,
|
|
}));
|
|
};
|
|
|
|
const splitGpuNames = (value: unknown): string[] => {
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.map((item) => toNonEmptyString(item, ''))
|
|
.filter((item) => item.length > 0);
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return value
|
|
.split(/[,;\n]+/)
|
|
.map((item) => item.trim())
|
|
.filter((item) => item.length > 0);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const normalizeDevice = (rawInput: unknown): DetailedDevice => {
|
|
const raw: RawRecord =
|
|
rawInput && typeof rawInput === 'object' ? (rawInput as RawRecord) : {};
|
|
|
|
const deviceId = Number(
|
|
resolveValue(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk'], [
|
|
'deviceId',
|
|
'device_id',
|
|
'id',
|
|
'deviceID',
|
|
'device_id_pk',
|
|
]) ?? 0
|
|
);
|
|
|
|
const drives = extractDrives(raw);
|
|
const ipAddresses = extractIpAddresses(raw);
|
|
const macAddresses = extractMacAddresses(raw, ipAddresses);
|
|
const clientEntry = resolveValue(raw, ['client'], ['client', 'clientInfo', 'customer']);
|
|
const clientNameFromNested =
|
|
clientEntry && typeof clientEntry === 'object'
|
|
? resolveValue(clientEntry as RawRecord, ['name', 'clientName', 'client_name'])
|
|
: undefined;
|
|
|
|
const hostname = toNonEmptyString(
|
|
resolveValue(raw, ['hostname', 'hostName', 'computerName', 'deviceName'], [
|
|
'hostname',
|
|
'hostName',
|
|
'computerName',
|
|
'deviceName',
|
|
'host',
|
|
'machineName',
|
|
]) ?? `Device ${deviceId}`,
|
|
deviceId ? `Device ${deviceId}` : 'Unknown Device'
|
|
);
|
|
|
|
return {
|
|
deviceId,
|
|
hostname,
|
|
osName: toNonEmptyString(
|
|
resolveValue(
|
|
raw,
|
|
['osName', 'os_name', 'operatingSystem', 'os'],
|
|
[
|
|
'osName',
|
|
'os_name',
|
|
'operatingSystem',
|
|
'operating_system',
|
|
'operatingSystemName',
|
|
'operating_system_name',
|
|
'osFriendlyName',
|
|
'os_caption',
|
|
'oscaption',
|
|
'os',
|
|
]
|
|
) ?? '',
|
|
''
|
|
),
|
|
osVersion: toNonEmptyString(
|
|
resolveValue(raw, ['osVersion', 'os_version', 'osVersionString'], [
|
|
'osVersion',
|
|
'os_version',
|
|
'osVersionString',
|
|
'operatingSystemVersion',
|
|
'osversion',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
windowsVersion: toNonEmptyString(
|
|
resolveValue(raw, ['windowsVersion', 'windows_version', 'osRelease'], [
|
|
'windowsVersion',
|
|
'windows_version',
|
|
'osRelease',
|
|
'windowsRelease',
|
|
'windows_release',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
windowsBuild: toNonEmptyString(
|
|
resolveValue(raw, ['windowsBuild', 'windows_build', 'buildNumber'], [
|
|
'windowsBuild',
|
|
'windows_build',
|
|
'buildNumber',
|
|
'build_number',
|
|
'windowsBuildNumber',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
osArchitecture: toNonEmptyString(
|
|
resolveValue(raw, ['osArchitecture', 'os_architecture', 'architecture'], [
|
|
'osArchitecture',
|
|
'os_architecture',
|
|
'architecture',
|
|
'platform',
|
|
'osarch',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
processorName: toNonEmptyString(
|
|
resolveValue(raw, ['processorName', 'processor_name', 'cpuName'], [
|
|
'processorName',
|
|
'processor_name',
|
|
'cpuName',
|
|
'cpu_name',
|
|
'processor',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
processorArchitecture: toNonEmptyString(
|
|
resolveValue(raw, ['processorArchitecture', 'processor_architecture', 'cpuArchitecture'], [
|
|
'processorArchitecture',
|
|
'processor_architecture',
|
|
'cpuArchitecture',
|
|
'cpu_architecture',
|
|
'cpuArch',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
gpuNames: splitGpuNames(
|
|
resolveValue(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus', 'graphics']) ?? []
|
|
),
|
|
totalMemory: toNonEmptyString(
|
|
resolveValue(
|
|
raw,
|
|
['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB'],
|
|
[
|
|
'totalMemory',
|
|
'total_memory',
|
|
'memory',
|
|
'totalMemoryMb',
|
|
'totalMemoryMB',
|
|
'memory_total',
|
|
'ram_total',
|
|
'physicalMemory',
|
|
]
|
|
) ?? '',
|
|
''
|
|
),
|
|
lastBootTime: toNonEmptyString(
|
|
resolveValue(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime'], [
|
|
'lastBootTime',
|
|
'last_boot_time',
|
|
'bootTime',
|
|
'lastBoot',
|
|
'lastBootUpTime',
|
|
'last_boot',
|
|
'boot_time',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
lastCheckedIn: toNonEmptyString(
|
|
resolveValue(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at'], [
|
|
'lastCheckedIn',
|
|
'last_checked_in',
|
|
'lastSeen',
|
|
'checked_in_at',
|
|
'lastSeenAt',
|
|
'last_seen',
|
|
]) ?? '',
|
|
''
|
|
),
|
|
drives,
|
|
ipAddresses,
|
|
macAddresses,
|
|
installedApplications: extractInstalledApps(raw),
|
|
clientName:
|
|
toOptionalString(
|
|
resolveValue(raw, ['clientName', 'client_name'], ['clientName', 'client_name', 'client']) ??
|
|
clientNameFromNested ??
|
|
resolveValue(raw, ['clientIdentifier', 'client_identifier'])
|
|
) ?? undefined,
|
|
};
|
|
};
|
|
|
|
|
|
interface DeviceVulnerability {
|
|
cveId: string;
|
|
title: string;
|
|
severity: string;
|
|
score?: number;
|
|
publishedDate: string;
|
|
lastModifiedDate: string;
|
|
}
|
|
|
|
interface CachedSoftwareEntry {
|
|
id: number;
|
|
softwareName: string;
|
|
hostname: string;
|
|
version: string;
|
|
deviceId: number;
|
|
totalCves: number;
|
|
lastUpdated: string;
|
|
}
|
|
|
|
interface RawDevicesResponse {
|
|
devices?: unknown[];
|
|
vulnerabilitiesByDevice?: { [deviceId: string]: DeviceVulnerability[] };
|
|
}
|
|
|
|
interface DeviceContextType {
|
|
devices: DetailedDevice[];
|
|
deviceVulns: { [deviceId: string]: DeviceVulnerability[] };
|
|
cachedSoftware: CachedSoftwareEntry[];
|
|
detailedCveLookup: { [cveId: string]: DeviceVulnerability }; // 🔥 New
|
|
setDevices: React.Dispatch<React.SetStateAction<DetailedDevice[]>>;
|
|
setDeviceVulns: React.Dispatch<React.SetStateAction<{ [deviceId: string]: DeviceVulnerability[] }>>;
|
|
setCachedSoftware: React.Dispatch<React.SetStateAction<CachedSoftwareEntry[]>>;
|
|
setDetailedCveLookup: React.Dispatch<React.SetStateAction<{ [cveId: string]: DeviceVulnerability }>>;
|
|
loading: boolean;
|
|
}
|
|
|
|
|
|
const DeviceContext = createContext<DeviceContextType | undefined>(undefined);
|
|
|
|
|
|
export const useDeviceContext = () => {
|
|
const context = useContext(DeviceContext);
|
|
if (!context) {
|
|
throw new Error('useDeviceContext must be used within a DeviceProvider');
|
|
}
|
|
return context;
|
|
};
|
|
|
|
export const DeviceProvider = ({
|
|
children,
|
|
initialData,
|
|
}: {
|
|
children: React.ReactNode;
|
|
initialData?: {
|
|
devices: DetailedDevice[];
|
|
vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] };
|
|
};
|
|
}) => {
|
|
const [devices, setDevices] = useState<DetailedDevice[]>(() => {
|
|
if (initialData?.devices?.length) {
|
|
return initialData.devices.map((device) => normalizeDevice(device));
|
|
}
|
|
return [];
|
|
});
|
|
|
|
const [deviceVulns, setDeviceVulns] = useState<{ [deviceId: string]: DeviceVulnerability[] }>(
|
|
initialData?.vulnerabilitiesByDevice ?? {}
|
|
);
|
|
|
|
const [detailedCveLookup, setDetailedCveLookup] = useState<{ [cveId: string]: DeviceVulnerability }>({});
|
|
|
|
const [cachedSoftware, setCachedSoftware] = useState<CachedSoftwareEntry[]>([]); // ⬅️ NEW
|
|
|
|
const [loading, setLoading] = useState<boolean>(!initialData);
|
|
|
|
const { username, roles, loading: authLoading } = useAuth();
|
|
|
|
useEffect(() => {
|
|
if (devices.length > 0 || initialData || authLoading || !username) return;
|
|
|
|
const fetchDevicesVulnsAndSoftware = async () => {
|
|
try {
|
|
const isAdmin = roles?.includes('ADMIN');
|
|
const devicesEndpoint = isAdmin
|
|
? '/cached/devices/with-vulns/all'
|
|
: '/cached/devices/with-vulns';
|
|
const softwareEndpoint = '/cached/software/summary';
|
|
|
|
console.group('📡 Fetching Devices, Vulnerabilities, and Software');
|
|
|
|
const [devicesRes, softwareRes] = await Promise.all([
|
|
api.get<RawDevicesResponse>(devicesEndpoint, { withCredentials: true }),
|
|
api.get<CachedSoftwareEntry[]>(softwareEndpoint, { withCredentials: true }),
|
|
]);
|
|
|
|
console.log('✅ Devices fetched:', devicesRes.data);
|
|
console.log('✅ Software fetched:', softwareRes.data);
|
|
console.groupEnd();
|
|
|
|
// STEP 1: Set basic data (keep original UTC timestamps for calculations)
|
|
const normalizedDevices = Array.isArray(devicesRes.data?.devices)
|
|
? (devicesRes.data.devices as unknown[]).map((device) => normalizeDevice(device))
|
|
: [];
|
|
|
|
setDevices(normalizedDevices);
|
|
setDeviceVulns(devicesRes.data?.vulnerabilitiesByDevice ?? {});
|
|
setCachedSoftware(softwareRes.data ?? []);
|
|
|
|
// STEP 2: Extract all unique CVE IDs
|
|
const uniqueCveIds = new Set<string>();
|
|
Object.values(devicesRes.data?.vulnerabilitiesByDevice ?? {}).forEach((vulns) => {
|
|
vulns.forEach((vuln) => {
|
|
if (vuln.cveId) {
|
|
uniqueCveIds.add(vuln.cveId);
|
|
}
|
|
});
|
|
});
|
|
|
|
// STEP 3: Fetch detailed CVE data
|
|
if (uniqueCveIds.size > 0) {
|
|
console.log('🔎 Fetching detailed CVEs for:', Array.from(uniqueCveIds));
|
|
const params = new URLSearchParams();
|
|
Array.from(uniqueCveIds).forEach(cveId => {
|
|
params.append('cveIds', cveId);
|
|
});
|
|
|
|
const cveDetailsRes = await api.get<DeviceVulnerability[]>('/vuln/cves/lookup', {
|
|
params,
|
|
withCredentials: true,
|
|
});
|
|
|
|
|
|
|
|
|
|
const lookupMap: { [cveId: string]: DeviceVulnerability } = {};
|
|
cveDetailsRes.data.forEach((cve) => {
|
|
lookupMap[cve.cveId] = cve;
|
|
});
|
|
|
|
setDetailedCveLookup(lookupMap);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.group('❌ Device fetch failed');
|
|
const axiosError = error as AxiosError;
|
|
console.error('🔴 Axios Error Message:', axiosError.message);
|
|
console.error('🧾 Axios Error Config:', axiosError.config);
|
|
if (axiosError.response) {
|
|
console.error('📉 Status:', axiosError.response.status);
|
|
console.error('📄 Response Headers:', axiosError.response.headers);
|
|
console.error('📄 Response Data:', axiosError.response.data);
|
|
} else if (axiosError.request) {
|
|
console.error('🛑 No response received:', axiosError.request);
|
|
} else {
|
|
console.error('❓ Unexpected error:', error);
|
|
}
|
|
console.groupEnd();
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
fetchDevicesVulnsAndSoftware();
|
|
}, [devices.length, initialData, username, authLoading, roles]);
|
|
|
|
useEffect(() => {
|
|
console.log("🧠 Devices updated:", devices);
|
|
}, [devices]);
|
|
|
|
if (typeof window !== 'undefined') {
|
|
(window as Window & { __debug_devices?: DetailedDevice[] }).__debug_devices = devices;
|
|
}
|
|
|
|
return (
|
|
<DeviceContext.Provider value={{
|
|
devices,
|
|
deviceVulns,
|
|
cachedSoftware,
|
|
detailedCveLookup, // ✅ provide it!
|
|
setDevices,
|
|
setDeviceVulns,
|
|
setCachedSoftware,
|
|
setDetailedCveLookup, // ✅ provide this too!
|
|
loading,
|
|
}}>
|
|
|
|
{children}
|
|
</DeviceContext.Provider>
|
|
);
|
|
};
|