From da14c92d98ddc07b1be785d68984cdea7658c7e8 Mon Sep 17 00:00:00 2001 From: jramos Date: Wed, 28 Jan 2026 14:36:33 -0700 Subject: [PATCH] added required code changes, components, and packages for login feature --- .gitignore | 3 + backend/middleware/auth.js | 70 ++++ backend/routes/auth.js | 184 +++++++++++ backend/routes/users.js | 217 ++++++++++++ backend/server.js | 56 ++-- backend/setup.js | 74 ++++- frontend/src/App.js | 129 +++++--- frontend/src/components/LoginForm.js | 107 ++++++ frontend/src/components/UserManagement.js | 380 ++++++++++++++++++++++ frontend/src/components/UserMenu.js | 94 ++++++ frontend/src/contexts/AuthContext.js | 112 +++++++ frontend/src/index.js | 5 +- package.json | 2 + 13 files changed, 1370 insertions(+), 63 deletions(-) create mode 100644 backend/middleware/auth.js create mode 100644 backend/routes/auth.js create mode 100644 backend/routes/users.js create mode 100644 frontend/src/components/LoginForm.js create mode 100644 frontend/src/components/UserManagement.js create mode 100644 frontend/src/components/UserMenu.js create mode 100644 frontend/src/contexts/AuthContext.js diff --git a/.gitignore b/.gitignore index cbb3c9b..9634c48 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ frontend.pid # Temporary files backend/uploads/temp/ +claude.md +claude_status.md +feature_request*.md diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..2a9f6b5 --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,70 @@ +// Authentication Middleware + +// Require authenticated user +function requireAuth(db) { + return async (req, res, next) => { + const sessionId = req.cookies?.session_id; + + if (!sessionId) { + return res.status(401).json({ error: 'Authentication required' }); + } + + try { + const session = await new Promise((resolve, reject) => { + db.get( + `SELECT s.*, u.id as user_id, u.username, u.email, u.role, u.is_active + FROM sessions s + JOIN users u ON s.user_id = u.id + WHERE s.session_id = ? AND s.expires_at > datetime('now')`, + [sessionId], + (err, row) => { + if (err) reject(err); + else resolve(row); + } + ); + }); + + if (!session) { + return res.status(401).json({ error: 'Session expired or invalid' }); + } + + if (!session.is_active) { + return res.status(401).json({ error: 'Account is disabled' }); + } + + // Attach user to request + req.user = { + id: session.user_id, + username: session.username, + email: session.email, + role: session.role + }; + + next(); + } catch (err) { + console.error('Auth middleware error:', err); + return res.status(500).json({ error: 'Authentication error' }); + } + }; +} + +// Require specific role(s) +function requireRole(...allowedRoles) { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ error: 'Authentication required' }); + } + + if (!allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + error: 'Insufficient permissions', + required: allowedRoles, + current: req.user.role + }); + } + + next(); + }; +} + +module.exports = { requireAuth, requireRole }; diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..4b5e584 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,184 @@ +// Authentication Routes +const express = require('express'); +const bcrypt = require('bcryptjs'); +const crypto = require('crypto'); + +function createAuthRouter(db) { + const router = express.Router(); + + // Login + router.post('/login', async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: 'Username and password are required' }); + } + + try { + // Find user + const user = await new Promise((resolve, reject) => { + db.get( + 'SELECT * FROM users WHERE username = ?', + [username], + (err, row) => { + if (err) reject(err); + else resolve(row); + } + ); + }); + + if (!user) { + return res.status(401).json({ error: 'Invalid username or password' }); + } + + if (!user.is_active) { + return res.status(401).json({ error: 'Account is disabled' }); + } + + // Verify password + const validPassword = await bcrypt.compare(password, user.password_hash); + if (!validPassword) { + return res.status(401).json({ error: 'Invalid username or password' }); + } + + // Generate session ID + const sessionId = crypto.randomBytes(32).toString('hex'); + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours + + // Create session + await new Promise((resolve, reject) => { + db.run( + 'INSERT INTO sessions (session_id, user_id, expires_at) VALUES (?, ?, ?)', + [sessionId, user.id, expiresAt.toISOString()], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + + // Update last login + await new Promise((resolve, reject) => { + db.run( + 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', + [user.id], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + + // Set cookie + res.cookie('session_id', sessionId, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 24 * 60 * 60 * 1000 // 24 hours + }); + + res.json({ + message: 'Login successful', + user: { + id: user.id, + username: user.username, + email: user.email, + role: user.role + } + }); + } catch (err) { + console.error('Login error:', err); + res.status(500).json({ error: 'Login failed' }); + } + }); + + // Logout + router.post('/logout', async (req, res) => { + const sessionId = req.cookies?.session_id; + + if (sessionId) { + // Delete session from database + await new Promise((resolve) => { + db.run( + 'DELETE FROM sessions WHERE session_id = ?', + [sessionId], + () => resolve() + ); + }); + } + + // Clear cookie + res.clearCookie('session_id'); + res.json({ message: 'Logged out successfully' }); + }); + + // Get current user + router.get('/me', async (req, res) => { + const sessionId = req.cookies?.session_id; + + if (!sessionId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const session = await new Promise((resolve, reject) => { + db.get( + `SELECT s.*, u.id as user_id, u.username, u.email, u.role, u.is_active + FROM sessions s + JOIN users u ON s.user_id = u.id + WHERE s.session_id = ? AND s.expires_at > datetime('now')`, + [sessionId], + (err, row) => { + if (err) reject(err); + else resolve(row); + } + ); + }); + + if (!session) { + res.clearCookie('session_id'); + return res.status(401).json({ error: 'Session expired' }); + } + + if (!session.is_active) { + res.clearCookie('session_id'); + return res.status(401).json({ error: 'Account is disabled' }); + } + + res.json({ + user: { + id: session.user_id, + username: session.username, + email: session.email, + role: session.role + } + }); + } catch (err) { + console.error('Get user error:', err); + res.status(500).json({ error: 'Failed to get user' }); + } + }); + + // Clean up expired sessions (can be called periodically) + router.post('/cleanup-sessions', async (req, res) => { + try { + await new Promise((resolve, reject) => { + db.run( + "DELETE FROM sessions WHERE expires_at < datetime('now')", + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + res.json({ message: 'Expired sessions cleaned up' }); + } catch (err) { + console.error('Session cleanup error:', err); + res.status(500).json({ error: 'Cleanup failed' }); + } + }); + + return router; +} + +module.exports = createAuthRouter; diff --git a/backend/routes/users.js b/backend/routes/users.js new file mode 100644 index 0000000..db2bb6f --- /dev/null +++ b/backend/routes/users.js @@ -0,0 +1,217 @@ +// User Management Routes (Admin only) +const express = require('express'); +const bcrypt = require('bcryptjs'); + +function createUsersRouter(db, requireAuth, requireRole) { + const router = express.Router(); + + // All routes require admin role + router.use(requireAuth(db), requireRole('admin')); + + // Get all users + router.get('/', async (req, res) => { + try { + const users = await new Promise((resolve, reject) => { + db.all( + `SELECT id, username, email, role, is_active, created_at, last_login + FROM users ORDER BY created_at DESC`, + (err, rows) => { + if (err) reject(err); + else resolve(rows); + } + ); + }); + res.json(users); + } catch (err) { + console.error('Get users error:', err); + res.status(500).json({ error: 'Failed to fetch users' }); + } + }); + + // Get single user + router.get('/:id', async (req, res) => { + try { + const user = await new Promise((resolve, reject) => { + db.get( + `SELECT id, username, email, role, is_active, created_at, last_login + FROM users WHERE id = ?`, + [req.params.id], + (err, row) => { + if (err) reject(err); + else resolve(row); + } + ); + }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + res.json(user); + } catch (err) { + console.error('Get user error:', err); + res.status(500).json({ error: 'Failed to fetch user' }); + } + }); + + // Create new user + router.post('/', async (req, res) => { + const { username, email, password, role } = req.body; + + if (!username || !email || !password) { + return res.status(400).json({ error: 'Username, email, and password are required' }); + } + + if (role && !['admin', 'editor', 'viewer'].includes(role)) { + return res.status(400).json({ error: 'Invalid role. Must be admin, editor, or viewer' }); + } + + try { + const passwordHash = await bcrypt.hash(password, 10); + + const result = await new Promise((resolve, reject) => { + db.run( + `INSERT INTO users (username, email, password_hash, role) + VALUES (?, ?, ?, ?)`, + [username, email, passwordHash, role || 'viewer'], + function(err) { + if (err) reject(err); + else resolve({ id: this.lastID }); + } + ); + }); + + res.status(201).json({ + message: 'User created successfully', + user: { + id: result.id, + username, + email, + role: role || 'viewer' + } + }); + } catch (err) { + console.error('Create user error:', err); + if (err.message.includes('UNIQUE constraint failed')) { + return res.status(409).json({ error: 'Username or email already exists' }); + } + res.status(500).json({ error: 'Failed to create user' }); + } + }); + + // Update user + router.patch('/:id', async (req, res) => { + const { username, email, password, role, is_active } = req.body; + const userId = req.params.id; + + // Prevent self-demotion from admin + if (userId == req.user.id && role && role !== 'admin') { + return res.status(400).json({ error: 'Cannot remove your own admin role' }); + } + + // Prevent self-deactivation + if (userId == req.user.id && is_active === false) { + return res.status(400).json({ error: 'Cannot deactivate your own account' }); + } + + try { + const updates = []; + const values = []; + + if (username) { + updates.push('username = ?'); + values.push(username); + } + if (email) { + updates.push('email = ?'); + values.push(email); + } + if (password) { + const passwordHash = await bcrypt.hash(password, 10); + updates.push('password_hash = ?'); + values.push(passwordHash); + } + if (role) { + if (!['admin', 'editor', 'viewer'].includes(role)) { + return res.status(400).json({ error: 'Invalid role' }); + } + updates.push('role = ?'); + values.push(role); + } + if (typeof is_active === 'boolean') { + updates.push('is_active = ?'); + values.push(is_active ? 1 : 0); + } + + if (updates.length === 0) { + return res.status(400).json({ error: 'No fields to update' }); + } + + values.push(userId); + + await new Promise((resolve, reject) => { + db.run( + `UPDATE users SET ${updates.join(', ')} WHERE id = ?`, + values, + function(err) { + if (err) reject(err); + else resolve({ changes: this.changes }); + } + ); + }); + + // If user was deactivated, delete their sessions + if (is_active === false) { + await new Promise((resolve) => { + db.run('DELETE FROM sessions WHERE user_id = ?', [userId], () => resolve()); + }); + } + + res.json({ message: 'User updated successfully' }); + } catch (err) { + console.error('Update user error:', err); + if (err.message.includes('UNIQUE constraint failed')) { + return res.status(409).json({ error: 'Username or email already exists' }); + } + res.status(500).json({ error: 'Failed to update user' }); + } + }); + + // Delete user + router.delete('/:id', async (req, res) => { + const userId = req.params.id; + + // Prevent self-deletion + if (userId == req.user.id) { + return res.status(400).json({ error: 'Cannot delete your own account' }); + } + + try { + // Delete sessions first (foreign key) + await new Promise((resolve) => { + db.run('DELETE FROM sessions WHERE user_id = ?', [userId], () => resolve()); + }); + + // Delete user + const result = await new Promise((resolve, reject) => { + db.run('DELETE FROM users WHERE id = ?', [userId], function(err) { + if (err) reject(err); + else resolve({ changes: this.changes }); + }); + }); + + if (result.changes === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + res.json({ message: 'User deleted successfully' }); + } catch (err) { + console.error('Delete user error:', err); + res.status(500).json({ error: 'Failed to delete user' }); + } + }); + + return router; +} + +module.exports = createUsersRouter; diff --git a/backend/server.js b/backend/server.js index d2a4b00..5d27b6b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,5 +1,5 @@ // CVE Management Backend API -// Install: npm install express sqlite3 multer cors dotenv +// Install: npm install express sqlite3 multer cors dotenv bcryptjs cookie-parser require('dotenv').config(); @@ -7,12 +7,19 @@ const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const multer = require('multer'); const cors = require('cors'); +const cookieParser = require('cookie-parser'); const path = require('path'); const fs = require('fs'); +// Auth imports +const { requireAuth, requireRole } = require('./middleware/auth'); +const createAuthRouter = require('./routes/auth'); +const createUsersRouter = require('./routes/users'); + const app = express(); const PORT = process.env.PORT || 3001; const API_HOST = process.env.API_HOST || 'localhost'; +const SESSION_SECRET = process.env.SESSION_SECRET || 'default-secret-change-me'; const CORS_ORIGINS = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',') : ['http://localhost:3000']; @@ -29,6 +36,7 @@ app.use(cors({ credentials: true })); app.use(express.json()); +app.use(cookieParser()); app.use('/uploads', express.static('uploads')); // Database connection @@ -37,6 +45,12 @@ const db = new sqlite3.Database('./cve_database.db', (err) => { else console.log('Connected to CVE database'); }); +// Auth routes (public) +app.use('/api/auth', createAuthRouter(db)); + +// User management routes (admin only) +app.use('/api/users', createUsersRouter(db, requireAuth, requireRole)); + // Simple storage - upload to temp directory first const storage = multer.diskStorage({ destination: (req, file, cb) => { @@ -59,8 +73,8 @@ const upload = multer({ // ========== CVE ENDPOINTS ========== -// Get all CVEs with optional filters -app.get('/api/cves', (req, res) => { +// Get all CVEs with optional filters (authenticated users) +app.get('/api/cves', requireAuth(db), (req, res) => { const { search, vendor, severity, status } = req.query; let query = ` @@ -106,8 +120,8 @@ app.get('/api/cves', (req, res) => { }); }); -// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR -app.get('/api/cves/check/:cveId', (req, res) => { +// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users) +app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { const { cveId } = req.params; const query = ` @@ -153,8 +167,8 @@ app.get('/api/cves/check/:cveId', (req, res) => { }); }); -// NEW ENDPOINT: Get all vendors for a specific CVE -app.get('/api/cves/:cveId/vendors', (req, res) => { +// NEW ENDPOINT: Get all vendors for a specific CVE (authenticated users) +app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => { const { cveId } = req.params; const query = ` @@ -173,8 +187,8 @@ app.get('/api/cves/:cveId/vendors', (req, res) => { }); -// Create new CVE entry - ALLOW MULTIPLE VENDORS -app.post('/api/cves', (req, res) => { +// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin) +app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { console.log('=== ADD CVE REQUEST ==='); console.log('Body:', req.body); console.log('======================='); @@ -210,8 +224,8 @@ app.post('/api/cves', (req, res) => { }); -// Update CVE status -app.patch('/api/cves/:cveId/status', (req, res) => { +// Update CVE status (editor or admin) +app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { cveId } = req.params; const { status } = req.body; @@ -228,8 +242,8 @@ app.patch('/api/cves/:cveId/status', (req, res) => { // ========== DOCUMENT ENDPOINTS ========== -// Get documents for a CVE - FILTER BY VENDOR -app.get('/api/cves/:cveId/documents', (req, res) => { +// Get documents for a CVE - FILTER BY VENDOR (authenticated users) +app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => { const { cveId } = req.params; const { vendor } = req.query; // NEW: Optional vendor filter @@ -251,8 +265,8 @@ app.get('/api/cves/:cveId/documents', (req, res) => { }); }); -// Upload document - ADD ERROR HANDLING FOR MULTER -app.post('/api/cves/:cveId/documents', (req, res, next) => { +// Upload document - ADD ERROR HANDLING FOR MULTER (editor or admin) +app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { console.error('MULTER ERROR:', err); @@ -327,8 +341,8 @@ app.post('/api/cves/:cveId/documents', (req, res, next) => { }); }); }); -// Delete document -app.delete('/api/documents/:id', (req, res) => { +// Delete document (admin only) +app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => { const { id } = req.params; // First get the file path to delete the actual file @@ -352,8 +366,8 @@ app.delete('/api/documents/:id', (req, res) => { // ========== UTILITY ENDPOINTS ========== -// Get all vendors -app.get('/api/vendors', (req, res) => { +// Get all vendors (authenticated users) +app.get('/api/vendors', requireAuth(db), (req, res) => { const query = `SELECT DISTINCT vendor FROM cves ORDER BY vendor`; db.all(query, [], (err, rows) => { @@ -364,8 +378,8 @@ app.get('/api/vendors', (req, res) => { }); }); -// Get statistics -app.get('/api/stats', (req, res) => { +// Get statistics (authenticated users) +app.get('/api/stats', requireAuth(db), (req, res) => { const query = ` SELECT COUNT(DISTINCT c.id) as total_cves, diff --git a/backend/setup.js b/backend/setup.js index 9e38cc8..504bbd7 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -2,6 +2,7 @@ // This creates a fresh database with multi-vendor support built-in const sqlite3 = require('sqlite3').verbose(); +const bcrypt = require('bcryptjs'); const fs = require('fs'); const path = require('path'); @@ -59,6 +60,34 @@ function initializeDatabase() { CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor); CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(type); + -- Users table for authentication + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'viewer', + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP, + CHECK (role IN ('admin', 'editor', 'viewer')) + ); + + -- Sessions table for session management + CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id VARCHAR(255) UNIQUE NOT NULL, + user_id INTEGER NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id); + CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); + CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); + INSERT OR IGNORE INTO required_documents (vendor, document_type, is_mandatory, description) VALUES ('Microsoft', 'advisory', 1, 'Official Microsoft Security Advisory'), ('Microsoft', 'screenshot', 0, 'Proof of patch application'), @@ -109,6 +138,42 @@ function createUploadsDirectory() { } } +// Create default admin user +async function createDefaultAdmin(db) { + return new Promise((resolve, reject) => { + // Check if admin already exists + db.get('SELECT id FROM users WHERE username = ?', ['admin'], async (err, row) => { + if (err) { + reject(err); + return; + } + + if (row) { + console.log('āœ“ Default admin user already exists'); + resolve(); + return; + } + + // Create admin user with password 'admin123' + const passwordHash = await bcrypt.hash('admin123', 10); + + db.run( + `INSERT INTO users (username, email, password_hash, role, is_active) + VALUES (?, ?, ?, ?, ?)`, + ['admin', 'admin@localhost', passwordHash, 'admin', 1], + (err) => { + if (err) { + reject(err); + } else { + console.log('āœ“ Created default admin user (admin/admin123)'); + resolve(); + } + } + ); + }); + }); +} + // Add sample CVE data (optional - for testing) async function addSampleData(db) { console.log('\nšŸ“ Adding sample CVE data for testing...'); @@ -179,12 +244,14 @@ function displaySummary() { console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•'); console.log('\nšŸ“Š What was created:'); console.log(' āœ“ SQLite database (cve_database.db)'); - console.log(' āœ“ Tables: cves, documents, required_documents'); + console.log(' āœ“ Tables: cves, documents, required_documents, users, sessions'); console.log(' āœ“ Multi-vendor support with UNIQUE(cve_id, vendor)'); console.log(' āœ“ Vendor column in documents table'); + console.log(' āœ“ User authentication with session-based auth'); console.log(' āœ“ Indexes for fast queries'); console.log(' āœ“ Document compliance view'); console.log(' āœ“ Uploads directory for file storage'); + console.log(' āœ“ Default admin user (admin/admin123)'); console.log('\nšŸ“ File structure will be:'); console.log(' uploads/'); console.log(' └── CVE-XXXX-XXXX/'); @@ -219,7 +286,10 @@ async function main() { // Initialize database const db = await initializeDatabase(); - + + // Create default admin user + await createDefaultAdmin(db); + // Add sample data await addSampleData(db); diff --git a/frontend/src/App.js b/frontend/src/App.js index d909e2a..43c4f91 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,9 @@ import React, { useState, useEffect } from 'react'; import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus } from 'lucide-react'; +import { useAuth } from './contexts/AuthContext'; +import LoginForm from './components/LoginForm'; +import UserMenu from './components/UserMenu'; +import UserManagement from './components/UserManagement'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001'; @@ -7,6 +11,7 @@ const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001'; const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low']; export default function App() { + const { isAuthenticated, loading: authLoading, canWrite, isAdmin } = useAuth(); const [searchQuery, setSearchQuery] = useState(''); const [selectedVendor, setSelectedVendor] = useState('All Vendors'); const [selectedSeverity, setSelectedSeverity] = useState('All Severities'); @@ -21,6 +26,7 @@ export default function App() { const [quickCheckCVE, setQuickCheckCVE] = useState(''); const [quickCheckResult, setQuickCheckResult] = useState(null); const [showAddCVE, setShowAddCVE] = useState(false); + const [showUserManagement, setShowUserManagement] = useState(false); const [newCVE, setNewCVE] = useState({ cve_id: '', vendor: '', @@ -30,19 +36,6 @@ export default function App() { }); const [uploadingFile, setUploadingFile] = useState(false); - // Fetch CVEs from API - useEffect(() => { - fetchCVEs(); - fetchVendors(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Refetch when filters change - useEffect(() => { - fetchCVEs(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, selectedVendor, selectedSeverity]); - const fetchCVEs = async () => { setLoading(true); setError(null); @@ -52,7 +45,9 @@ export default function App() { if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor); if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity); - const response = await fetch(`${API_BASE}/cves?${params}`); + const response = await fetch(`${API_BASE}/cves?${params}`, { + credentials: 'include' + }); if (!response.ok) throw new Error('Failed to fetch CVEs'); const data = await response.json(); setCves(data); @@ -66,7 +61,9 @@ export default function App() { const fetchVendors = async () => { try { - const response = await fetch(`${API_BASE}/vendors`); + const response = await fetch(`${API_BASE}/vendors`, { + credentials: 'include' + }); if (!response.ok) throw new Error('Failed to fetch vendors'); const data = await response.json(); setVendors(['All Vendors', ...data]); @@ -78,9 +75,11 @@ export default function App() { const fetchDocuments = async (cveId, vendor) => { const key = `${cveId}-${vendor}`; if (cveDocuments[key]) return; - + try { - const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`); + const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`, { + credentials: 'include' + }); if (!response.ok) throw new Error('Failed to fetch documents'); const data = await response.json(); setCveDocuments(prev => ({ ...prev, [key]: data })); @@ -91,9 +90,11 @@ export default function App() { const quickCheckCVEStatus = async () => { if (!quickCheckCVE.trim()) return; - + try { - const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`); + const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`, { + credentials: 'include' + }); if (!response.ok) throw new Error('Failed to check CVE'); const data = await response.json(); setQuickCheckResult(data); @@ -104,7 +105,6 @@ export default function App() { }; const handleViewDocuments = async (cveId, vendor) => { - const key = `${cveId}-${vendor}`; if (selectedCVE === cveId && selectedVendorView === vendor) { setSelectedCVE(null); setSelectedVendorView(null); @@ -143,6 +143,7 @@ export default function App() { const response = await fetch(`${API_BASE}/cves`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(newCVE) }); @@ -195,6 +196,7 @@ export default function App() { try { const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, { method: 'POST', + credentials: 'include', body: formData }); @@ -219,14 +221,15 @@ export default function App() { if (!window.confirm('Are you sure you want to delete this document?')) { return; } - + try { const response = await fetch(`${API_BASE}/documents/${docId}`, { - method: 'DELETE' + method: 'DELETE', + credentials: 'include' }); - + if (!response.ok) throw new Error('Failed to delete document'); - + alert('Document deleted successfully!'); const key = `${cveId}-${vendor}`; delete cveDocuments[key]; @@ -237,6 +240,40 @@ export default function App() { } }; + // Fetch CVEs from API when authenticated + useEffect(() => { + if (isAuthenticated) { + fetchCVEs(); + fetchVendors(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthenticated]); + + // Refetch when filters change + useEffect(() => { + if (isAuthenticated) { + fetchCVEs(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery, selectedVendor, selectedSeverity]); + + // Show loading while checking auth + if (authLoading) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + // Show login if not authenticated + if (!isAuthenticated) { + return ; + } + // Group CVEs by CVE ID const groupedCVEs = cves.reduce((acc, cve) => { if (!acc[cve.cve_id]) { @@ -257,15 +294,25 @@ export default function App() {

CVE Dashboard

Query vulnerabilities, manage vendors, and attach documentation

- +
+ {canWrite() && ( + + )} + setShowUserManagement(true)} /> +
+ {/* User Management Modal */} + {showUserManagement && ( + setShowUserManagement(false)} /> + )} + {/* Add CVE Modal */} {showAddCVE && (
@@ -634,6 +681,7 @@ export default function App() { > View + {isAdmin() && ( + )}
))} @@ -648,14 +697,16 @@ export default function App() { ) : (

No documents attached yet

)} - + {canWrite() && ( + + )} )} diff --git a/frontend/src/components/LoginForm.js b/frontend/src/components/LoginForm.js new file mode 100644 index 0000000..846e768 --- /dev/null +++ b/frontend/src/components/LoginForm.js @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { Loader, AlertCircle, Lock, User } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; + +export default function LoginForm() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setLoading(true); + + const result = await login(username, password); + + if (!result.success) { + setError(result.error); + } + + setLoading(false); + }; + + return ( +
+
+
+
+ +
+

CVE Dashboard

+

Sign in to access the dashboard

+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+
+ +
+ + setUsername(e.target.value)} + className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + placeholder="Enter your username" + disabled={loading} + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + placeholder="Enter your password" + disabled={loading} + /> +
+
+ + +
+ +
+

+ Default admin credentials: admin / admin123 +

+
+
+
+ ); +} diff --git a/frontend/src/components/UserManagement.js b/frontend/src/components/UserManagement.js new file mode 100644 index 0000000..7a3784d --- /dev/null +++ b/frontend/src/components/UserManagement.js @@ -0,0 +1,380 @@ +import React, { useState, useEffect } from 'react'; +import { X, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, User, Mail, Shield } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; + +const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; + +export default function UserManagement({ onClose }) { + const { user: currentUser } = useAuth(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showAddUser, setShowAddUser] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [formData, setFormData] = useState({ + username: '', + email: '', + password: '', + role: 'viewer' + }); + const [formError, setFormError] = useState(''); + const [formSuccess, setFormSuccess] = useState(''); + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + try { + const response = await fetch(`${API_BASE}/users`, { + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to fetch users'); + const data = await response.json(); + setUsers(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setFormError(''); + setFormSuccess(''); + + try { + const url = editingUser + ? `${API_BASE}/users/${editingUser.id}` + : `${API_BASE}/users`; + + const method = editingUser ? 'PATCH' : 'POST'; + + const body = { ...formData }; + if (editingUser && !body.password) { + delete body.password; + } + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(body) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Operation failed'); + } + + setFormSuccess(editingUser ? 'User updated successfully' : 'User created successfully'); + fetchUsers(); + + setTimeout(() => { + setShowAddUser(false); + setEditingUser(null); + setFormData({ username: '', email: '', password: '', role: 'viewer' }); + setFormSuccess(''); + }, 1500); + } catch (err) { + setFormError(err.message); + } + }; + + const handleEdit = (user) => { + setEditingUser(user); + setFormData({ + username: user.username, + email: user.email, + password: '', + role: user.role + }); + setShowAddUser(true); + setFormError(''); + setFormSuccess(''); + }; + + const handleDelete = async (userId) => { + if (!window.confirm('Are you sure you want to delete this user?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/users/${userId}`, { + method: 'DELETE', + credentials: 'include' + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Delete failed'); + } + + fetchUsers(); + } catch (err) { + alert(err.message); + } + }; + + const handleToggleActive = async (user) => { + try { + const response = await fetch(`${API_BASE}/users/${user.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ is_active: !user.is_active }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Update failed'); + } + + fetchUsers(); + } catch (err) { + alert(err.message); + } + }; + + const getRoleBadgeColor = (role) => { + switch (role) { + case 'admin': + return 'bg-red-100 text-red-800'; + case 'editor': + return 'bg-blue-100 text-blue-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + return ( +
+
+
+
+

User Management

+

Manage user accounts and permissions

+
+ +
+ +
+ {!showAddUser && ( + + )} + + {showAddUser && ( +
+

+ {editingUser ? 'Edit User' : 'Add New User'} +

+ + {formError && ( +
+ + {formError} +
+ )} + + {formSuccess && ( +
+ + {formSuccess} +
+ )} + +
+
+
+ +
+ + setFormData({ ...formData, username: e.target.value })} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + /> +
+
+ +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + /> +
+
+ +
+ + setFormData({ ...formData, password: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + /> +
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ )} + + {loading ? ( +
+ +

Loading users...

+
+ ) : error ? ( +
+ +

{error}

+
+ ) : ( +
+ + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
UserRoleStatusLast LoginActions
+
+

{user.username}

+

{user.email}

+
+
+ + {user.role.charAt(0).toUpperCase() + user.role.slice(1)} + + + + + {user.last_login + ? new Date(user.last_login).toLocaleString() + : 'Never'} + +
+ + +
+
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/UserMenu.js b/frontend/src/components/UserMenu.js new file mode 100644 index 0000000..aa9d365 --- /dev/null +++ b/frontend/src/components/UserMenu.js @@ -0,0 +1,94 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { User, LogOut, ChevronDown, Shield } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; + +export default function UserMenu({ onManageUsers }) { + const { user, logout, isAdmin } = useAuth(); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + // Close menu when clicking outside + useEffect(() => { + function handleClickOutside(event) { + if (menuRef.current && !menuRef.current.contains(event.target)) { + setIsOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const getRoleBadgeColor = (role) => { + switch (role) { + case 'admin': + return 'bg-red-100 text-red-800'; + case 'editor': + return 'bg-blue-100 text-blue-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const handleLogout = async () => { + setIsOpen(false); + await logout(); + }; + + const handleManageUsers = () => { + setIsOpen(false); + if (onManageUsers) { + onManageUsers(); + } + }; + + if (!user) return null; + + return ( +
+ + + {isOpen && ( +
+
+

{user.username}

+

{user.email}

+ + {user.role.charAt(0).toUpperCase() + user.role.slice(1)} + +
+ + {isAdmin() && ( + + )} + + +
+ )} +
+ ); +} diff --git a/frontend/src/contexts/AuthContext.js b/frontend/src/contexts/AuthContext.js new file mode 100644 index 0000000..8947ef5 --- /dev/null +++ b/frontend/src/contexts/AuthContext.js @@ -0,0 +1,112 @@ +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; + +const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Check if user is authenticated on mount + const checkAuth = useCallback(async () => { + try { + const response = await fetch(`${API_BASE}/auth/me`, { + credentials: 'include' + }); + + if (response.ok) { + const data = await response.json(); + setUser(data.user); + } else { + setUser(null); + } + } catch (err) { + console.error('Auth check error:', err); + setUser(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + // Login function + const login = async (username, password) => { + setError(null); + try { + const response = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Login failed'); + } + + setUser(data.user); + return { success: true }; + } catch (err) { + setError(err.message); + return { success: false, error: err.message }; + } + }; + + // Logout function + const logout = async () => { + try { + await fetch(`${API_BASE}/auth/logout`, { + method: 'POST', + credentials: 'include' + }); + } catch (err) { + console.error('Logout error:', err); + } + setUser(null); + }; + + // Check if user has a specific role + const hasRole = (...roles) => { + return user && roles.includes(user.role); + }; + + // Check if user can perform write operations (editor or admin) + const canWrite = () => hasRole('editor', 'admin'); + + // Check if user is admin + const isAdmin = () => hasRole('admin'); + + const value = { + user, + loading, + error, + login, + logout, + checkAuth, + hasRole, + canWrite, + isAdmin, + isAuthenticated: !!user + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/frontend/src/index.js b/frontend/src/index.js index d563c0f..1a300f4 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,11 +3,14 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { AuthProvider } from './contexts/AuthContext'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + ); diff --git a/package.json b/package.json index 71947f0..392db41 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "author": "", "license": "ISC", "dependencies": { + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^16.6.1", "express": "^5.2.1",