/** * 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 - 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 with custom health endpoints { name: 'drone-detection-backend', url: 'http://drone-detection-backend:3000/health/metrics', type: 'app' }, { name: 'drone-detection-frontend', url: 'http://drone-detection-frontend:80/health/metrics', type: 'app' }, { name: 'drone-detection-management', url: 'http://drone-detection-management:3001/health/metrics', type: 'app' }, // Database containers - try standard health endpoints { name: 'postgres', url: 'http://postgres:5432', type: 'database' }, { name: 'redis', url: 'http://redis:6379', type: 'cache' }, // Infrastructure containers { name: 'nginx', url: 'http://nginx:80/nginx_status', type: 'proxy' }, { name: 'nginx-proxy-manager', url: 'http://nginx-proxy-manager:81/api/health', type: 'proxy' } ]; // Try internal container health endpoints first try { const https = require('https'); const http = require('http'); const healthChecks = await Promise.allSettled( containerEndpoints.map(async ({ name, url, type }) => { return new Promise((resolve, reject) => { const urlObj = new URL(url); const client = urlObj.protocol === 'https:' ? https : http; const req = client.request({ hostname: urlObj.hostname, port: urlObj.port, 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' } }); } catch (e) { resolve({ name, metrics: { status: 'responding', type, source: 'basic_check', 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...'); } // 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) { // Determine container type let type = 'unknown'; const name = container.toLowerCase(); if (name.includes('postgres') || name.includes('mysql') || name.includes('mongo')) type = 'database'; else if (name.includes('redis') || name.includes('memcached')) type = 'cache'; else if (name.includes('nginx') || name.includes('proxy') || name.includes('traefik')) type = 'proxy'; else if (name.includes('drone-detection') || name.includes('uamils')) type = 'application'; else if (name.includes('elasticsearch') || name.includes('kibana') || name.includes('logstash')) type = 'logging'; else if (name.includes('prometheus') || name.includes('grafana')) type = 'monitoring'; containerMetrics[container] = { cpu: cpu, memory: { usage: memUsage, percentage: memPerc }, network: netIO, disk: blockIO, type: type, source: 'docker_stats' }; } }); } catch (dockerError) { console.log('Docker stats failed, trying compose and processes...'); // 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) { 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;