/** * Tenant Self-Management Routes * Allows tenant admins to manage their own tenant settings */ const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const router = express.Router(); const { Tenant, User } = require('../models'); const { authenticateToken } = require('../middleware/auth'); const { requirePermissions, requireAnyPermission, hasPermission } = require('../middleware/rbac'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); const { securityLogger } = require('../middleware/logger'); const { enforceUserLimit, enforceDeviceLimit, enforceApiRateLimit, getTenantLimitsStatus } = require('../middleware/tenant-limits'); // Initialize multi-tenant auth const multiAuth = new MultiTenantAuth(); // Initialize SecurityLogger with models const models = require('../models'); securityLogger.setModels(models); // Configure multer for logo uploads const storage = multer.diskStorage({ destination: function (req, file, cb) { const uploadDir = path.join(__dirname, '../uploads/logos'); // Create directory if it doesn't exist if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req, file, cb) { // Use tenant ID and timestamp for unique filename const tenantId = req.user.tenant_id; const ext = path.extname(file.originalname); const filename = `tenant-${tenantId}-${Date.now()}${ext}`; cb(null, filename); } }); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 // 5MB limit }, fileFilter: function (req, file, cb) { // Accept only image files if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed'), false); } } }); /** * GET /tenant/limits * Get current tenant limits and usage */ router.get('/limits', authenticateToken, requirePermissions(['tenant.view']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const limitsStatus = await getTenantLimitsStatus(tenant.id); res.json({ success: true, data: limitsStatus }); } catch (error) { console.error('Error fetching tenant limits:', error); res.status(500).json({ success: false, message: 'Failed to fetch tenant limits' }); } }); /** * GET /tenant/info * Get current tenant information */ router.get('/info', authenticateToken, requirePermissions(['tenant.view']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Return tenant info (excluding sensitive data) const tenantInfo = { id: tenant.id, name: tenant.name, slug: tenant.slug, domain: tenant.domain, subscription_type: tenant.subscription_type, is_active: tenant.is_active, auth_provider: tenant.auth_provider, branding: tenant.branding, features: tenant.features, admin_email: tenant.admin_email, admin_phone: tenant.admin_phone, billing_email: tenant.billing_email, created_at: tenant.created_at, // IP restriction info (for security admins only) ip_restriction_enabled: hasPermission(req.user.role, 'security.view') ? tenant.ip_restriction_enabled : undefined, ip_whitelist: hasPermission(req.user.role, 'security.view') ? tenant.ip_whitelist : undefined, ip_restriction_message: hasPermission(req.user.role, 'security.view') ? tenant.ip_restriction_message : undefined }; res.json({ success: true, data: tenantInfo }); } catch (error) { console.error('Error fetching tenant info:', error); res.status(500).json({ success: false, message: 'Failed to fetch tenant information' }); } }); /** * Validates user has access to tenant and logs the validation */ async function validateTenantAccess(req, res, action) { try { // Validate user is authenticated if (!req.user || !req.user.id) { securityLogger.logSecurityEvent('error', 'Tenant logo access attempt without valid user authentication', { action, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: false, message: 'Authentication required', status: 401 }; } // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { securityLogger.logSecurityEvent('error', `Failed to determine tenant for ${action}`, { action, userId: req.user.id, username: req.user.username, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: false, message: 'Invalid tenant', status: 400 }; } // Get tenant by slug const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { securityLogger.logSecurityEvent('error', `Tenant not found for ${action}`, { action, tenantSlug: tenantId, userId: req.user.id, username: req.user.username, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: false, message: 'Tenant not found', status: 404 }; } // Validate user belongs to this tenant if (req.user.tenant_id !== tenant.id) { securityLogger.logSecurityEvent('warning', `User attempted ${action} on different tenant`, { action, userId: req.user.id, username: req.user.username, userTenantId: req.user.tenant_id, requestedTenantId: tenant.id, tenantSlug: tenantId, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: false, message: 'Access denied: User not member of tenant', status: 403 }; } securityLogger.logSecurityEvent('info', `${action} access validated`, { action, userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenantId, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: true, tenant }; } catch (error) { securityLogger.logSecurityEvent('error', `Error validating tenant access for ${action}`, { action, error: error.message, userId: req.user?.id, username: req.user?.username, ip: req.ip, userAgent: req.get('User-Agent'), path: req.path }); return { success: false, message: 'Internal server error', status: 500 }; } } /** * POST /tenant/logo-upload * Upload tenant logo (branding admin or higher) */ router.post('/logo-upload', authenticateToken, requirePermissions(['branding.edit']), upload.single('logo'), async (req, res) => { try { if (!req.file) { securityLogger.logSecurityEvent('warning', 'Logo upload attempted without file', { action: 'logo_upload', userId: req.user?.id, username: req.user?.username, ip: req.ip, userAgent: req.get('User-Agent') }); return res.status(400).json({ success: false, message: 'No file uploaded' }); } // Enhanced security validation const validation = await validateTenantAccess(req, res, 'logo_upload'); if (!validation.success) { // Clean up uploaded file on validation failure if (fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } return res.status(validation.status).json({ success: false, message: validation.message }); } const { tenant } = validation; // Clean up old logo file if it exists if (tenant.branding?.logo_url) { const oldLogoPath = tenant.branding.logo_url.replace('/uploads/logos/', ''); const oldFilePath = path.join(__dirname, '../uploads/logos', oldLogoPath); if (fs.existsSync(oldFilePath)) { fs.unlinkSync(oldFilePath); securityLogger.logSecurityEvent('info', 'Old logo file cleaned up', { action: 'logo_upload', oldLogoPath, userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenant.slug }); } } // Create logo URL const logoUrl = `/uploads/logos/${req.file.filename}`; // Update tenant branding with new logo const updatedBranding = { ...tenant.branding, logo_url: logoUrl }; await tenant.update({ branding: updatedBranding }); // Audit log successful upload securityLogger.logSecurityEvent('info', 'Tenant logo uploaded successfully', { action: 'logo_upload', userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenant.slug, fileName: req.file.filename, fileSize: req.file.size, logoUrl, ip: req.ip, userAgent: req.get('User-Agent') }); console.log(`✅ Tenant "${tenant.slug}" logo uploaded by user "${req.user.username}"`); res.json({ success: true, message: 'Logo uploaded successfully', data: { logo_url: logoUrl, branding: updatedBranding } }); } catch (error) { console.error('Error uploading logo:', error); // Audit log the error securityLogger.logSecurityEvent('error', 'Logo upload failed with error', { action: 'logo_upload', error: error.message, userId: req.user?.id, username: req.user?.username, fileName: req.file?.filename, ip: req.ip, userAgent: req.get('User-Agent') }); // Clean up uploaded file on error if (req.file && fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } res.status(500).json({ success: false, message: 'Failed to upload logo' }); } }); /** * DELETE /tenant/logo * Remove tenant logo (branding admin or higher) */ router.delete('/logo', authenticateToken, requirePermissions(['branding.edit']), async (req, res) => { try { // Enhanced security validation const validation = await validateTenantAccess(req, res, 'logo_removal'); if (!validation.success) { return res.status(validation.status).json({ success: false, message: validation.message }); } const { tenant } = validation; // Check if tenant has a logo to remove if (!tenant.branding?.logo_url) { securityLogger.logSecurityEvent('warning', 'Logo removal attempted when no logo exists', { action: 'logo_removal', userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenant.slug, ip: req.ip, userAgent: req.get('User-Agent') }); return res.status(400).json({ success: false, message: 'No logo to remove' }); } const oldLogoUrl = tenant.branding.logo_url; // Delete the physical logo file const logoPath = tenant.branding.logo_url.replace('/uploads/logos/', ''); const filePath = path.join(__dirname, '../uploads/logos', logoPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); securityLogger.logSecurityEvent('info', 'Logo file deleted from filesystem', { action: 'logo_removal', logoPath, userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenant.slug }); } // Update tenant branding to remove logo const updatedBranding = { ...tenant.branding, logo_url: null }; await tenant.update({ branding: updatedBranding }); // Audit log successful removal securityLogger.logSecurityEvent('info', 'Tenant logo removed successfully', { action: 'logo_removal', userId: req.user.id, username: req.user.username, tenantId: tenant.id, tenantSlug: tenant.slug, removedLogoUrl: oldLogoUrl, ip: req.ip, userAgent: req.get('User-Agent') }); console.log(`✅ Tenant "${tenant.slug}" logo removed by user "${req.user.username}"`); res.json({ success: true, message: 'Logo removed successfully', data: { branding: updatedBranding } }); } catch (error) { console.error('Error removing logo:', error); // Audit log the error securityLogger.logSecurityEvent('error', 'Logo removal failed with error', { action: 'logo_removal', error: error.message, userId: req.user?.id, username: req.user?.username, ip: req.ip, userAgent: req.get('User-Agent') }); res.status(500).json({ success: false, message: 'Failed to remove logo' }); } }); /** * PUT /tenant/branding * Update tenant branding (branding admin or higher) */ router.put('/branding', authenticateToken, requirePermissions(['branding.edit']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const { logo_url, primary_color, secondary_color, company_name } = req.body; // Validate colors const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; if (primary_color && !colorRegex.test(primary_color)) { return res.status(400).json({ success: false, message: 'Invalid primary color format' }); } if (secondary_color && !colorRegex.test(secondary_color)) { return res.status(400).json({ success: false, message: 'Invalid secondary color format' }); } // Update branding const updatedBranding = { ...tenant.branding, logo_url: logo_url || tenant.branding?.logo_url || '', primary_color: primary_color || tenant.branding?.primary_color || '#3B82F6', secondary_color: secondary_color || tenant.branding?.secondary_color || '#1F2937', company_name: company_name || tenant.branding?.company_name || '' }; await tenant.update({ branding: updatedBranding }); console.log(`✅ Tenant "${tenantId}" branding updated by user "${req.user.username}"`); res.json({ success: true, message: 'Branding updated successfully', data: { branding: updatedBranding } }); } catch (error) { console.error('Error updating tenant branding:', error); res.status(500).json({ success: false, message: 'Failed to update branding' }); } }); /** * PUT /tenant/security * Update tenant security settings (security admin or higher) */ router.put('/security', authenticateToken, requirePermissions(['security.edit']), async (req, res) => { try { console.log('🔒 Security settings update request:', req.body); // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); console.log('🔍 Security update - tenant:', tenantId); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } console.log('🔍 Current tenant settings:', { ip_restriction_enabled: tenant.ip_restriction_enabled, ip_whitelist: tenant.ip_whitelist, ip_restriction_message: tenant.ip_restriction_message }); const { ip_restriction_enabled, ip_whitelist, ip_restriction_message } = req.body; // Validate IP whitelist if provided if (ip_whitelist && Array.isArray(ip_whitelist)) { for (const ip of ip_whitelist) { // Basic IP validation (could be enhanced) if (typeof ip !== 'string' || ip.trim().length === 0) { return res.status(400).json({ success: false, message: 'Invalid IP address in whitelist' }); } } } // Update security settings const updates = {}; if (typeof ip_restriction_enabled === 'boolean') { updates.ip_restriction_enabled = ip_restriction_enabled; console.log('🔧 Setting ip_restriction_enabled to:', ip_restriction_enabled); } if (ip_whitelist) { updates.ip_whitelist = ip_whitelist; console.log('🔧 Setting ip_whitelist to:', ip_whitelist); } if (ip_restriction_message) { updates.ip_restriction_message = ip_restriction_message; console.log('🔧 Setting ip_restriction_message to:', ip_restriction_message); } console.log('🔧 Final updates object:', updates); await tenant.update(updates); await tenant.reload(); // Refresh the tenant object console.log('🔧 After update - tenant settings:', { ip_restriction_enabled: tenant.ip_restriction_enabled, ip_whitelist: tenant.ip_whitelist, ip_restriction_message: tenant.ip_restriction_message }); console.log(`✅ Tenant "${tenantId}" security settings updated by user "${req.user.username}"`); res.json({ success: true, message: 'Security settings updated successfully', data: { ip_restriction_enabled: tenant.ip_restriction_enabled, ip_whitelist: tenant.ip_whitelist, ip_restriction_message: tenant.ip_restriction_message } }); } catch (error) { console.error('Error updating tenant security:', error); res.status(500).json({ success: false, message: 'Failed to update security settings' }); } }); /** * GET /tenant/users * Get users in current tenant (user admin or higher) */ router.get('/users', authenticateToken, requirePermissions(['users.view']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Get users in this tenant const users = await User.findAll({ where: { tenant_id: tenant.id }, attributes: ['id', 'username', 'email', 'role', 'is_active', 'last_login', 'created_at'], order: [['created_at', 'DESC']] }); res.json({ success: true, data: users }); } catch (error) { console.error('Error fetching tenant users:', error); res.status(500).json({ success: false, message: 'Failed to fetch users' }); } }); /** * POST /tenant/users * Create a new user in current tenant (user admin or higher, local auth only) */ router.post('/users', authenticateToken, requirePermissions(['users.create']), enforceUserLimit(), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant uses local authentication if (tenant.auth_provider !== 'local') { return res.status(400).json({ success: false, message: `User creation is only available for local authentication. This tenant uses ${tenant.auth_provider}.` }); } const { username, email, password, role = 'viewer', is_active = true } = req.body; // Validate required fields if (!username || !email || !password) { return res.status(400).json({ success: false, message: 'Username, email, and password are required' }); } // Validate role const validRoles = ['admin', 'operator', 'viewer']; if (!validRoles.includes(role)) { return res.status(400).json({ success: false, message: 'Invalid role. Must be admin, operator, or viewer' }); } // Check if username or email already exists in this tenant const existingUser = await User.findOne({ where: { tenant_id: tenant.id, [require('sequelize').Op.or]: [ { username }, { email } ] } }); if (existingUser) { return res.status(400).json({ success: false, message: 'Username or email already exists in this tenant' }); } // Create user const user = await User.create({ username, email, password_hash: password, // Will be hashed by the model role, is_active, tenant_id: tenant.id, created_by: req.user.id }); console.log(`✅ User "${username}" created in tenant "${tenantId}" by admin "${req.user.username}"`); // Return user data (without password) const userData = { id: user.id, username: user.username, email: user.email, role: user.role, is_active: user.is_active, created_at: user.created_at }; res.status(201).json({ success: true, message: 'User created successfully', data: userData }); } catch (error) { console.error('Error creating tenant user:', error); res.status(500).json({ success: false, message: 'Failed to create user' }); } }); /** * PUT /tenant/users/:userId/status * Update user status (activate/deactivate) (user admin or higher, local auth only) */ router.put('/users/:userId/status', authenticateToken, requirePermissions(['users.edit']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant uses local authentication if (tenant.auth_provider !== 'local') { return res.status(400).json({ success: false, message: `User management is only available for local authentication. This tenant uses ${tenant.auth_provider}.` }); } const { userId } = req.params; const { is_active } = req.body; if (typeof is_active !== 'boolean') { return res.status(400).json({ success: false, message: 'is_active must be a boolean value' }); } // Find user in this tenant const user = await User.findOne({ where: { id: userId, tenant_id: tenant.id } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Prevent self-deactivation if (user.id === req.user.id && !is_active) { return res.status(400).json({ success: false, message: 'You cannot deactivate your own account' }); } // Update user status await user.update({ is_active }); console.log(`✅ User "${user.username}" ${is_active ? 'activated' : 'deactivated'} in tenant "${tenantId}" by admin "${req.user.username}"`); res.json({ success: true, message: `User ${is_active ? 'activated' : 'deactivated'} successfully`, data: { id: user.id, username: user.username, is_active: user.is_active } }); } catch (error) { console.error('Error updating user status:', error); res.status(500).json({ success: false, message: 'Failed to update user status' }); } }); /** * PUT /tenant/users/:userId * Update user details (user admin or higher, local auth only) */ router.put('/users/:userId', authenticateToken, requirePermissions(['users.edit']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant uses local authentication if (tenant.auth_provider !== 'local') { return res.status(400).json({ success: false, message: `User management is only available for local authentication. This tenant uses ${tenant.auth_provider}.` }); } const { userId } = req.params; const { email, first_name, last_name, phone, role, password } = req.body; // Find user in this tenant const user = await User.findOne({ where: { id: userId, tenant_id: tenant.id } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Prepare update data const updateData = {}; if (email !== undefined) updateData.email = email; if (first_name !== undefined) updateData.first_name = first_name; if (last_name !== undefined) updateData.last_name = last_name; if (phone !== undefined) updateData.phone = phone; // Role update with permission check if (role !== undefined) { // Check if current user has permission to assign this role if (!hasPermission(req.user.role, 'users.edit')) { return res.status(403).json({ success: false, message: 'Insufficient permissions to change user roles' }); } updateData.role = role; } // Password update if (password && password.trim()) { const bcrypt = require('bcryptjs'); updateData.password_hash = await bcrypt.hash(password.trim(), 10); } // Update user await user.update(updateData); console.log(`✅ User "${user.username}" updated in tenant "${tenantId}" by admin "${req.user.username}"`); // Return updated user data (without password) const userData = { id: user.id, username: user.username, email: user.email, first_name: user.first_name, last_name: user.last_name, phone: user.phone, role: user.role, is_active: user.is_active, created_at: user.created_at, updated_at: user.updated_at }; res.json({ success: true, message: 'User updated successfully', data: userData }); } catch (error) { console.error('Error updating user:', error); res.status(500).json({ success: false, message: 'Failed to update user' }); } }); /** * DELETE /tenant/users/:userId * Delete a user permanently (user admin or higher, local auth only) */ router.delete('/users/:userId', authenticateToken, requirePermissions(['users.delete']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant uses local authentication if (tenant.auth_provider !== 'local') { return res.status(400).json({ success: false, message: `User deletion is only available for local authentication. This tenant uses ${tenant.auth_provider}.` }); } const { userId } = req.params; // Find user in this tenant const user = await User.findOne({ where: { id: userId, tenant_id: tenant.id } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Prevent self-deletion if (user.id === req.user.id) { return res.status(400).json({ success: false, message: 'Cannot delete your own account' }); } // Store user info for logging before deletion const userInfo = { id: user.id, username: user.username, email: user.email }; // Delete the user await user.destroy(); console.log(`🗑️ User "${userInfo.username}" (${userInfo.email}) deleted from tenant "${tenantId}" by admin "${req.user.username}"`); res.json({ success: true, message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ success: false, message: 'Failed to delete user' }); } }); /** * DELETE /tenant/users/:userId * Delete user permanently (user admin or higher, local auth only) */ router.delete('/users/:userId', authenticateToken, requirePermissions(['users.delete']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant uses local authentication if (tenant.auth_provider !== 'local') { return res.status(400).json({ success: false, message: `User management is only available for local authentication. This tenant uses ${tenant.auth_provider}.` }); } const { userId } = req.params; // Find user in this tenant const user = await User.findOne({ where: { id: userId, tenant_id: tenant.id } }); if (!user) { return res.status(404).json({ success: false, message: 'User not found in this tenant' }); } // Prevent self-deletion if (user.id === req.user.id) { return res.status(400).json({ success: false, message: 'Cannot delete your own user account' }); } // Store username for logging before deletion const deletedUsername = user.username; // Delete the user permanently await user.destroy(); console.log(`⚠️ User "${deletedUsername}" permanently deleted from tenant "${tenantId}" by admin "${req.user.username}"`); res.json({ success: true, message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ success: false, message: 'Failed to delete user' }); } }); /** * GET /tenant/auth * Get authentication configuration (auth admins or higher) */ router.get('/auth', authenticateToken, requirePermissions(['auth.view']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Return auth configuration (excluding sensitive credentials) const authConfig = { auth_provider: tenant.auth_provider, auth_config: tenant.auth_config ? { ...tenant.auth_config, // Hide sensitive fields client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined, bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined, service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined, certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined } : {}, session_timeout: tenant.session_timeout || 480, require_mfa: tenant.require_mfa || false, allow_concurrent_sessions: tenant.allow_concurrent_sessions !== false }; console.log(`✅ Auth config retrieved for tenant "${tenantId}" by "${req.user.username}"`); res.json({ success: true, data: authConfig }); } catch (error) { console.error('Error retrieving auth config:', error); res.status(500).json({ success: false, message: 'Failed to retrieve authentication configuration' }); } }); /** * PUT /tenant/auth * Update authentication configuration (auth admins or higher) */ router.put('/auth', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const { auth_provider, auth_config, session_timeout, require_mfa, allow_concurrent_sessions, role_mappings } = req.body; // Validate auth provider const validProviders = ['local', 'saml', 'oauth', 'ldap', 'ad']; if (auth_provider && !validProviders.includes(auth_provider)) { return res.status(400).json({ success: false, message: 'Invalid authentication provider' }); } // Validate session timeout if (session_timeout && (session_timeout < 15 || session_timeout > 1440)) { return res.status(400).json({ success: false, message: 'Session timeout must be between 15 and 1440 minutes' }); } // Prepare update data const updateData = {}; if (auth_provider) updateData.auth_provider = auth_provider; if (auth_config) { // Merge with existing config, preserving hidden sensitive fields const existingConfig = tenant.auth_config || {}; updateData.auth_config = { ...existingConfig, ...auth_config, // Restore hidden fields if they weren't changed client_secret: auth_config.client_secret === '***HIDDEN***' ? existingConfig.client_secret : auth_config.client_secret, bind_password: auth_config.bind_password === '***HIDDEN***' ? existingConfig.bind_password : auth_config.bind_password, service_password: auth_config.service_password === '***HIDDEN***' ? existingConfig.service_password : auth_config.service_password, certificate: auth_config.certificate === '***HIDDEN***' ? existingConfig.certificate : auth_config.certificate }; } if (session_timeout !== undefined) updateData.session_timeout = session_timeout; if (require_mfa !== undefined) updateData.require_mfa = require_mfa; if (allow_concurrent_sessions !== undefined) updateData.allow_concurrent_sessions = allow_concurrent_sessions; if (role_mappings) updateData.role_mappings = role_mappings; // Update tenant await tenant.update(updateData); console.log(`✅ Auth config updated for tenant "${tenantId}" by admin "${req.user.username}"`); // Return updated config (with hidden sensitive fields) const updatedConfig = { auth_provider: tenant.auth_provider, auth_config: tenant.auth_config ? { ...tenant.auth_config, client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined, bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined, service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined, certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined } : {}, session_timeout: tenant.session_timeout, require_mfa: tenant.require_mfa, allow_concurrent_sessions: tenant.allow_concurrent_sessions, role_mappings: tenant.role_mappings }; res.json({ success: true, message: 'Authentication configuration updated successfully', data: updatedConfig }); } catch (error) { console.error('Error updating auth config:', error); res.status(500).json({ success: false, message: 'Failed to update authentication configuration' }); } }); /** * POST /tenant/auth/test * Test authentication configuration (auth admins or higher) */ router.post('/auth/test', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => { try { // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } const { test_username, test_password } = req.body; // Simulate authentication test based on provider const authProvider = tenant.auth_provider; let testResult = { success: false, message: 'Authentication test not implemented for this provider', details: {} }; switch (authProvider) { case 'local': testResult = { success: true, message: 'Local authentication is always available', details: { provider: 'local' } }; break; case 'saml': // In real implementation, this would test SAML SSO endpoint testResult = { success: true, message: 'SAML configuration appears valid (test connection would be performed in production)', details: { provider: 'saml', sso_url: tenant.auth_config?.sso_url, entity_id: tenant.auth_config?.entity_id } }; break; case 'oauth': // In real implementation, this would test OAuth endpoint testResult = { success: true, message: 'OAuth configuration appears valid (test connection would be performed in production)', details: { provider: 'oauth', oauth_provider: tenant.auth_config?.oauth_provider, client_id: tenant.auth_config?.client_id } }; break; case 'ldap': case 'ad': // In real implementation, this would test LDAP/AD connection testResult = { success: true, message: `${authProvider.toUpperCase()} configuration appears valid (test connection would be performed in production)`, details: { provider: authProvider, server: tenant.auth_config?.ldap_server || tenant.auth_config?.domain_controller } }; break; } console.log(`✅ Auth test performed for tenant "${tenantId}" by admin "${req.user.username}"`); res.json({ success: true, message: 'Authentication test completed', data: testResult }); } catch (error) { console.error('Error testing auth config:', error); res.status(500).json({ success: false, message: 'Failed to test authentication configuration' }); } }); module.exports = router;