1126 lines
33 KiB
JavaScript
1126 lines
33 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 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)
|
|
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';
|
|
if (container.includes('postgres') || container.includes('mysql') || container.includes('mongo')) type = 'database';
|
|
else if (container.includes('redis') || container.includes('memcached')) type = 'cache';
|
|
else if (container.includes('nginx') || container.includes('proxy') || container.includes('traefik')) type = 'proxy';
|
|
else if (container.includes('drone-detection') || container.includes('uamils')) type = 'application';
|
|
else if (container.includes('elasticsearch') || container.includes('kibana') || container.includes('logstash')) type = 'logging';
|
|
else if (container.includes('prometheus') || container.includes('grafana')) type = 'monitoring';
|
|
|
|
// If we don't have health endpoint data, use docker stats
|
|
if (!containerMetrics[container]) {
|
|
containerMetrics[container] = {
|
|
cpu: cpu,
|
|
memory: { usage: memUsage, percentage: memPerc },
|
|
network: netIO,
|
|
disk: blockIO,
|
|
type: type,
|
|
source: 'docker_stats'
|
|
};
|
|
} else {
|
|
// Enhance existing health data with docker stats
|
|
containerMetrics[container] = {
|
|
...containerMetrics[container],
|
|
cpu: cpu,
|
|
memory: { usage: memUsage, percentage: memPerc },
|
|
network: netIO,
|
|
disk: blockIO
|
|
};
|
|
}
|
|
}
|
|
});
|
|
} catch (dockerError) {
|
|
console.log('Docker stats failed, trying docker compose...');
|
|
|
|
// Try container inspection via docker compose
|
|
try {
|
|
const { stdout: composeStatus } = await execAsync('docker-compose ps --format json');
|
|
const containers = JSON.parse(`[${composeStatus.split('\n').filter(line => line.trim()).join(',')}]`);
|
|
|
|
containers.forEach(container => {
|
|
if (container.Name && !containerMetrics[container.Name]) {
|
|
let type = 'unknown';
|
|
const name = container.Name.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')) type = 'proxy';
|
|
else if (name.includes('drone-detection') || name.includes('uamils')) type = 'application';
|
|
|
|
containerMetrics[container.Name] = {
|
|
status: container.State,
|
|
health: container.Health || 'unknown',
|
|
ports: container.Ports,
|
|
type: type,
|
|
source: 'docker_compose'
|
|
};
|
|
}
|
|
});
|
|
} catch (composeError) {
|
|
// Final fallback - try to detect containers via process list
|
|
try {
|
|
const { stdout: processes } = await execAsync('ps aux | grep -E "(postgres|redis|nginx|docker)" | grep -v grep');
|
|
const processLines = processes.split('\n').filter(line => line.trim());
|
|
|
|
const detectedServices = {};
|
|
processLines.forEach(line => {
|
|
if (line.includes('postgres')) detectedServices['postgres-process'] = { status: 'running', type: 'database', source: 'process_list' };
|
|
if (line.includes('redis')) detectedServices['redis-process'] = { status: 'running', type: 'cache', source: 'process_list' };
|
|
if (line.includes('nginx')) detectedServices['nginx-process'] = { status: 'running', type: 'proxy', source: 'process_list' };
|
|
});
|
|
|
|
if (Object.keys(detectedServices).length > 0) {
|
|
containerMetrics = { ...containerMetrics, ...detectedServices };
|
|
} else {
|
|
containerMetrics = {
|
|
error: 'All container monitoring methods failed',
|
|
attempts: ['health_endpoints', 'docker_stats', 'docker_compose', 'process_list'],
|
|
lastError: composeError.message
|
|
};
|
|
}
|
|
} catch (processError) {
|
|
containerMetrics = {
|
|
error: 'All container monitoring methods failed',
|
|
attempts: ['health_endpoints', 'docker_stats', 'docker_compose', 'process_list'],
|
|
lastError: processError.message
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get system memory and CPU info
|
|
let systemMetrics = {};
|
|
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]);
|
|
|
|
const { stdout: cpuInfo } = await execAsync('top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - $1}\'');
|
|
const cpuUsage = parseFloat(cpuInfo.trim());
|
|
|
|
const { stdout: diskInfo } = await execAsync('df -h / | awk \'NR==2{print $3 " / " $2 " (" $5 ")"}\'');
|
|
|
|
systemMetrics = {
|
|
memory: {
|
|
used: `${usedMem}MB`,
|
|
total: `${totalMem}MB`,
|
|
percentage: Math.round((usedMem / totalMem) * 100)
|
|
},
|
|
cpu: {
|
|
usage: `${cpuUsage.toFixed(1)}%`,
|
|
percentage: cpuUsage
|
|
},
|
|
disk: diskInfo.trim()
|
|
};
|
|
} 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: 'GET',
|
|
timeout: 5000
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
const cert = res.connection.getPeerCertificate();
|
|
if (cert && cert.valid_to) {
|
|
const expiryDate = new Date(cert.valid_to);
|
|
const daysUntilExpiry = Math.ceil((expiryDate - new Date()) / (1000 * 60 * 60 * 24));
|
|
|
|
resolve({
|
|
status: daysUntilExpiry > 30 ? 'valid' : daysUntilExpiry > 7 ? 'warning' : 'critical',
|
|
expiresAt: expiryDate.toISOString(),
|
|
daysUntilExpiry: daysUntilExpiry,
|
|
issuer: cert.issuer?.O || 'Unknown',
|
|
subject: cert.subject?.CN || hostname
|
|
});
|
|
} else {
|
|
resolve({
|
|
status: 'error',
|
|
expiresAt: null,
|
|
error: 'Certificate not found'
|
|
});
|
|
}
|
|
});
|
|
|
|
req.on('error', () => {
|
|
resolve({
|
|
status: 'error',
|
|
expiresAt: null,
|
|
error: 'Connection failed'
|
|
});
|
|
});
|
|
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
resolve({
|
|
status: 'error',
|
|
expiresAt: null,
|
|
error: '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;
|