const express = require("express"); const bcrypt = require("bcrypt"); const { query } = require("../config/database"); const { requireAuth, requireRole } = require("../middleware/auth"); const { apiLimiter } = require("../config/rateLimiter"); const logger = require("../config/logger"); const { validators, handleValidationErrors, } = require("../middleware/validators"); const { asyncHandler } = require("../middleware/errorHandler"); const router = express.Router(); // Apply rate limiting router.use(apiLimiter); // Require admin role for all routes router.use(requireAuth); router.use(requireRole("role-admin")); // Get all users with roles router.get("/", async (req, res) => { try { const result = await query(` SELECT u.id, u.username, u.email, u.name, u.role, u.isactive, u.last_login, u.createdat, u.passwordneverexpires FROM adminusers u ORDER BY u.createdat DESC `); res.json({ success: true, users: result.rows, }); } catch (error) { logger.error("Get users error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Get all roles router.get("/roles", async (req, res) => { try { const result = await query(` SELECT id, name, description, permissions FROM roles ORDER BY name `); res.json({ success: true, roles: result.rows, }); } catch (error) { logger.error("Get roles error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Get single user by ID router.get("/:id", async (req, res) => { try { const { id } = req.params; const result = await query( ` SELECT u.id, u.username, u.email, u.name, u.role, u.isactive, u.last_login, u.createdat, u.passwordneverexpires, u.role_id FROM adminusers u WHERE u.id = $1 `, [id], ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } res.json({ success: true, user: result.rows[0], }); } catch (error) { logger.error("Get user error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Create new user router.post("/", async (req, res) => { try { const { name, username, email, password, role, passwordneverexpires } = req.body; // Validate required fields if (!username || !email || !password || !role) { return res.status(400).json({ success: false, message: "Name, username, email, password, and role are required", }); } // Check if user already exists const existing = await query( "SELECT id FROM adminusers WHERE email = $1 OR username = $2", [email, username], ); if (existing.rows.length > 0) { return res.status(400).json({ success: false, message: "User with this email or username already exists", }); } // Hash password with bcrypt (12 rounds minimum for security) const BCRYPT_COST = 12; // Validate password requirements if (password.length < 8) { return res.status(400).json({ success: false, message: "Password must be at least 8 characters long", }); } if (!/[A-Z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one uppercase letter", }); } if (!/[a-z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one lowercase letter", }); } if (!/[0-9]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one number", }); } const hashedPassword = await bcrypt.hash(password, BCRYPT_COST); // Calculate password expiry (90 days from now if not never expires) let passwordExpiresAt = null; if (!passwordneverexpires) { const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 90); passwordExpiresAt = expiryDate.toISOString(); } // Resolve role - handle both role ID (e.g., 'role-admin') and role name (e.g., 'Admin') let roleId, roleName; // First try to find by ID let roleResult = await query("SELECT id, name FROM roles WHERE id = $1", [ role, ]); if (roleResult.rows.length > 0) { // Found by ID roleId = roleResult.rows[0].id; roleName = roleResult.rows[0].name; } else { // Try to find by name roleResult = await query("SELECT id, name FROM roles WHERE name = $1", [ role, ]); if (roleResult.rows.length > 0) { roleId = roleResult.rows[0].id; roleName = roleResult.rows[0].name; } else { // Default to admin role roleId = "role-admin"; roleName = "Admin"; } } // Insert new user with both role and role_id fields const result = await query( ` INSERT INTO adminusers ( id, name, username, email, passwordhash, role, role_id, passwordneverexpires, password_expires_at, isactive, created_by, createdat, lastpasswordchange ) VALUES ( 'user-' || gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, true, $9, NOW(), NOW() ) RETURNING id, name, username, email, role, role_id, isactive, createdat, passwordneverexpires `, [ name || username, username, email, hashedPassword, roleName, roleId, passwordneverexpires || false, passwordExpiresAt, req.session.user.email, ], ); res.json({ success: true, message: "User created successfully", user: result.rows[0], }); } catch (error) { logger.error("Create user error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Update user router.put("/:id", async (req, res) => { try { const { id } = req.params; const { name, username, email, role, isactive, passwordneverexpires, password, } = req.body; // Build update query dynamically const updates = []; const values = []; let paramCount = 1; if (name !== undefined) { updates.push(`name = $${paramCount++}`); values.push(name); } if (username !== undefined) { updates.push(`username = $${paramCount++}`); values.push(username); } if (email !== undefined) { updates.push(`email = $${paramCount++}`); values.push(email); } if (role !== undefined) { // Resolve role - handle both role ID (e.g., 'role-admin') and role name (e.g., 'Admin') let roleId, roleName; // First try to find by ID let roleResult = await query("SELECT id, name FROM roles WHERE id = $1", [ role, ]); if (roleResult.rows.length > 0) { roleId = roleResult.rows[0].id; roleName = roleResult.rows[0].name; } else { // Try to find by name roleResult = await query("SELECT id, name FROM roles WHERE name = $1", [ role, ]); if (roleResult.rows.length > 0) { roleId = roleResult.rows[0].id; roleName = roleResult.rows[0].name; } else { // Default to admin role roleId = "role-admin"; roleName = "Admin"; } } updates.push(`role = $${paramCount++}`); values.push(roleName); updates.push(`role_id = $${paramCount++}`); values.push(roleId); } if (isactive !== undefined) { updates.push(`isactive = $${paramCount++}`); values.push(isactive); } if (passwordneverexpires !== undefined) { updates.push(`passwordneverexpires = $${paramCount++}`); values.push(passwordneverexpires); // If setting to never expire, clear expiry date if (passwordneverexpires) { updates.push(`password_expires_at = NULL`); } } // Handle password update if provided if (password !== undefined && password !== "") { // Validate password requirements if (password.length < 8) { return res.status(400).json({ success: false, message: "Password must be at least 8 characters long", }); } if (!/[A-Z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one uppercase letter", }); } if (!/[a-z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one lowercase letter", }); } if (!/[0-9]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one number", }); } const hashedPassword = await bcrypt.hash(password, 12); updates.push(`passwordhash = $${paramCount++}`); values.push(hashedPassword); updates.push(`lastpasswordchange = NOW()`); } updates.push(`updatedat = NOW()`); values.push(id); const result = await query( ` UPDATE adminusers SET ${updates.join(", ")} WHERE id = $${paramCount} RETURNING id, name, username, email, role, role_id, isactive, passwordneverexpires `, values, ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } res.json({ success: true, message: "User updated successfully", user: result.rows[0], }); } catch (error) { logger.error("Update user error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Change user password (PUT endpoint for password modal) router.put("/:id/password", async (req, res) => { try { const { id } = req.params; const { password } = req.body; // Validate password requirements if (!password || password.length < 8) { return res.status(400).json({ success: false, message: "Password must be at least 8 characters long", }); } if (!/[A-Z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one uppercase letter", }); } if (!/[a-z]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one lowercase letter", }); } if (!/[0-9]/.test(password)) { return res.status(400).json({ success: false, message: "Password must contain at least one number", }); } // Hash new password with bcrypt (12 rounds) const hashedPassword = await bcrypt.hash(password, 12); // Get user's password expiry setting const userResult = await query( "SELECT passwordneverexpires FROM adminusers WHERE id = $1", [id], ); if (userResult.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } // Calculate new expiry date (90 days from now if not never expires) let passwordExpiresAt = null; if (!userResult.rows[0].passwordneverexpires) { const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 90); passwordExpiresAt = expiryDate.toISOString(); } // Update password await query( ` UPDATE adminusers SET passwordhash = $1, password_expires_at = $2, lastpasswordchange = NOW(), updatedat = NOW() WHERE id = $3 `, [hashedPassword, passwordExpiresAt, id], ); res.json({ success: true, message: "Password changed successfully", }); } catch (error) { logger.error("Change password error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Reset user password router.post("/:id/reset-password", async (req, res) => { try { const { id } = req.params; const { new_password } = req.body; if (!new_password || new_password.length < 6) { return res.status(400).json({ success: false, message: "Password must be at least 6 characters long", }); } // Hash new password with bcrypt (10 rounds) const hashedPassword = await bcrypt.hash(new_password, 10); // Get user's password expiry setting const userResult = await query( "SELECT passwordneverexpires FROM adminusers WHERE id = $1", [id], ); if (userResult.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } // Calculate new expiry date (90 days from now if not never expires) let passwordExpiresAt = null; if (!userResult.rows[0].passwordneverexpires) { const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 90); passwordExpiresAt = expiryDate.toISOString(); } // Update password await query( ` UPDATE adminusers SET passwordhash = $1, password_expires_at = $2, lastpasswordchange = NOW(), updatedat = NOW() WHERE id = $3 `, [hashedPassword, passwordExpiresAt, id], ); res.json({ success: true, message: "Password reset successfully", }); } catch (error) { logger.error("Reset password error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Delete user router.delete("/:id", async (req, res) => { try { const { id } = req.params; // Prevent deleting yourself if (id === req.session.user.id) { return res.status(400).json({ success: false, message: "Cannot delete your own account", }); } const result = await query( "DELETE FROM adminusers WHERE id = $1 RETURNING id", [id], ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } res.json({ success: true, message: "User deleted successfully", }); } catch (error) { logger.error("Delete user error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); // Toggle user active status router.post("/:id/toggle-status", async (req, res) => { try { const { id } = req.params; // Prevent deactivating yourself if (id === req.session.user.id) { return res.status(400).json({ success: false, message: "Cannot deactivate your own account", }); } const result = await query( ` UPDATE adminusers SET isactive = NOT isactive, updated_at = NOW() WHERE id = $1 RETURNING id, isactive `, [id], ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: "User not found", }); } res.json({ success: true, message: `User ${ result.rows[0].isactive ? "activated" : "deactivated" } successfully`, isactive: result.rows[0].isactive, }); } catch (error) { logger.error("Toggle status error:", error); res.status(500).json({ success: false, message: "Server error" }); } }); module.exports = router;