/** * 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'); const { createErrorResponse, getSystemMessage, getLanguageFromRequest } = require('../utils/i18n'); // Management-specific authentication middleware - NO shared auth with tenants const requireManagementAuth = (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { const errorResponse = createErrorResponse(req, 401, 'AUTHENTICATION_REQUIRED'); return res.status(errorResponse.status).json(errorResponse.json); } 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)) { const errorResponse = createErrorResponse(req, 403, 'INSUFFICIENT_PRIVILEGES'); return res.status(errorResponse.status).json(errorResponse.json); } req.managementUser = { id: decoded.userId, username: decoded.username, role: decoded.role, isManagement: true }; next(); } catch (error) { const errorResponse = createErrorResponse(req, 403, 'INVALID_TOKEN'); return res.status(errorResponse.status).json(errorResponse.json); } }; // 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) { const errorResponse = createErrorResponse(req, 401, 'INVALID_MANAGEMENT_CREDENTIALS'); return res.status(errorResponse.status).json(errorResponse.json); } 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) { const errorResponse = createErrorResponse(req, 500, 'MANAGEMENT_AUTH_ERROR'); errorResponse.json.error = error.message; return res.status(errorResponse.status).json(errorResponse.json); } }); // 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 - Comprehensive platform system information */ router.get('/system-info', async (req, res) => { try { const { exec } = require('child_process'); const https = require('https'); const { promisify } = require('util'); const execAsync = promisify(exec); // Get basic statistics const tenantCount = await Tenant.count(); const userCount = await User.count(); // Get container metrics using internal health endpoints let containerMetrics = {}; const containerEndpoints = [ // Application containers - use proper service names and ports { name: 'frontend', url: 'http://frontend/health', type: 'app' }, { name: 'backend', url: 'http://backend:3000/health', type: 'app' }, { name: 'management', url: 'http://management:3001/health', type: 'app' }, // Database containers - use proper connection checks { name: 'postgres', url: 'postgres://postgres:5432', type: 'database' }, { name: 'redis', url: 'redis://redis:6379', type: 'cache' } ]; // Try internal container health endpoints first try { const https = require('https'); const http = require('http'); const net = require('net'); const healthChecks = await Promise.allSettled( containerEndpoints.map(async ({ name, url, type }) => { return new Promise((resolve, reject) => { // Handle database/redis connections differently if (type === 'database' && url.startsWith('postgres://')) { // Simple TCP connection test for postgres const socket = net.createConnection(5432, 'postgres'); socket.setTimeout(3000); socket.on('connect', () => { socket.destroy(); resolve({ name, metrics: { status: 'connected', type, source: 'tcp_check', port: 5432 } }); }); socket.on('error', (error) => { resolve({ name, metrics: { status: 'unreachable', type, error: error.message, source: 'tcp_check_failed' } }); }); socket.on('timeout', () => { socket.destroy(); resolve({ name, metrics: { status: 'timeout', type, source: 'tcp_check_failed' } }); }); return; } if (type === 'cache' && url.startsWith('redis://')) { // Simple TCP connection test for redis const socket = net.createConnection(6379, 'redis'); socket.setTimeout(3000); socket.on('connect', () => { socket.destroy(); resolve({ name, metrics: { status: 'connected', type, source: 'tcp_check', port: 6379 } }); }); socket.on('error', (error) => { resolve({ name, metrics: { status: 'unreachable', type, error: error.message, source: 'tcp_check_failed' } }); }); socket.on('timeout', () => { socket.destroy(); resolve({ name, metrics: { status: 'timeout', type, source: 'tcp_check_failed' } }); }); return; } // HTTP health checks for app containers const urlObj = new URL(url); const client = urlObj.protocol === 'https:' ? https : http; const req = client.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80), path: urlObj.pathname || '/', method: 'GET', timeout: 3000 }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const metrics = res.headers['content-type']?.includes('application/json') ? JSON.parse(data) : { status: 'healthy', raw: data }; resolve({ name, metrics: { ...metrics, type, source: 'health_endpoint', httpStatus: res.statusCode } }); } catch (e) { resolve({ name, metrics: { status: 'responding', type, source: 'basic_check', httpStatus: res.statusCode, data: data.substring(0, 100) } }); } }); }); req.on('error', (error) => { resolve({ name, metrics: { status: 'unreachable', type, error: error.message, source: 'health_check_failed' } }); }); req.on('timeout', () => { req.destroy(); resolve({ name, metrics: { status: 'timeout', type, source: 'health_check_failed' } }); }); req.end(); }); }) ); healthChecks.forEach((result) => { if (result.status === 'fulfilled') { containerMetrics[result.value.name] = result.value.metrics; } }); } catch (healthError) { console.log('Container health checks failed, trying Docker stats...', healthError.message); } // Fallback to Docker stats for ALL containers (not just our apps) if (Object.keys(containerMetrics).length === 0 || Object.values(containerMetrics).every(m => m.status === 'unreachable')) { try { const { stdout } = await execAsync('docker stats --no-stream --format "table {{.Container}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}\\t{{.NetIO}}\\t{{.BlockIO}}"'); const lines = stdout.trim().split('\n').slice(1); lines.forEach(line => { const [container, cpu, memUsage, memPerc, netIO, blockIO] = line.split('\t'); if (container && cpu) { // Map actual container names to our simplified names let simpleName = container; let type = 'unknown'; if (container.includes('frontend') || container.includes('nginx')) { simpleName = 'frontend'; type = 'app'; } else if (container.includes('backend') || container.includes('api')) { simpleName = 'backend'; type = 'app'; } else if (container.includes('management') || container.includes('admin')) { simpleName = 'management'; type = 'app'; } else if (container.includes('postgres') || container.includes('postgresql')) { simpleName = 'postgres'; type = 'database'; } else if (container.includes('redis')) { simpleName = 'redis'; type = 'cache'; } // Use simplified name for consistency containerMetrics[simpleName] = { cpu: cpu, memory: { usage: memUsage, percentage: memPerc }, network: netIO, disk: blockIO, type: type, source: 'docker_stats', status: 'running', container_name: container }; } }); } catch (dockerError) { console.log('Docker stats failed, using TCP checks as final verification...', dockerError.message); // Try container inspection via docker compose try { const { stdout: composeStatus } = await execAsync('docker-compose ps --services 2>/dev/null || docker compose ps --services 2>/dev/null'); const services = composeStatus.trim().split('\n').filter(s => s.trim()); if (services.length > 0) { for (const service of services) { let type = 'unknown'; const name = service.toLowerCase(); if (name.includes('postgres') || name.includes('mysql') || name.includes('mongo') || name.includes('db')) type = 'database'; else if (name.includes('redis') || name.includes('cache')) type = 'cache'; else if (name.includes('nginx') || name.includes('proxy')) type = 'proxy'; else if (name.includes('drone-detection') || name.includes('uamils') || name.includes('app') || name.includes('backend') || name.includes('frontend')) type = 'application'; containerMetrics[service] = { status: 'detected', health: 'unknown', type: type, source: 'docker_compose_services' }; } } } catch (composeError) { // Final fallback - try to detect running services via different methods try { // Check for common database ports const portChecks = [ { port: 5432, name: 'postgresql', type: 'database' }, { port: 3306, name: 'mysql', type: 'database' }, { port: 6379, name: 'redis', type: 'cache' }, { port: 80, name: 'nginx', type: 'proxy' }, { port: 443, name: 'nginx-ssl', type: 'proxy' } ]; const { stdout: netstatOutput } = await execAsync('netstat -tlnp 2>/dev/null || ss -tlnp 2>/dev/null || echo "no netstat"'); for (const { port, name, type } of portChecks) { if (netstatOutput.includes(`:${port} `)) { containerMetrics[`${name}-service`] = { status: 'port_listening', port: port, type: type, source: 'port_detection' }; } } // If still no containers found, show a helpful message if (Object.keys(containerMetrics).length === 0) { containerMetrics = { info: 'No containers detected', message: 'This could mean Docker is not running, no containers are active, or the monitoring system needs Docker access', suggestions: [ 'Check if Docker is running: docker ps', 'Ensure management container has Docker socket access', 'Try: docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ...' ] }; } } catch (finalError) { containerMetrics = { error: 'All container monitoring methods failed', attempts: ['health_endpoints', 'docker_stats', 'docker_compose', 'port_detection'], lastError: finalError.message, troubleshooting: { docker_access: 'Ensure management container can access Docker daemon', permissions: 'Container may need privileged access or Docker socket mount', environment: 'Check if running in Docker environment vs local development' } }; } } } } // Get system memory and CPU info let systemMetrics = {}; try { // Try Linux commands first try { const { stdout: memInfo } = await execAsync('free -m'); const memLines = memInfo.split('\n')[1].split(/\s+/); const totalMem = parseInt(memLines[1]); const usedMem = parseInt(memLines[2]); systemMetrics.memory = { used: `${usedMem}MB`, total: `${totalMem}MB`, percentage: Math.round((usedMem / totalMem) * 100) }; } catch (memError) { // Fallback for Windows or other systems const totalMem = Math.round(require('os').totalmem() / 1024 / 1024); const freeMem = Math.round(require('os').freemem() / 1024 / 1024); const usedMem = totalMem - freeMem; systemMetrics.memory = { used: `${usedMem}MB`, total: `${totalMem}MB`, percentage: Math.round((usedMem / totalMem) * 100) }; } // CPU usage - fix negative values try { const { stdout: cpuInfo } = await execAsync('top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - $1}\''); let cpuUsage = parseFloat(cpuInfo.trim()); // Fix negative or invalid CPU values if (isNaN(cpuUsage) || cpuUsage < 0 || cpuUsage > 100) { // Fallback to load average calculation const loadAvg = require('os').loadavg()[0]; const cpuCount = require('os').cpus().length; cpuUsage = Math.min((loadAvg / cpuCount) * 100, 100); } systemMetrics.cpu = { usage: `${cpuUsage.toFixed(1)}%`, percentage: cpuUsage }; } catch (cpuError) { // Ultimate fallback const loadAvg = require('os').loadavg()[0]; const cpuCount = require('os').cpus().length; const cpuUsage = Math.min((loadAvg / cpuCount) * 100, 100); systemMetrics.cpu = { usage: `${cpuUsage.toFixed(1)}%`, percentage: cpuUsage }; } // Disk usage try { const { stdout: diskInfo } = await execAsync('df -h / | awk \'NR==2{print $3 " / " $2 " (" $5 ")"}\''); systemMetrics.disk = diskInfo.trim(); } catch (diskError) { systemMetrics.disk = 'N/A'; } } catch (sysError) { console.log('System metrics not available:', sysError.message); systemMetrics = { error: 'System metrics not available', message: sysError.message, memory: { used: 'N/A', total: 'N/A', percentage: 0 }, cpu: { usage: 'N/A', percentage: 0 }, disk: 'N/A' }; } // Check SSL certificate expiry const checkSSLCert = (hostname) => { return new Promise((resolve) => { const options = { hostname: hostname, port: 443, method: 'HEAD', timeout: 5000, // Allow self-signed certificates for development rejectUnauthorized: false }; const req = https.request(options, (res) => { const cert = res.connection.getPeerCertificate(); if (cert && cert.valid_to) { const expiryDate = new Date(cert.valid_to); const now = new Date(); const daysUntilExpiry = Math.ceil((expiryDate - now) / (1000 * 60 * 60 * 24)); resolve({ status: daysUntilExpiry > 30 ? 'valid' : daysUntilExpiry > 7 ? 'warning' : 'critical', expiresAt: expiryDate.toISOString(), daysUntilExpiry: daysUntilExpiry, issuer: cert.issuer?.O || cert.issuer?.CN || 'Unknown', subject: cert.subject?.CN || hostname, fingerprint: cert.fingerprint || 'N/A' }); } else { resolve({ status: 'error', expiresAt: null, error: 'Certificate information not available' }); } }); req.on('error', (error) => { // Try to determine the type of error let errorMessage = error.message; if (error.code === 'ENOTFOUND') { errorMessage = 'Domain not found (DNS resolution failed)'; } else if (error.code === 'ECONNREFUSED') { errorMessage = 'Connection refused (service not running on port 443)'; } else if (error.code === 'ETIMEDOUT') { errorMessage = 'Connection timeout'; } else if (error.code === 'CERT_HAS_EXPIRED') { errorMessage = 'Certificate has expired'; } resolve({ status: 'error', expiresAt: null, error: errorMessage, errorCode: error.code }); }); req.on('timeout', () => { req.destroy(); resolve({ status: 'error', expiresAt: null, error: 'Connection timeout (5 seconds)', errorCode: 'TIMEOUT' }); }); req.end(); }); }; // Check SSL for management host only const managementHost = 'management.dev.uggla.uamils.com'; let sslStatus = {}; try { const sslCheck = await checkSSLCert(managementHost); sslStatus[managementHost] = sslCheck; } catch (sslError) { console.log('SSL check failed:', sslError.message); sslStatus[managementHost] = { status: 'error', expiresAt: null, error: 'SSL check failed: ' + sslError.message }; } res.json({ success: true, data: { platform: { name: 'UAMILS Platform', version: '1.0.0', environment: process.env.NODE_ENV || 'development', uptime: formatUptime(process.uptime()) }, statistics: { tenants: tenantCount, total_users: userCount, uptime_seconds: process.uptime() }, system: systemMetrics, containers: containerMetrics, ssl: sslStatus, security: { management_access_level: req.managementUser.role, last_backup: process.env.LAST_BACKUP_DATE || 'Not configured' }, timestamp: new Date().toISOString() } }); } 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 }); } }); // Helper function to format uptime function formatUptime(seconds) { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) { return `${days}d ${hours}h ${minutes}m`; } else if (hours > 0) { return `${hours}h ${minutes}m`; } else { return `${minutes}m`; } } /** * 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) { const errorResponse = createErrorResponse(req, 400, 'NAME_SLUG_REQUIRED'); return res.status(errorResponse.status).json(errorResponse.json); } // 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) { const errorResponse = createErrorResponse(req, 409, 'TENANT_SLUG_EXISTS'); return res.status(errorResponse.status).json(errorResponse.json); } // Check for unique domain if provided if (tenantData.domain) { const existingDomain = await Tenant.findOne({ where: { domain: tenantData.domain } }); if (existingDomain) { const errorResponse = createErrorResponse(req, 409, 'DOMAIN_EXISTS'); return res.status(errorResponse.status).json(errorResponse.json); } } // 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: getSystemMessage(getLanguageFromRequest(req), 'TENANT_CREATED') }); } catch (error) { console.error('Management: Error creating tenant:', error); const errorResponse = createErrorResponse(req, 500, 'TENANT_CREATION_FAILED'); return res.status(errorResponse.status).json(errorResponse.json); } }); /** * 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) { const errorResponse = createErrorResponse(req, 404, 'TENANT_NOT_FOUND'); return res.status(errorResponse.status).json(errorResponse.json); } res.json({ success: true, data: tenant }); } catch (error) { console.error('Management: Error fetching tenant:', error); const errorResponse = createErrorResponse(req, 500, 'FETCH_TENANT_FAILED'); return res.status(errorResponse.status).json(errorResponse.json); } }); /** * 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' }); } }); /** * GET /management/audit-logs * Retrieve security audit logs with filtering and pagination */ router.get('/audit-logs', requireManagementAuth, async (req, res) => { try { const { page = 1, limit = 50, level, action, tenantId, userId, startDate, endDate, search } = req.query; // Build where clause for filtering const where = {}; if (level) { where.level = level.toUpperCase(); } if (action) { where.action = { [Op.like]: `%${action}%` }; } if (tenantId) { where.tenant_id = tenantId; } if (userId) { where.user_id = userId; } if (startDate || endDate) { where.timestamp = {}; if (startDate) { where.timestamp[Op.gte] = new Date(startDate); } if (endDate) { where.timestamp[Op.lte] = new Date(endDate); } } if (search) { where[Op.or] = [ { message: { [Op.like]: `%${search}%` } }, { username: { [Op.like]: `%${search}%` } }, { tenant_slug: { [Op.like]: `%${search}%` } } ]; } // Calculate offset for pagination const offset = (parseInt(page) - 1) * parseInt(limit); // Get audit logs with associated data const { AuditLog } = require('../models'); const { count, rows: auditLogs } = await AuditLog.findAndCountAll({ where, include: [ { model: User, as: 'user', attributes: ['id', 'username', 'email'], required: false }, { model: Tenant, as: 'tenant', attributes: ['id', 'name', 'slug'], required: false } ], order: [['timestamp', 'DESC']], limit: parseInt(limit), offset: offset }); // Calculate pagination info const totalPages = Math.ceil(count / parseInt(limit)); const hasNextPage = parseInt(page) < totalPages; const hasPrevPage = parseInt(page) > 1; // Log the management access console.log(`[MANAGEMENT AUDIT] ${new Date().toISOString()} - Admin ${req.managementUser.username} accessed audit logs`); res.json({ success: true, data: { auditLogs, pagination: { currentPage: parseInt(page), totalPages, totalCount: count, limit: parseInt(limit), hasNextPage, hasPrevPage }, filters: { level, action, tenantId, userId, startDate, endDate, search } } }); } catch (error) { console.error('Management: Error retrieving audit logs:', error); res.status(500).json({ success: false, message: 'Failed to retrieve audit logs' }); } }); /** * GET /management/audit-logs/actions * Get list of available audit log actions for filtering */ router.get('/audit-logs/actions', requireManagementAuth, async (req, res) => { try { const { AuditLog } = require('../models'); const actions = await AuditLog.findAll({ attributes: [[AuditLog.sequelize.fn('DISTINCT', AuditLog.sequelize.col('action')), 'action']], where: { action: { [Op.ne]: null } }, raw: true }); res.json({ success: true, data: actions.map(item => item.action).filter(Boolean).sort() }); } catch (error) { console.error('Management: Error retrieving audit log actions:', error); res.status(500).json({ success: false, message: 'Failed to retrieve audit log actions' }); } }); /** * GET /management/audit-logs/summary * Get audit log summary statistics */ router.get('/audit-logs/summary', requireManagementAuth, async (req, res) => { try { const { timeframe = '24h' } = req.query; // Calculate time range const now = new Date(); let startTime; switch (timeframe) { case '1h': startTime = new Date(now.getTime() - 60 * 60 * 1000); break; case '24h': startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); break; case '7d': startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case '30d': startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; default: startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); } const { AuditLog } = require('../models'); // Get summary statistics const [totalLogs, successfulActions, failedActions, warningActions, criticalActions] = await Promise.all([ AuditLog.count({ where: { timestamp: { [Op.gte]: startTime } } }), AuditLog.count({ where: { timestamp: { [Op.gte]: startTime }, success: true } }), AuditLog.count({ where: { timestamp: { [Op.gte]: startTime }, success: false } }), AuditLog.count({ where: { timestamp: { [Op.gte]: startTime }, level: 'WARNING' } }), AuditLog.count({ where: { timestamp: { [Op.gte]: startTime }, level: 'CRITICAL' } }) ]); // Get top actions const topActions = await AuditLog.findAll({ attributes: [ 'action', [AuditLog.sequelize.fn('COUNT', AuditLog.sequelize.col('action')), 'count'] ], where: { timestamp: { [Op.gte]: startTime }, action: { [Op.ne]: null } }, group: ['action'], order: [[AuditLog.sequelize.literal('count'), 'DESC']], limit: 10, raw: true }); res.json({ success: true, data: { timeframe, period: { start: startTime.toISOString(), end: now.toISOString() }, summary: { totalLogs, successfulActions, failedActions, warningActions, criticalActions }, topActions } }); } catch (error) { console.error('Management: Error retrieving audit log summary:', error); res.status(500).json({ success: false, message: 'Failed to retrieve audit log summary' }); } }); module.exports = router;