import express from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { createPermissionHelpers } from './permissions.js'; export function createAuthRoutes({ pool }) { const router = express.Router(); const { requirePermission, getUserPermissions } = createPermissionHelpers({ pool }); // Local authenticate(): used by user-management endpoints that need req.user populated // with id/username/email/is_admin. NOT the per-service authenticate() — that lives in // shared/auth/middleware.js and is used by downstream services. Auth-server's surface is // small enough that a local copy is fine; the security boundary is the JWT verify step. async function authenticate(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); const result = await pool.query( 'SELECT id, username, email, is_admin, rocket_chat_user_id FROM users WHERE id = $1', [decoded.userId] ); if (result.rows.length === 0) { return res.status(401).json({ error: 'User not found' }); } req.user = result.rows[0]; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } } router.post('/login', async (req, res) => { try { const { username, password } = req.body; const result = await pool.query( 'SELECT id, username, password, is_admin, is_active, rocket_chat_user_id 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]; if (!user.is_active) { return res.status(403).json({ error: 'Account is inactive' }); } const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid username or password' }); } await pool.query( 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [user.id] ); const token = jwt.sign( { userId: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: '8h' } ); const permissions = await getUserPermissions(user.id); res.json({ token, user: { id: user.id, username: user.username, is_admin: user.is_admin, rocket_chat_user_id: user.rocket_chat_user_id, permissions, }, }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Server error' }); } }); router.get('/me', authenticate, async (req, res) => { try { const permissions = await getUserPermissions(req.user.id); res.json({ id: req.user.id, username: req.user.username, email: req.user.email, is_admin: req.user.is_admin, rocket_chat_user_id: req.user.rocket_chat_user_id, permissions, }); } catch (error) { console.error('Error getting current user:', error); res.status(500).json({ error: 'Server error' }); } }); router.get('/users', authenticate, requirePermission('view:users'), async (req, res) => { try { const result = await pool.query(` SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, 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' }); } }); router.get('/users/:id', authenticate, requirePermission('view:users'), async (req, res) => { try { const userId = req.params.id; const userResult = await pool.query(` SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, created_at, last_login FROM users WHERE id = $1 `, [userId]); if (userResult.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } 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]); res.json({ ...userResult.rows[0], permissions: permissionsResult.rows, }); } catch (error) { console.error('Error getting user:', error); res.status(500).json({ error: 'Server error' }); } }); router.post('/users', authenticate, requirePermission('create:users'), async (req, res) => { const client = await pool.connect(); try { const { username, email, password, is_admin, is_active, rocket_chat_user_id, permissions } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password are required' }); } 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' }); } await client.query('BEGIN'); const hashedPassword = await bcrypt.hash(password, 10); const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null; const userResult = await client.query(` INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING id `, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rcUserId]); const userId = userResult.rows[0].id; if (!is_admin && Array.isArray(permissions) && permissions.length > 0) { const permissionIds = normalizePermissionIds(permissions); if (permissionIds.length > 0) { await client.query( `INSERT INTO user_permissions (user_id, permission_id) SELECT $1, unnest($2::int[]) ON CONFLICT DO NOTHING`, [userId, permissionIds] ); } } 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(); } }); 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, rocket_chat_user_id, permissions } = req.body; 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' }); } await client.query('BEGIN'); const updateFields = []; const updateValues = [userId]; 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); } if (rocket_chat_user_id !== undefined) { updateFields.push(`rocket_chat_user_id = $${paramIndex++}`); updateValues.push(rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null); } if (password) { const hashedPassword = await bcrypt.hash(password, 10); updateFields.push(`password = $${paramIndex++}`); updateValues.push(hashedPassword); } if (updateFields.length > 0) { updateFields.push(`updated_at = CURRENT_TIMESTAMP`); await client.query(` UPDATE users SET ${updateFields.join(', ')} WHERE id = $1 `, updateValues); } if (Array.isArray(permissions)) { await client.query('DELETE FROM user_permissions WHERE user_id = $1', [userId]); const newIsAdmin = is_admin !== undefined ? is_admin : (await client.query('SELECT is_admin FROM users WHERE id = $1', [userId])).rows[0].is_admin; if (!newIsAdmin && permissions.length > 0) { const permissionIds = normalizePermissionIds(permissions); if (permissionIds.length > 0) { await client.query( `INSERT INTO user_permissions (user_id, permission_id) SELECT $1, unnest($2::int[]) ON CONFLICT DO NOTHING`, [userId, permissionIds] ); } } } 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(); } }); router.delete('/users/:id', authenticate, requirePermission('delete:users'), async (req, res) => { try { const userId = req.params.id; if (req.user.id === parseInt(userId, 10)) { return res.status(400).json({ error: 'Cannot delete your own account' }); } 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' }); } }); 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' }); } }); 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' }); } }); return router; } function normalizePermissionIds(permissions) { return permissions .map((p) => { if (typeof p === 'object' && p?.id) return parseInt(p.id, 10); if (typeof p === 'number') return p; if (typeof p === 'string' && !Number.isNaN(parseInt(p, 10))) return parseInt(p, 10); return null; }) .filter((id) => id !== null && !Number.isNaN(id)); }