From 04e9f548e67d2ce800818025b6cf0bee86e3acb2 Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sat, 13 Sep 2025 12:13:16 +0200 Subject: [PATCH] Fix jwt-token --- management/src/contexts/AuthContext.jsx | 19 +- scripts/fix-management-nginx.sh | 95 +++++ server/.env.example | 3 + server/routes/index.js | 4 + server/routes/management.js | 469 ++++++++++++++++++++++++ 5 files changed, 582 insertions(+), 8 deletions(-) create mode 100644 scripts/fix-management-nginx.sh create mode 100644 server/routes/management.js diff --git a/management/src/contexts/AuthContext.jsx b/management/src/contexts/AuthContext.jsx index 429f9fa..cbfd88d 100644 --- a/management/src/contexts/AuthContext.jsx +++ b/management/src/contexts/AuthContext.jsx @@ -35,26 +35,27 @@ export const AuthProvider = ({ children }) => { const login = async (username, password) => { try { - const response = await api.post('/users/login', { + // Use dedicated management auth endpoint + const response = await api.post('/management/auth/login', { username, password }) - const { token, user: userData } = response.data.data + const { token, user: userData } = response.data - // Check if user is admin - if (userData.role !== 'admin') { - throw new Error('Access denied. Admin privileges required.') + // Verify management user + if (!userData.role || !['super_admin', 'platform_admin'].includes(userData.role)) { + throw new Error('Access denied. Management privileges required.') } localStorage.setItem('management_token', token) localStorage.setItem('management_user', JSON.stringify(userData)) setUser(userData) - toast.success('Login successful') + toast.success(`Welcome, ${userData.username}! Management access granted.`) return { success: true } } catch (error) { - const message = error.response?.data?.message || error.message || 'Login failed' + const message = error.response?.data?.message || error.message || 'Management login failed' toast.error(message) return { success: false, message } } @@ -73,7 +74,9 @@ export const AuthProvider = ({ children }) => { login, logout, isAuthenticated: !!user, - isAdmin: user?.role === 'admin' + isAdmin: user?.role === 'admin' || user?.role === 'super_admin' || user?.role === 'platform_admin', + isSuperAdmin: user?.role === 'super_admin', + isPlatformAdmin: user?.role === 'platform_admin' } return {children} diff --git a/scripts/fix-management-nginx.sh b/scripts/fix-management-nginx.sh new file mode 100644 index 0000000..95b2eb6 --- /dev/null +++ b/scripts/fix-management-nginx.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# Update nginx configuration to properly route management portal API calls + +DOMAIN="${DOMAIN:-dev.uggla.uamils.com}" +NGINX_CONFIG="/etc/nginx/sites-enabled/dev.uggla.uamils.com" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +# Check if management server block exists +if ! grep -q "server_name management.$DOMAIN" "$NGINX_CONFIG" 2>/dev/null; then + log "Adding management subdomain configuration to nginx..." + + # Backup current config + cp "$NGINX_CONFIG" "${NGINX_CONFIG}.backup" + + # Add management server block before the wildcard block + # Find the line with wildcard subdomain and insert before it + sed -i '/# Wildcard subdomains/i\ +# Management Portal (specific subdomain - MUST come before wildcard)\ +server {\ + listen 443 ssl http2;\ + server_name management.'$DOMAIN';\ +\ + ssl_certificate /etc/letsencrypt/live/'$DOMAIN'/fullchain.pem;\ + ssl_certificate_key /etc/letsencrypt/live/'$DOMAIN'/privkey.pem;\ +\ + ssl_protocols TLSv1.2 TLSv1.3;\ + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;\ + ssl_prefer_server_ciphers off;\ + ssl_session_cache shared:SSL:10m;\ + ssl_session_timeout 10m;\ + ssl_stapling on;\ + ssl_stapling_verify on;\ +\ + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;\ + add_header X-Frame-Options DENY always;\ + add_header X-Content-Type-Options nosniff always;\ + add_header X-XSS-Protection "1; mode=block" always;\ + add_header Referrer-Policy "strict-origin-when-cross-origin" always;\ +\ + # Management frontend - port 3003\ + location / {\ + proxy_pass http://127.0.0.1:3003;\ + proxy_http_version 1.1;\ + proxy_set_header Upgrade $http_upgrade;\ + proxy_set_header Connection '\''upgrade'\'';\ + proxy_set_header Host $host;\ + proxy_set_header X-Real-IP $remote_addr;\ + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\ + proxy_set_header X-Forwarded-Proto $scheme;\ + proxy_cache_bypass $http_upgrade;\ + proxy_connect_timeout 30s;\ + proxy_send_timeout 30s;\ + proxy_read_timeout 30s;\ + }\ +\ + # Management API - port 3002 with management header\ + location /api/ {\ + proxy_pass http://127.0.0.1:3002;\ + proxy_http_version 1.1;\ + proxy_set_header Upgrade $http_upgrade;\ + proxy_set_header Connection '\''upgrade'\'';\ + proxy_set_header Host $host;\ + proxy_set_header X-Real-IP $remote_addr;\ + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\ + proxy_set_header X-Forwarded-Proto $scheme;\ + proxy_set_header X-Management-Portal "true";\ + proxy_cache_bypass $http_upgrade;\ + proxy_connect_timeout 30s;\ + proxy_send_timeout 30s;\ + proxy_read_timeout 30s;\ + }\ +}\ +\ +' "$NGINX_CONFIG" + + log "Management subdomain configuration added" +else + log "Management subdomain configuration already exists" +fi + +# Test and reload nginx +if nginx -t; then + log "✅ Nginx configuration is valid" + systemctl reload nginx + log "✅ Nginx reloaded" + log "🌐 Management portal API should now work at: https://management.$DOMAIN/api/" +else + log "❌ Nginx configuration error - restoring backup" + cp "${NGINX_CONFIG}.backup" "$NGINX_CONFIG" + exit 1 +fi diff --git a/server/.env.example b/server/.env.example index 9a8b32e..6c083a7 100644 --- a/server/.env.example +++ b/server/.env.example @@ -12,6 +12,9 @@ DB_PASSWORD=password # JWT Secret JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +# Management Portal JWT Secret (completely separate from tenant auth) +MANAGEMENT_JWT_SECRET=management-super-secret-key-change-in-production + # Twilio Configuration (for SMS alerts) TWILIO_ACCOUNT_SID=your-twilio-account-sid TWILIO_AUTH_TOKEN=your-twilio-auth-token diff --git a/server/routes/index.js b/server/routes/index.js index c381c28..6a5646b 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); // Import route modules +const managementRoutes = require('./management'); const deviceRoutes = require('./device'); const userRoutes = require('./user'); const alertRoutes = require('./alert'); @@ -13,6 +14,9 @@ const detectorsRoutes = require('./detectors'); const detectionsRoutes = require('./detections'); const droneTypesRoutes = require('./droneTypes'); +// Management portal routes (before API versioning) +router.use('/management', managementRoutes); + // API versioning router.use('/v1/devices', deviceRoutes); router.use('/v1/users', userRoutes); diff --git a/server/routes/management.js b/server/routes/management.js new file mode 100644 index 0000000..787c9c7 --- /dev/null +++ b/server/routes/management.js @@ -0,0 +1,469 @@ +/** + * 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('bcrypt'); +const { Tenant, User } = 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; + + // Hardcoded management users for now (should be in separate DB table) + const MANAGEMENT_USERS = { + 'admin': { + password: await bcrypt.hash('admin123', 10), // Change this! + role: 'super_admin' + }, + 'platform_admin': { + password: await bcrypt.hash('platform123', 10), // Change this! + role: 'platform_admin' + } + }; + + const managementUser = MANAGEMENT_USERS[username]; + if (!managementUser || !await bcrypt.compare(password, managementUser.password)) { + 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: username, + username: username, + role: managementUser.role, + isManagement: true + }, MANAGEMENT_SECRET, { expiresIn: '8h' }); + + res.json({ + success: true, + token, + user: { + username, + role: managementUser.role + } + }); + } catch (error) { + res.status(500).json({ + success: false, + message: 'Management authentication error', + error: error.message + }); + } +}); + +// Apply management authentication to all other routes +router.use(requireManagementAuth); + +// Security audit logging for all management operations +router.use((req, res, next) => { + const auditLog = { + timestamp: new Date().toISOString(), + user: req.managementUser.username, + role: req.managementUser.role, + method: req.method, + path: req.path, + ip: req.ip, + userAgent: req.headers['user-agent'] + }; + + console.log('[MANAGEMENT AUDIT]', JSON.stringify(auditLog)); + next(); +}); + +/** + * GET /api/management/system-info - Platform system information + */ +router.get('/system-info', async (req, res) => { + try { + const tenantCount = await Tenant.count(); + const userCount = await User.count(); + + res.json({ + success: true, + data: { + platform: { + name: 'UAMILS Platform', + version: '1.0.0', + environment: process.env.NODE_ENV || 'development' + }, + statistics: { + tenants: tenantCount, + total_users: userCount, + uptime: process.uptime() + }, + security: { + management_access_level: req.managementUser.role, + last_backup: process.env.LAST_BACKUP_DATE || 'Not configured' + } + } + }); + } catch (error) { + console.error('Management: Error fetching system info:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch system information', + error: error.message + }); + } +}); + +/** + * GET /api/management/tenants - List all tenants with admin details + */ +router.get('/tenants', async (req, res) => { + try { + const { limit = 50, offset = 0, search, auth_provider } = req.query; + + const whereClause = {}; + if (search) { + whereClause[Op.or] = [ + { name: { [Op.iLike]: `%${search}%` } }, + { slug: { [Op.iLike]: `%${search}%` } }, + { domain: { [Op.iLike]: `%${search}%` } } + ]; + } + if (auth_provider) { + whereClause.auth_provider = auth_provider; + } + + const tenants = await Tenant.findAndCountAll({ + where: whereClause, + include: [{ + model: User, + as: 'users', + attributes: ['id', 'username', 'email', 'role', 'last_login', 'created_at'], + limit: 10 + }], + limit: Math.min(parseInt(limit), 100), + offset: parseInt(offset), + order: [['created_at', 'DESC']] + }); + + res.json({ + success: true, + data: tenants.rows, + pagination: { + total: tenants.count, + limit: parseInt(limit), + offset: parseInt(offset), + pages: Math.ceil(tenants.count / parseInt(limit)) + } + }); + + } catch (error) { + console.error('Management: Error fetching tenants:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch tenants' + }); + } +}); + +/** + * POST /api/management/tenants - Create new tenant + */ +router.post('/tenants', async (req, res) => { + try { + const tenantData = req.body; + + // Enhanced validation for management portal + if (!tenantData.name || !tenantData.slug) { + return res.status(400).json({ + success: false, + message: 'Name and slug are required' + }); + } + + // 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' + }); + } + + // Log management action + console.log(`Management: Admin ${req.user.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' + }); + } + + // Log management action + console.log(`Management: Admin ${req.user.username} updating tenant: ${tenant.name}`); + + await tenant.update(req.body); + + 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.user.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' + }); + } +}); + +module.exports = router;