const express = require('express'); const router = express.Router(); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { requirePermission, getUserPermissions } = require('./permissions'); // Get pool from global or create a new one if not available let pool; if (typeof global.pool !== 'undefined') { pool = global.pool; } else { // If global pool is not available, create a new connection const { Pool } = require('pg'); pool = new Pool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, port: process.env.DB_PORT, }); console.log('Created new database pool in routes.js'); } // Authentication middleware const authenticate = async (req, res, next) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Authentication required' }); } const token = authHeader.split(' ')[1]; const decoded = jwt.verify(token, process.env.JWT_SECRET); // Get user from database const result = await pool.query( 'SELECT id, username, is_admin FROM users WHERE id = $1', [decoded.userId] ); if (result.rows.length === 0) { return res.status(401).json({ error: 'User not found' }); } // Attach user to request req.user = result.rows[0]; next(); } catch (error) { console.error('Authentication error:', error); res.status(401).json({ error: 'Invalid token' }); } }; // Login route router.post('/login', async (req, res) => { try { const { username, password } = req.body; // Get user from database const result = await pool.query( 'SELECT id, username, password, is_admin, is_active FROM users WHERE username = $1', [username] ); if (result.rows.length === 0) { return res.status(401).json({ error: 'Invalid username or password' }); } const user = result.rows[0]; // Check if user is active if (!user.is_active) { return res.status(403).json({ error: 'Account is inactive' }); } // Verify password const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid username or password' }); } // Update last login await pool.query( 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [user.id] ); // Generate JWT const token = jwt.sign( { userId: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: '8h' } ); // Get user permissions const permissions = await getUserPermissions(user.id); res.json({ token, user: { id: user.id, username: user.username, is_admin: user.is_admin, permissions } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Server error' }); } }); // Get current user router.get('/me', authenticate, async (req, res) => { try { // Get user permissions const permissions = await getUserPermissions(req.user.id); res.json({ id: req.user.id, username: req.user.username, is_admin: req.user.is_admin, permissions }); } catch (error) { console.error('Error getting current user:', error); res.status(500).json({ error: 'Server error' }); } }); // Get all users router.get('/users', authenticate, requirePermission('view:users'), async (req, res) => { try { const result = await pool.query(` SELECT id, username, email, is_admin, is_active, created_at, last_login FROM users ORDER BY username `); res.json(result.rows); } catch (error) { console.error('Error getting users:', error); res.status(500).json({ error: 'Server error' }); } }); // Get user with permissions router.get('/users/:id', authenticate, requirePermission('view:users'), async (req, res) => { try { const userId = req.params.id; // Get user details const userResult = await pool.query(` SELECT id, username, email, is_admin, is_active, created_at, last_login FROM users WHERE id = $1 `, [userId]); if (userResult.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } // Get user permissions const permissionsResult = await pool.query(` SELECT p.id, p.name, p.code, p.category, p.description FROM permissions p JOIN user_permissions up ON p.id = up.permission_id WHERE up.user_id = $1 ORDER BY p.category, p.name `, [userId]); // Combine user and permissions const user = { ...userResult.rows[0], permissions: permissionsResult.rows }; res.json(user); } catch (error) { console.error('Error getting user:', error); res.status(500).json({ error: 'Server error' }); } }); // Create new user router.post('/users', authenticate, requirePermission('create:users'), async (req, res) => { const client = await pool.connect(); try { const { username, email, password, is_admin, is_active, permissions } = req.body; console.log("Create user request:", { username, email, is_admin, is_active, permissions: permissions || [] }); // Validate required fields if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } // Check if username is taken const existingUser = await client.query( 'SELECT id FROM users WHERE username = $1', [username] ); if (existingUser.rows.length > 0) { return res.status(400).json({ error: 'Username already exists' }); } // Start transaction await client.query('BEGIN'); // Hash password const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); // Insert new user const userResult = await client.query(` INSERT INTO users (username, email, password, is_admin, is_active, created_at) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) RETURNING id `, [username, email || null, hashedPassword, !!is_admin, is_active !== false]); const userId = userResult.rows[0].id; // Assign permissions if provided and not admin if (!is_admin && Array.isArray(permissions) && permissions.length > 0) { console.log("Adding permissions for new user:", userId); console.log("Permissions received:", permissions); // Check permission format const permissionIds = permissions.map(p => { if (typeof p === 'object' && p.id) { console.log("Permission is an object with ID:", p.id); return parseInt(p.id, 10); } else if (typeof p === 'number') { console.log("Permission is a number:", p); return p; } else if (typeof p === 'string' && !isNaN(parseInt(p, 10))) { console.log("Permission is a string that can be parsed as a number:", p); return parseInt(p, 10); } else { console.log("Unknown permission format:", typeof p, p); // If it's a permission code, we need to look up the ID return null; } }).filter(id => id !== null); console.log("Filtered permission IDs:", permissionIds); if (permissionIds.length > 0) { const permissionValues = permissionIds .map(permId => `(${userId}, ${permId})`) .join(','); console.log("Inserting permission values:", permissionValues); try { await client.query(` INSERT INTO user_permissions (user_id, permission_id) VALUES ${permissionValues} ON CONFLICT DO NOTHING `); console.log("Successfully inserted permissions for new user:", userId); } catch (err) { console.error("Error inserting permissions for new user:", err); throw err; } } else { console.log("No valid permission IDs found to insert for new user"); } } else { console.log("Not adding permissions: is_admin =", is_admin, "permissions array:", Array.isArray(permissions), "length:", permissions ? permissions.length : 0); } await client.query('COMMIT'); res.status(201).json({ id: userId, message: 'User created successfully' }); } catch (error) { await client.query('ROLLBACK'); console.error('Error creating user:', error); res.status(500).json({ error: 'Server error' }); } finally { client.release(); } }); // Update user router.put('/users/:id', authenticate, requirePermission('edit:users'), async (req, res) => { const client = await pool.connect(); try { const userId = req.params.id; const { username, email, password, is_admin, is_active, permissions } = req.body; console.log("Update user request:", { userId, username, email, is_admin, is_active, permissions: permissions || [] }); // Check if user exists const userExists = await client.query( 'SELECT id FROM users WHERE id = $1', [userId] ); if (userExists.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } // Start transaction await client.query('BEGIN'); // Build update fields const updateFields = []; const updateValues = [userId]; // First parameter is the user ID let paramIndex = 2; if (username !== undefined) { updateFields.push(`username = $${paramIndex++}`); updateValues.push(username); } if (email !== undefined) { updateFields.push(`email = $${paramIndex++}`); updateValues.push(email || null); } if (is_admin !== undefined) { updateFields.push(`is_admin = $${paramIndex++}`); updateValues.push(!!is_admin); } if (is_active !== undefined) { updateFields.push(`is_active = $${paramIndex++}`); updateValues.push(!!is_active); } // Update password if provided if (password) { const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); updateFields.push(`password = $${paramIndex++}`); updateValues.push(hashedPassword); } // Update user if there are fields to update if (updateFields.length > 0) { updateFields.push(`updated_at = CURRENT_TIMESTAMP`); await client.query(` UPDATE users SET ${updateFields.join(', ')} WHERE id = $1 `, updateValues); } // Update permissions if provided if (Array.isArray(permissions)) { console.log("Updating permissions for user:", userId); console.log("Permissions received:", permissions); // First remove existing permissions await client.query( 'DELETE FROM user_permissions WHERE user_id = $1', [userId] ); console.log("Deleted existing permissions for user:", userId); // Add new permissions if any and not admin const newIsAdmin = is_admin !== undefined ? is_admin : (await client.query('SELECT is_admin FROM users WHERE id = $1', [userId])).rows[0].is_admin; console.log("User is admin:", newIsAdmin); if (!newIsAdmin && permissions.length > 0) { console.log("Adding permissions:", permissions); // Check permission format const permissionIds = permissions.map(p => { if (typeof p === 'object' && p.id) { console.log("Permission is an object with ID:", p.id); return parseInt(p.id, 10); } else if (typeof p === 'number') { console.log("Permission is a number:", p); return p; } else if (typeof p === 'string' && !isNaN(parseInt(p, 10))) { console.log("Permission is a string that can be parsed as a number:", p); return parseInt(p, 10); } else { console.log("Unknown permission format:", typeof p, p); // If it's a permission code, we need to look up the ID return null; } }).filter(id => id !== null); console.log("Filtered permission IDs:", permissionIds); if (permissionIds.length > 0) { const permissionValues = permissionIds .map(permId => `(${userId}, ${permId})`) .join(','); console.log("Inserting permission values:", permissionValues); try { await client.query(` INSERT INTO user_permissions (user_id, permission_id) VALUES ${permissionValues} ON CONFLICT DO NOTHING `); console.log("Successfully inserted permissions for user:", userId); } catch (err) { console.error("Error inserting permissions:", err); throw err; } } else { console.log("No valid permission IDs found to insert"); } } } await client.query('COMMIT'); res.json({ message: 'User updated successfully' }); } catch (error) { await client.query('ROLLBACK'); console.error('Error updating user:', error); res.status(500).json({ error: 'Server error' }); } finally { client.release(); } }); // Delete user router.delete('/users/:id', authenticate, requirePermission('delete:users'), async (req, res) => { try { const userId = req.params.id; // Check that user is not deleting themselves if (req.user.id === parseInt(userId, 10)) { return res.status(400).json({ error: 'Cannot delete your own account' }); } // Delete user (this will cascade to user_permissions due to FK constraints) const result = await pool.query( 'DELETE FROM users WHERE id = $1 RETURNING id', [userId] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } res.json({ message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ error: 'Server error' }); } }); // Get all permissions grouped by category router.get('/permissions/categories', authenticate, requirePermission('view:users'), async (req, res) => { try { const result = await pool.query(` SELECT category, json_agg( json_build_object( 'id', id, 'name', name, 'code', code, 'description', description ) ORDER BY name ) as permissions FROM permissions GROUP BY category ORDER BY category `); res.json(result.rows); } catch (error) { console.error('Error getting permissions:', error); res.status(500).json({ error: 'Server error' }); } }); // Get all permissions router.get('/permissions', authenticate, requirePermission('view:users'), async (req, res) => { try { const result = await pool.query(` SELECT * FROM permissions ORDER BY category, name `); res.json(result.rows); } catch (error) { console.error('Error getting permissions:', error); res.status(500).json({ error: 'Server error' }); } }); module.exports = router;