Files
cve-dashboard/frontend/src/App.js
Jordan Ramos 8c789ce765 Add View As (impersonation) feature for Admin users
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
2026-06-24 12:57:57 -06:00

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>
);
}