/** * Management Portal API Routes * Completely separate authentication system for security isolation */ const express = require('express'); const router = express.Router(); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); // Fixed: use bcryptjs instead of bcrypt const { Op } = require('sequelize'); // Add Sequelize operators const { Tenant, User, ManagementUser } = require('../models'); // Management-specific authentication middleware - NO shared auth with tenants const requireManagementAuth = (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ success: false, message: 'Management authentication required' }); } try { // Use separate JWT secret for management const MANAGEMENT_SECRET = process.env.MANAGEMENT_JWT_SECRET || 'mgmt-super-secret-change-in-production'; const decoded = jwt.verify(token, MANAGEMENT_SECRET); // Verify this is a management token with proper role if (!decoded.isManagement || !['super_admin', 'platform_admin'].includes(decoded.role)) { return res.status(403).json({ success: false, message: 'Insufficient management privileges' }); } req.managementUser = { id: decoded.userId, username: decoded.username, role: decoded.role, isManagement: true }; next(); } catch (error) { return res.status(403).json({ success: false, message: 'Invalid management token', error: error.message }); } }; // Management login endpoint - separate from tenant auth router.post('/auth/login', async (req, res) => { try { const { username, password } = req.body; // Use ManagementUser model instead of hardcoded users const managementUser = await ManagementUser.findByCredentials(username, password); if (!managementUser) { return res.status(401).json({ success: false, message: 'Invalid management credentials' }); } const MANAGEMENT_SECRET = process.env.MANAGEMENT_JWT_SECRET || 'mgmt-super-secret-change-in-production'; const token = jwt.sign({ userId: managementUser.id, username: managementUser.username, role: managementUser.role, isManagement: true }, MANAGEMENT_SECRET, { expiresIn: '8h' }); res.json({ success: true, token, user: { id: managementUser.id, username: managementUser.username, email: managementUser.email, first_name: managementUser.first_name, last_name: managementUser.last_name, role: managementUser.role } }); } catch (error) { res.status(500).json({ success: false, message: 'Management authentication error', error: error.message }); } }); // Apply management authentication to all other routes router.use(requireManagementAuth); // Security audit logging for all management operations router.use((req, res, next) => { const auditLog = { timestamp: new Date().toISOString(), user: req.managementUser.username, role: req.managementUser.role, method: req.method, path: req.path, ip: req.ip, userAgent: req.headers['user-agent'] }; console.log('[MANAGEMENT AUDIT]', JSON.stringify(auditLog)); next(); }); /** * GET /api/management/system-info - Platform system information */ router.get('/system-info', async (req, res) => { try { const tenantCount = await Tenant.count(); const userCount = await User.count(); res.json({ success: true, data: { platform: { name: 'UAMILS Platform', version: '1.0.0', environment: process.env.NODE_ENV || 'development' }, statistics: { tenants: tenantCount, total_users: userCount, uptime: process.uptime() }, security: { management_access_level: req.managementUser.role, last_backup: process.env.LAST_BACKUP_DATE || 'Not configured' } } }); } catch (error) { console.error('Management: Error fetching system info:', error); res.status(500).json({ success: false, message: 'Failed to fetch system information', error: error.message }); } }); /** * GET /api/management/tenants - List all tenants with admin details */ router.get('/tenants', async (req, res) => { try { const { limit = 50, offset = 0, search, auth_provider } = req.query; const whereClause = {}; if (search) { whereClause[Op.or] = [ { name: { [Op.iLike]: `%${search}%` } }, { slug: { [Op.iLike]: `%${search}%` } }, { domain: { [Op.iLike]: `%${search}%` } } ]; } if (auth_provider) { whereClause.auth_provider = auth_provider; } const tenants = await Tenant.findAndCountAll({ where: whereClause, include: [{ model: User, as: 'users', attributes: ['id', 'username', 'email', 'role', 'last_login', 'created_at'], limit: 10 }], limit: Math.min(parseInt(limit), 100), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: tenants.rows, pagination: { total: tenants.count, limit: parseInt(limit), offset: parseInt(offset), pages: Math.ceil(tenants.count / parseInt(limit)) } }); } catch (error) { console.error('Management: Error fetching tenants:', error); res.status(500).json({ success: false, message: 'Failed to fetch tenants' }); } }); /** * POST /api/management/tenants - Create new tenant */ router.post('/tenants', async (req, res) => { try { const tenantData = req.body; // Enhanced validation for management portal if (!tenantData.name || !tenantData.slug) { return res.status(400).json({ success: false, message: 'Name and slug are required' }); } // Convert empty domain string to null to avoid unique constraint issues if (tenantData.domain === '') { tenantData.domain = null; } // Check for unique slug const existingTenant = await Tenant.findOne({ where: { slug: tenantData.slug } }); if (existingTenant) { return res.status(409).json({ success: false, message: 'Tenant slug already exists' }); } // Check for unique domain if provided if (tenantData.domain) { const existingDomain = await Tenant.findOne({ where: { domain: tenantData.domain } }); if (existingDomain) { return res.status(409).json({ success: false, message: 'Domain already exists for another tenant' }); } } // Log management action console.log(`Management: Admin ${req.managementUser.username} creating tenant: ${tenantData.name}`); const tenant = await Tenant.create(tenantData); res.status(201).json({ success: true, data: tenant, message: 'Tenant created successfully' }); } catch (error) { console.error('Management: Error creating tenant:', error); res.status(500).json({ success: false, message: 'Failed to create tenant' }); } }); /** * GET /api/management/tenants/:id - Get tenant details */ router.get('/tenants/:id', async (req, res) => { try { const tenant = await Tenant.findByPk(req.params.id, { include: [{ model: User, as: 'users', attributes: ['id', 'username', 'email', 'role', 'last_login', 'created_at'] }] }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } res.json({ success: true, data: tenant }); } catch (error) { console.error('Management: Error fetching tenant:', error); res.status(500).json({ success: false, message: 'Failed to fetch tenant' }); } }); /** * PUT /api/management/tenants/:id - Update tenant */ router.put('/tenants/:id', async (req, res) => { try { const tenant = await Tenant.findByPk(req.params.id); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const updateData = req.body; // Convert empty domain string to null to avoid unique constraint issues if (updateData.domain === '') { updateData.domain = null; } // Check for unique domain if provided and different from current if (updateData.domain && updateData.domain !== tenant.domain) { const existingDomain = await Tenant.findOne({ where: { domain: updateData.domain, id: { [require('sequelize').Op.ne]: tenant.id } } }); if (existingDomain) { return res.status(409).json({ success: false, message: 'Domain already exists for another tenant' }); } } // Log management action console.log(`Management: Admin ${req.managementUser.username} updating tenant: ${tenant.name}`); await tenant.update(updateData); res.json({ success: true, data: tenant, message: 'Tenant updated successfully' }); } catch (error) { console.error('Management: Error updating tenant:', error); res.status(500).json({ success: false, message: 'Failed to update tenant' }); } }); /** * DELETE /api/management/tenants/:id - Delete tenant */ router.delete('/tenants/:id', async (req, res) => { try { const tenant = await Tenant.findByPk(req.params.id); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Prevent deletion of default tenant if (tenant.slug === 'default') { return res.status(403).json({ success: false, message: 'Cannot delete default tenant' }); } // Log management action console.log(`Management: Admin ${req.managementUser.username} deleting tenant: ${tenant.name}`); await tenant.destroy(); res.json({ success: true, message: 'Tenant deleted successfully' }); } catch (error) { console.error('Management: Error deleting tenant:', error); res.status(500).json({ success: false, message: 'Failed to delete tenant' }); } }); /** * GET /api/management/users - List all users across tenants */ router.get('/users', async (req, res) => { try { const { limit = 50, offset = 0, search, role, tenant_id } = req.query; const whereClause = {}; if (search) { whereClause[Op.or] = [ { username: { [Op.iLike]: `%${search}%` } }, { email: { [Op.iLike]: `%${search}%` } } ]; } if (role) { whereClause.role = role; } if (tenant_id) { whereClause.tenant_id = tenant_id; } const users = await User.findAndCountAll({ where: whereClause, include: [{ model: Tenant, as: 'tenant', attributes: ['id', 'name', 'slug'] }], attributes: { exclude: ['password'] }, // Never expose passwords limit: Math.min(parseInt(limit), 100), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: users.rows, pagination: { total: users.count, limit: parseInt(limit), offset: parseInt(offset), pages: Math.ceil(users.count / parseInt(limit)) } }); } catch (error) { console.error('Management: Error fetching users:', error); res.status(500).json({ success: false, message: 'Failed to fetch users' }); } }); /** * GET /api/management/system/info - System information */ router.get('/system/info', async (req, res) => { try { // Gather system statistics const tenantCount = await Tenant.count(); const userCount = await User.count(); const activeTenantsCount = await Tenant.count({ where: { is_active: true } }); const systemInfo = { version: process.env.APP_VERSION || '1.0.0', environment: process.env.NODE_ENV || 'production', uptime: process.uptime(), database: { status: 'connected', version: 'PostgreSQL 15', connections: 5, // You can get this from pg pool maxConnections: 100 }, statistics: { tenants: tenantCount, users: userCount, activeTenants: activeTenantsCount }, memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + 'MB', total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + 'MB', percentage: Math.round((process.memoryUsage().heapUsed / process.memoryUsage().heapTotal) * 100) }, lastBackup: new Date(Date.now() - 24*60*60*1000).toISOString(), // Mock ssl: { status: 'valid', expiresAt: new Date(Date.now() + 90*24*60*60*1000).toISOString() // Mock 90 days } }; res.json({ success: true, data: systemInfo }); } catch (error) { console.error('Management: Error fetching system info:', error); res.status(500).json({ success: false, message: 'Failed to fetch system information' }); } }); /** * POST /api/management/tenants/:tenantId/users - Create user in specific tenant */ router.post('/tenants/:tenantId/users', async (req, res) => { try { const { tenantId } = req.params; const userData = req.body; // Verify tenant exists const tenant = await Tenant.findByPk(tenantId); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if user already exists in this tenant const existingUser = await User.findOne({ where: { username: userData.username, tenant_id: tenantId } }); if (existingUser) { return res.status(409).json({ success: false, message: 'User already exists in this tenant' }); } // Hash password const bcrypt = require('bcryptjs'); const hashedPassword = await bcrypt.hash(userData.password, 10); // Create user with tenant association const user = await User.create({ ...userData, password_hash: hashedPassword, // Use correct field name tenant_id: tenantId, created_by: req.managementUser.username }); // Remove password_hash from response const userResponse = user.toJSON(); delete userResponse.password_hash; console.log(`Management: Admin ${req.managementUser.username} created user ${userData.username} in tenant ${tenant.name}`); res.status(201).json({ success: true, data: userResponse, message: 'User created successfully' }); } catch (error) { console.error('Management: Error creating user:', error); res.status(500).json({ success: false, message: 'Failed to create user', error: error.message }); } }); /** * PUT /api/management/tenants/:tenantId/users/:userId - Update user in tenant */ router.put('/tenants/:tenantId/users/:userId', async (req, res) => { try { const { tenantId, userId } = req.params; const updates = req.body; const user = await User.findOne({ where: { id: userId, tenant_id: tenantId }, include: [{ model: Tenant, as: 'tenant', attributes: ['name'] }] }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Hash password if provided if (updates.password) { const bcrypt = require('bcryptjs'); updates.password_hash = await bcrypt.hash(updates.password, 10); delete updates.password; // Remove plain password } await user.update(updates); // Remove password_hash from response const userResponse = user.toJSON(); delete userResponse.password_hash; console.log(`Management: Admin ${req.managementUser.username} updated user ${user.username} in tenant ${user.tenant.name}`); res.json({ success: true, data: userResponse, message: 'User updated successfully' }); } catch (error) { console.error('Management: Error updating user:', error); res.status(500).json({ success: false, message: 'Failed to update user', error: error.message }); } }); /** * DELETE /api/management/tenants/:tenantId/users/:userId - Delete user from tenant */ router.delete('/tenants/:tenantId/users/:userId', async (req, res) => { try { const { tenantId, userId } = req.params; const user = await User.findOne({ where: { id: userId, tenant_id: tenantId }, include: [{ model: Tenant, as: 'tenant', attributes: ['name'] }] }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Prevent deleting the last admin user if (user.role === 'admin') { const adminCount = await User.count({ where: { tenant_id: tenantId, role: 'admin' } }); if (adminCount <= 1) { return res.status(403).json({ success: false, message: 'Cannot delete the last admin user in tenant' }); } } console.log(`Management: Admin ${req.managementUser.username} deleting user ${user.username} from tenant ${user.tenant.name}`); await user.destroy(); res.json({ success: true, message: 'User deleted successfully' }); } catch (error) { console.error('Management: Error deleting user:', error); res.status(500).json({ success: false, message: 'Failed to delete user', error: error.message }); } }); /** * GET /api/management/tenants/:tenantId/users - Get all users in a tenant */ router.get('/tenants/:tenantId/users', async (req, res) => { try { const { tenantId } = req.params; const { limit = 50, offset = 0, search, role } = req.query; // Verify tenant exists const tenant = await Tenant.findByPk(tenantId); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const whereClause = { tenant_id: tenantId }; if (search) { whereClause[Op.or] = [ { username: { [Op.iLike]: `%${search}%` } }, { email: { [Op.iLike]: `%${search}%` } }, { first_name: { [Op.iLike]: `%${search}%` } }, { last_name: { [Op.iLike]: `%${search}%` } } ]; } if (role) { whereClause.role = role; } const users = await User.findAndCountAll({ where: whereClause, attributes: { exclude: ['password_hash'] }, // Exclude password_hash, not password limit: Math.min(parseInt(limit), 100), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: users.rows, pagination: { total: users.count, limit: parseInt(limit), offset: parseInt(offset), pages: Math.ceil(users.count / parseInt(limit)) }, tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug } }); } catch (error) { console.error('Management: Error fetching tenant users:', error); res.status(500).json({ success: false, message: 'Failed to fetch tenant users', error: error.message }); } }); /** * POST /api/management/tenants/:tenantId/activate - Activate tenant */ router.post('/tenants/:tenantId/activate', async (req, res) => { try { const { tenantId } = req.params; const tenant = await Tenant.findByPk(tenantId); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } await tenant.update({ is_active: true }); console.log(`Management: Admin ${req.managementUser.username} activated tenant ${tenant.name}`); res.json({ success: true, data: tenant, message: 'Tenant activated successfully' }); } catch (error) { console.error('Management: Error activating tenant:', error); res.status(500).json({ success: false, message: 'Failed to activate tenant' }); } }); /** * POST /api/management/tenants/:tenantId/deactivate - Deactivate tenant */ router.post('/tenants/:tenantId/deactivate', async (req, res) => { try { const { tenantId } = req.params; const tenant = await Tenant.findByPk(tenantId); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } await tenant.update({ is_active: false }); console.log(`Management: Admin ${req.managementUser.username} deactivated tenant ${tenant.name}`); res.json({ success: true, data: tenant, message: 'Tenant deactivated successfully' }); } catch (error) { console.error('Management: Error deactivating tenant:', error); res.status(500).json({ success: false, message: 'Failed to deactivate tenant' }); } }); module.exports = router;