Initial commit - frontend
This commit is contained in:
159
src/context/AuthContext.tsx
Normal file
159
src/context/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
213
src/context/DeviceContext.tsx
Normal file
213
src/context/DeviceContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
43
src/context/ThemeContext.tsx
Normal file
43
src/context/ThemeContext.tsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user