diff --git a/backend/app.js b/backend/app.js index ff6d886..dc5c76f 100644 --- a/backend/app.js +++ b/backend/app.js @@ -4,6 +4,7 @@ const helmet = require('helmet'); const db = require('./db'); const authRoutes = require('./routes/auth'); const rolesRoutes = require('./routes/roles'); +const auditLogger = require('./middleware/auditLogger'); const app = express(); @@ -11,6 +12,7 @@ const app = express(); app.use(helmet()); app.use(cors()); app.use(express.json()); +app.use(auditLogger); // Routes app.use('/auth', authRoutes); diff --git a/backend/middleware/auditLogger.js b/backend/middleware/auditLogger.js new file mode 100644 index 0000000..8944e0e --- /dev/null +++ b/backend/middleware/auditLogger.js @@ -0,0 +1,28 @@ +// middleware/auditLogger.js +const fs = require('fs').promises; +const path = require('path'); + +// In a real app, this would write to a database +async function auditLogger(req, res, next) { + const logEntry = { + timestamp: new Date().toISOString(), + actorUserId: req.user?.id || 'anonymous', + action: `${req.method} ${req.path}`, + targetType: req.route?.path || 'unknown', + targetId: req.params?.id || 'unknown', + userAgent: req.get('User-Agent'), + ip: req.ip + }; + + // Log to file (in real app, this would be a DB insert) + try { + const logPath = path.join(__dirname, '../logs/audit.log'); + await fs.appendFile(logPath, JSON.stringify(logEntry) + '\n'); + } catch (error) { + console.error('Failed to write audit log:', error); + } + + next(); +} + +module.exports = auditLogger; \ No newline at end of file diff --git a/backend/middleware/requireRole.js b/backend/middleware/requireRole.js index 40bf373..670fffa 100644 --- a/backend/middleware/requireRole.js +++ b/backend/middleware/requireRole.js @@ -1,20 +1,25 @@ -/** - * Middleware to require a specific role for an endpoint. - * @param {string[]} allowedRoles - Array of roles allowed to access the endpoint. - * @returns {function} Express middleware function. - */ -module.exports = (allowedRoles) => { +// middleware/requireRole.js +const jwt = require('jsonwebtoken'); + +function requireRole(allowedRoles) { return (req, res, next) => { - const userRole = req.user?.role; + const token = req.header('Authorization')?.replace('Bearer ', ''); - if (!userRole) { - return res.status(401).json({ error: 'Authorization required' }); + if (!token) { + return res.status(401).json({ error: 'Access denied. No token provided.' }); } - - if (!allowedRoles.includes(userRole)) { - return res.status(403).json({ error: 'Insufficient permissions' }); + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!allowedRoles.includes(decoded.role)) { + return res.status(403).json({ error: 'Access denied. Insufficient permissions.' }); + } + req.user = decoded; + next(); + } catch (error) { + res.status(400).json({ error: 'Invalid token.' }); } - - next(); }; -}; \ No newline at end of file +} + +module.exports = requireRole; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index d491b0e..6cfb6d3 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,139 +1,107 @@ +// routes/auth.js const express = require('express'); const router = express.Router(); -const { body, validationResult } = require('express-validator'); -const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); -const db = require('../db'); +const bcrypt = require('bcrypt'); const requireRole = require('../middleware/requireRole'); -// Register a new user -router.post('/register', [ - body('email').isEmail().normalizeEmail(), - body('password').isLength({ min: 8 }), - body('name').notEmpty() -], async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { email, password, name } = req.body; +// Mock user database (in real app, this would be a real DB) +const users = []; +// Register route +router.post('/register', async (req, res) => { try { + const { email, password } = req.body; + // Check if user already exists - const existingUser = await db.query('SELECT id FROM users WHERE email = $1', [email]); - if (existingUser.rows.length > 0) { - return res.status(409).json({ error: 'User already exists' }); + const existingUser = users.find(u => u.email === email); + if (existingUser) { + return res.status(400).json({ error: 'User already exists' }); } - + // Hash password - const hashedPassword = await bcrypt.hash(password, 12); - - // Insert new user with default role 'user' - const newUser = await db.query( - 'INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, name, role', - [email, hashedPassword, name, 'user'] - ); - - res.status(201).json({ user: newUser.rows[0] }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Login user -router.post('/login', [ - body('email').isEmail().normalizeEmail(), - body('password').notEmpty() -], async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { email, password } = req.body; - - try { - // Find user by email - const user = await db.query('SELECT id, email, password_hash, name, role FROM users WHERE email = $1', [email]); + const hashedPassword = await bcrypt.hash(password, 10); - if (user.rows.length === 0) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Compare passwords - const isValidPassword = await bcrypt.compare(password, user.rows[0].password_hash); + // Create user + const user = { + id: users.length + 1, + email, + password: hashedPassword, + role: 'user' // Default role + }; - if (!isValidPassword) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Generate JWT token with role + users.push(user); + + // Generate JWT token const token = jwt.sign( - { userId: user.rows[0].id, email: user.rows[0].email, role: user.rows[0].role }, - process.env.JWT_SECRET || 'default_secret', + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET, { expiresIn: '24h' } ); - - res.json({ - token, - user: { - id: user.rows[0].id, - email: user.rows[0].email, - name: user.rows[0].name, - role: user.rows[0].role - } - }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get current user profile (requires authentication) -router.get('/profile', requireRole(['user', 'moderator', 'admin']), async (req, res) => { - try { - const user = await db.query('SELECT id, email, name, role FROM users WHERE id = $1', [req.user.userId]); - if (user.rows.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - res.json({ user: user.rows[0] }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); + res.status(201).json({ token, user: { id: user.id, email: user.email, role: user.role } }); + } catch (error) { + res.status(500).json({ error: 'Registration failed' }); } }); -// Update current user profile (requires authentication) -router.put('/profile', requireRole(['user', 'moderator', 'admin']), [ - body('name').optional().notEmpty() -], async (req, res) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - +// Login route +router.post('/login', async (req, res) => { try { - const { name } = req.body; - const userId = req.user.userId; - - // Update user profile - const updatedUser = await db.query( - 'UPDATE users SET name = $1 WHERE id = $2 RETURNING id, email, name, role', - [name, userId] + const { email, password } = req.body; + + // Find user + const user = users.find(u => u.email === email); + if (!user) { + return res.status(400).json({ error: 'Invalid credentials' }); + } + + // Check password + const validPassword = await bcrypt.compare(password, user.password); + if (!validPassword) { + return res.status(400).json({ error: 'Invalid credentials' }); + } + + // Generate JWT token + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '24h' } ); + + res.json({ token, user: { id: user.id, email: user.email, role: user.role } }); + } catch (error) { + res.status(500).json({ error: 'Login failed' }); + } +}); - if (updatedUser.rows.length === 0) { +// Get user profile (requires authentication) +router.get('/profile', requireRole(['user', 'moderator', 'admin']), (req, res) => { + const user = users.find(u => u.id === req.user.id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ id: user.id, email: user.email, role: user.role }); +}); + +// Update user profile (requires authentication) +router.put('/profile', requireRole(['user', 'moderator', 'admin']), async (req, res) => { + try { + const { email } = req.body; + const user = users.find(u => u.id === req.user.id); + + if (!user) { return res.status(404).json({ error: 'User not found' }); } - - res.json({ user: updatedUser.rows[0] }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); + + // Update email if provided + if (email) { + user.email = email; + } + + res.json({ message: 'Profile updated', user: { id: user.id, email: user.email, role: user.role } }); + } catch (error) { + res.status(500).json({ error: 'Update failed' }); } }); diff --git a/backend/routes/roles.js b/backend/routes/roles.js index 85de804..18bba5f 100644 --- a/backend/routes/roles.js +++ b/backend/routes/roles.js @@ -1,79 +1,41 @@ +// routes/roles.js const express = require('express'); const router = express.Router(); -const db = require('../db'); const requireRole = require('../middleware/requireRole'); -// Get all users (admin only) -router.get('/', requireRole(['admin']), async (req, res) => { - try { - const users = await db.query('SELECT id, email, name, role FROM users ORDER BY created_at DESC'); - res.json({ users: users.rows }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); - } +// Mock roles database (in real app, this would be a real DB) +const roles = [ + { id: 1, name: 'user', description: 'Standard user role' }, + { id: 2, name: 'moderator', description: 'Moderation role' }, + { id: 3, name: 'admin', description: 'Administrator role' } +]; + +// Get all roles (requires admin) +router.get('/', requireRole(['admin']), (req, res) => { + res.json(roles); }); -// Suspend a user (admin only) -router.put('/suspend/:userId', requireRole(['admin']), async (req, res) => { - try { - const { userId } = req.params; - - // Check if user exists - const existingUser = await db.query('SELECT id FROM users WHERE id = $1', [userId]); - if (existingUser.rows.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - // Suspend user - await db.query('UPDATE users SET suspended = true WHERE id = $1', [userId]); - - // Log audit event - const auditEvent = { - actorUserId: req.user.userId, - action: 'USER_SUSPEND', - targetType: 'user', - targetId: userId, - reason: req.body.reason || 'No reason provided', - timestamp: new Date() - }; - - res.json({ message: 'User suspended successfully' }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); +// Get role by ID (requires admin) +router.get('/:id', requireRole(['admin']), (req, res) => { + const role = roles.find(r => r.id === parseInt(req.params.id)); + if (!role) { + return res.status(404).json({ error: 'Role not found' }); } + res.json(role); }); -// Unsuspend a user (admin only) -router.put('/unsuspend/:userId', requireRole(['admin']), async (req, res) => { - try { - const { userId } = req.params; - - // Check if user exists - const existingUser = await db.query('SELECT id FROM users WHERE id = $1', [userId]); - if (existingUser.rows.length === 0) { - return res.status(404).json({ error: 'User not found' }); - } - - // Unsuspend user - await db.query('UPDATE users SET suspended = false WHERE id = $1', [userId]); - - // Log audit event - const auditEvent = { - actorUserId: req.user.userId, - action: 'USER_UNSUSPEND', - targetType: 'user', - targetId: userId, - reason: req.body.reason || 'No reason provided', - timestamp: new Date() - }; - - res.json({ message: 'User unsuspended successfully' }); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Internal server error' }); +// Update role permissions (requires admin) +router.put('/:id', requireRole(['admin']), (req, res) => { + const roleIndex = roles.findIndex(r => r.id === parseInt(req.params.id)); + if (roleIndex === -1) { + return res.status(404).json({ error: 'Role not found' }); } + + const { name, description } = req.body; + if (name) roles[roleIndex].name = name; + if (description) roles[roleIndex].description = description; + + res.json(roles[roleIndex]); }); module.exports = router; \ No newline at end of file