Initial commit - frontend

This commit is contained in:
Bailey Taylor
2025-09-19 03:26:52 +00:00
commit 27e9a08ee0
234 changed files with 32097 additions and 0 deletions

159
src/context/AuthContext.tsx Normal file
View File

@@ -0,0 +1,159 @@
//src/context/AuthContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import Cookies from 'js-cookie';
import { jwtDecode } from 'jwt-decode';
import { useRouter } from 'next/navigation';
import { disableConsoleInProd } from '@/lib/disableConsole';
disableConsoleInProd();
interface JwtPayload {
sub: string;
displayname: string;
userId?: number;
idauth?: string;
exp: number;
roles?: string[];
}
interface AuthContextType {
username: string;
displayname: string;
userId?: number;
clientIdentifier?: string;
roles?: string[]; // 👈 Add this
loading: boolean;
refreshAuth: () => Promise<void>;
authToken?: string;
}
interface AuthProviderProps {
children: ReactNode;
username?: string;
displayname?: string;
roles?: string[];
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({
children,
username = '',
displayname = '',
roles = [],
}: AuthProviderProps) => {
const [authInfo, setAuthInfo] = useState<AuthContextType>(() => ({
username,
displayname,
roles,
userId: undefined,
clientIdentifier: undefined,
loading: true,
authToken: undefined,
refreshAuth: () => Promise.resolve(), // stub, will be replaced later
}));
const router = useRouter();
const refreshAuth = async () => {
if (typeof window === 'undefined') return;
console.log("🔁 [AuthContext] Running refreshAuth");
setAuthInfo((prev) => ({ ...prev, loading: true }));
const token = Cookies.get('authToken');
console.log("🔐 Retrieved token from cookie:", token);
if (!token) {
setAuthInfo({
username: '',
displayname: '',
userId: undefined,
clientIdentifier: undefined,
roles: [],
authToken: undefined,
loading: false,
refreshAuth, // <- update it here
});
return;
}
try {
const decoded = jwtDecode<JwtPayload>(token);
console.log("🎯 Decoded username:", decoded.sub);
setAuthInfo({
username: decoded.sub,
displayname: decoded.displayname,
userId: decoded.userId,
clientIdentifier: decoded.idauth,
roles: decoded.roles ?? [],
authToken: token,
loading: false,
refreshAuth, // ✅ inject real function now
});
} catch (err) {
console.warn("❌ Failed to decode JWT in refreshAuth", err);
setAuthInfo((prev) => ({ ...prev, loading: false, refreshAuth }));
}
};
useEffect(() => {
// ✅ Don't run refreshAuth if we already got username from SSR
if (username && username !== 'user') {
setAuthInfo((prev) => ({ ...prev, loading: false }));
return;
}
console.log("🏁 [AuthContext] Calling refreshAuth from useEffect");
refreshAuth();
const interval = setInterval(() => {
const token = Cookies.get('authToken');
if (!token) return;
try {
const decoded = jwtDecode<JwtPayload>(token);
const now = Date.now() / 1000;
if (decoded.exp < now) {
localStorage.setItem('authRedirectReason', 'Session expired. Please log in again.');
Cookies.remove('authToken');
router.push('/login');
}
} catch (err) {
console.warn('Token check failed', err);
}
}, 30000);
return () => clearInterval(interval);
}, []);
return (
<AuthContext.Provider value={{ ...authInfo, refreshAuth }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -0,0 +1,213 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import api from '@/lib/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;
}
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 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;
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(devicesEndpoint, { withCredentials: true }),
api.get(softwareEndpoint, { withCredentials: true }),
]);
console.log('✅ Devices fetched:', devicesRes.data);
console.log('✅ Software fetched:', softwareRes.data);
console.groupEnd();
// STEP 1: Set basic data
setDevices(devicesRes.data.devices);
setDeviceVulns(devicesRes.data.vulnerabilitiesByDevice);
setCachedSoftware(softwareRes.data);
// STEP 2: Extract all unique CVE IDs
const uniqueCveIds = new Set<string>();
(Object.values(devicesRes.data.vulnerabilitiesByDevice) as DeviceVulnerability[][]).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: any) {
console.group('❌ Device fetch failed');
console.error('🔴 Axios Error Message:', error.message);
console.error('🧾 Axios Error Config:', error.config);
if (error.response) {
console.error('📉 Status:', error.response.status);
console.error('📄 Response Headers:', error.response.headers);
console.error('📄 Response Data:', error.response.data);
} else if (error.request) {
console.error('🛑 No response received:', error.request);
}
console.groupEnd();
} finally {
setLoading(false);
}
};
fetchDevicesVulnsAndSoftware();
}, [devices.length, initialData, username, authLoading]);
useEffect(() => {
console.log("🧠 Devices updated:", devices);
}, [devices]);
if (typeof window !== 'undefined') {
(window as any).__debug_devices = devices;
}
return (
<DeviceContext.Provider value={{
devices,
deviceVulns,
cachedSoftware,
detailedCveLookup, // ✅ provide it!
setDevices,
setDeviceVulns,
setCachedSoftware,
setDetailedCveLookup, // ✅ provide this too!
loading,
}}>
{children}
</DeviceContext.Provider>
);
};

View File

@@ -0,0 +1,43 @@
// src/context/ThemeContext.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { disableConsoleInProd } from '@/lib/disableConsole';
disableConsoleInProd();
interface ThemeContextType {
darkMode: boolean;
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProviderCustom = ({ children }: { children: React.ReactNode }) => {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const stored = localStorage.getItem('theme');
const prefersDark = stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches);
setDarkMode(prefersDark);
document.documentElement.classList.toggle('dark', prefersDark);
}, []);
const toggle = () => {
const next = !darkMode;
setDarkMode(next);
document.documentElement.classList.toggle('dark', next);
localStorage.setItem('theme', next ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ darkMode, toggle }}>
{children}
</ThemeContext.Provider>
);
};
export const useThemeMode = () => {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useThemeMode must be used within ThemeProviderCustom');
return ctx;
};