Files
drone-detector/server/routes/management.js
2025-09-14 21:07:43 +02:00

1330 lines
39 KiB
JavaScript

/**
* 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 - 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) {
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;