Allow Admin users to temporarily view the app as another user to verify permissions and team scoping without switching accounts. Backend: - Migration: add impersonate_user_id column to sessions table - requireAuth(): when impersonation is active, override req.user with target user's identity; store real admin identity in req.realUser - POST /api/auth/impersonate: start impersonation (Admin only, cannot impersonate self or other Admins) - POST /api/auth/stop-impersonate: end impersonation, revert to real user - GET /api/auth/me: returns impersonating flag and realUser when active - Audit logging on impersonate start/stop Frontend: - AuthContext: add impersonating, realUser state; startImpersonation() and stopImpersonation() helpers - ImpersonationBanner: fixed amber banner showing target user identity with Exit button - UserManagement: Eye icon button on each non-Admin user row to start View As (visible only to Admin, hidden for self and other Admins) - App.js: mount ImpersonationBanner at top of authenticated view
190 lines
9.1 KiB
JavaScript
190 lines
9.1 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Plus, RefreshCw, Menu, Loader } from 'lucide-react';
|
|
import { useAuth } from './contexts/AuthContext';
|
|
import LoginForm from './components/LoginForm';
|
|
import UserMenu from './components/UserMenu';
|
|
import UserManagement from './components/UserManagement';
|
|
import AuditLog from './components/AuditLog';
|
|
import NvdSyncModal from './components/NvdSyncModal';
|
|
import NavDrawer from './components/NavDrawer';
|
|
import AdminScopeToggle from './components/AdminScopeToggle';
|
|
import ImpersonationBanner from './components/ImpersonationBanner';
|
|
import VulnerabilityTriagePage from './components/pages/ReportingPage';
|
|
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
|
import ExportsPage from './components/pages/ExportsPage';
|
|
import CompliancePage from './components/pages/CompliancePage';
|
|
import CCPMetricsPage from './components/pages/CCPMetricsPage';
|
|
import JiraPage from './components/pages/JiraPage';
|
|
import AdminPage from './components/pages/AdminPage';
|
|
import ArcherTemplatePage from './components/pages/ArcherTemplatePage';
|
|
import HomePage from './components/pages/HomePage';
|
|
import FeedbackModal from './components/FeedbackModal';
|
|
import NotificationBell from './components/NotificationBell';
|
|
import { canAccessPage } from './config/pageVisibility';
|
|
import './App.css';
|
|
|
|
export default function App() {
|
|
const { isAuthenticated, loading: authLoading, canWrite, user } = useAuth();
|
|
|
|
const [currentPage, setCurrentPageRaw] = useState(() => {
|
|
try {
|
|
const saved = localStorage.getItem('cve-dashboard-page');
|
|
return saved && canAccessPage(saved, user?.group) ? saved : 'home';
|
|
} catch { return 'home'; }
|
|
});
|
|
const setCurrentPage = (page) => {
|
|
if (!canAccessPage(page, user?.group)) { setCurrentPageRaw('home'); return; }
|
|
setCurrentPageRaw(page);
|
|
try { localStorage.setItem('cve-dashboard-page', page); } catch {}
|
|
};
|
|
|
|
const [navOpen, setNavOpen] = useState(false);
|
|
const [calendarFilter, setCalendarFilter] = useState(null);
|
|
const [reportingExcFilter, setReportingExcFilter] = useState(null);
|
|
const [showAddCVE, setShowAddCVE] = useState(false);
|
|
const [showUserManagement, setShowUserManagement] = useState(false);
|
|
const [showAuditLog, setShowAuditLog] = useState(false);
|
|
const [showFeedback, setShowFeedback] = useState(false);
|
|
const [feedbackType, setFeedbackType] = useState('bug');
|
|
const [showNvdSync, setShowNvdSync] = useState(false);
|
|
|
|
// Navigation handler that accepts optional context (filters)
|
|
const handleNavigate = (page, context) => {
|
|
if (page === 'triage') {
|
|
setCalendarFilter(context?.calendarFilter || null);
|
|
setReportingExcFilter(context?.reportingExcFilter || null);
|
|
}
|
|
setCurrentPage(page);
|
|
};
|
|
|
|
// Show loading while checking auth
|
|
if (authLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<Loader className="w-12 h-12 text-[#0476D9] mx-auto animate-spin" />
|
|
<p className="text-gray-600 mt-4">Loading...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show login if not authenticated
|
|
if (!isAuthenticated) {
|
|
return <LoginForm />;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-intel-darkest grid-bg p-6 relative overflow-hidden fade-in">
|
|
<ImpersonationBanner />
|
|
<NavDrawer
|
|
isOpen={navOpen}
|
|
onClose={() => setNavOpen(false)}
|
|
currentPage={currentPage}
|
|
onNavigate={(page) => {
|
|
if (page === 'triage') { setCalendarFilter(null); setReportingExcFilter(null); }
|
|
setCurrentPage(page);
|
|
}}
|
|
/>
|
|
{/* Scanning line effect */}
|
|
<div className="scan-line"></div>
|
|
|
|
<div className={`${currentPage === 'triage' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10`}>
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<div className="flex justify-between items-start mb-6">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<button
|
|
onClick={() => setNavOpen(true)}
|
|
style={{ background: 'none', border: '1px solid rgba(14, 165, 233, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', cursor: 'pointer', color: '#64748B', flexShrink: 0 }}
|
|
onMouseEnter={e => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.6)'; }}
|
|
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.25)'; }}
|
|
title="Navigation"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<img src="/shieldlogo.jpeg" alt="AEGIS" style={{ width: '44px', height: '44px', borderRadius: '6px' }} />
|
|
<div>
|
|
<h1 className="text-4xl font-bold text-intel-accent mb-1 font-mono tracking-tight">
|
|
AEGIS
|
|
</h1>
|
|
<p className="text-gray-400 text-sm font-sans">Advanced Engineering Group Intelligence System</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => setShowNvdSync(true)}
|
|
className="intel-button intel-button-success relative z-10 flex items-center gap-2"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
NVD Sync
|
|
</button>
|
|
)}
|
|
{canWrite() && currentPage === 'home' && (
|
|
<button
|
|
onClick={() => setShowAddCVE(true)}
|
|
className="intel-button intel-button-primary relative z-10 flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Entry
|
|
</button>
|
|
)}
|
|
<AdminScopeToggle />
|
|
<button
|
|
onClick={() => { setFeedbackType('bug'); setShowFeedback(true); }}
|
|
title="Report a Bug"
|
|
style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
|
|
padding: '0.4rem 0.7rem',
|
|
background: 'rgba(239,68,68,0.08)',
|
|
border: '1px solid rgba(239,68,68,0.25)',
|
|
borderRadius: '0.375rem',
|
|
color: '#94A3B8',
|
|
fontSize: '0.72rem',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
cursor: 'pointer',
|
|
transition: 'all 0.15s',
|
|
}}
|
|
onMouseEnter={e => { e.currentTarget.style.color = '#F87171'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.5)'; }}
|
|
onMouseLeave={e => { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.25)'; }}
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
|
|
Bug
|
|
</button>
|
|
<NotificationBell />
|
|
<UserMenu onManageUsers={() => setShowUserManagement(true)} onAuditLog={() => setShowAuditLog(true)} onFeatureRequest={() => { setFeedbackType('feature'); setShowFeedback(true); }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Page content — generic route guard via canAccessPage */}
|
|
{currentPage === 'home' && <HomePage onNavigate={handleNavigate} showAddCVE={showAddCVE} setShowAddCVE={setShowAddCVE} />}
|
|
{currentPage === 'triage' && <VulnerabilityTriagePage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
|
|
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
|
{currentPage === 'ccp-metrics' && <CCPMetricsPage />}
|
|
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
|
{currentPage === 'exports' && <ExportsPage />}
|
|
{currentPage === 'jira' && <JiraPage />}
|
|
{currentPage === 'archer-templates' && <ArcherTemplatePage />}
|
|
{currentPage === 'admin' && <AdminPage />}
|
|
|
|
{/* Global Modals */}
|
|
{showUserManagement && <UserManagement onClose={() => setShowUserManagement(false)} />}
|
|
{showAuditLog && <AuditLog onClose={() => setShowAuditLog(false)} />}
|
|
{showNvdSync && <NvdSyncModal onClose={() => setShowNvdSync(false)} onSyncComplete={() => {}} />}
|
|
<FeedbackModal
|
|
isOpen={showFeedback}
|
|
onClose={() => setShowFeedback(false)}
|
|
defaultType={feedbackType}
|
|
currentPage={currentPage}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|