import { Response } from 'express'; import { Tenant } from '../models/tenant.model'; import { User } from '../models/user.model'; import { AuthenticatedRequest } from '../types/auth'; import { EvaluationJob, EvaluationResult } from '../models/evaluation'; import { CreditOperation } from '../models/creditOperation.model'; import mongoose from 'mongoose'; import elkService from '../services/elkService'; export const getCurrentTenant = async (req: AuthenticatedRequest, res: Response) => { try { // The tenant middleware ensures req.user exists and req.tenantId is set const userId = req.user!.id; const tenantId = req.tenantId; // Log tenant fetch attempt elkService.logBusinessEvent('tenant_fetch_requested', { tenantId, requestedBy: userId }, userId, tenantId); // Find the tenant directly using the tenantId from middleware const tenant = await Tenant.findById(tenantId); if (!tenant) { elkService.logBusinessEvent('tenant_fetch_failed', { tenantId, requestedBy: userId, reason: 'tenant_not_found' }, userId, tenantId); return res.status(404).json({ message: 'Tenant not found' }); } // Log successful tenant fetch elkService.logBusinessEvent('tenant_fetched', { tenantId, tenantName: tenant.name, requestedBy: userId }, userId, tenantId); res.json(tenant); } catch (error) { console.error('Error fetching tenant:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenant_fetch', { tenantId: req.tenantId, userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; export const updateTenant = async (req: AuthenticatedRequest, res: Response) => { try { // The tenant middleware ensures req.user exists and req.tenantId is set const tenantId = req.tenantId; const userId = req.user!.id; // Only allow tenant_admin or superuser to update tenant if (!['tenant_admin', 'superuser'].includes(req.user!.role)) { elkService.logBusinessEvent('tenant_update_failed', { tenantId, updatedBy: userId, reason: 'insufficient_permissions', userRole: req.user!.role }, userId, tenantId); return res.status(403).json({ message: 'Forbidden' }); } // Log tenant update attempt elkService.logBusinessEvent('tenant_update_attempted', { tenantId, updatedBy: userId, updateData: req.body }, userId, tenantId); const updatedTenant = await Tenant.findByIdAndUpdate( tenantId, { $set: req.body }, { new: true, runValidators: true } ); if (!updatedTenant) { elkService.logBusinessEvent('tenant_update_failed', { tenantId, updatedBy: userId, reason: 'tenant_not_found' }, userId, tenantId); return res.status(404).json({ message: 'Tenant not found' }); } // Log successful tenant update elkService.logBusinessEvent('tenant_updated', { tenantId, tenantName: updatedTenant.name, updatedBy: userId, updatedFields: Object.keys(req.body) }, userId, tenantId); res.json(updatedTenant); } catch (error) { console.error('Error updating tenant:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenant_update', { tenantId: req.tenantId, userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; // Super Admin Tenant Management export const getAllTenants = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } const userId = req.user!.id; // Log tenants list request elkService.logBusinessEvent('tenants_list_requested', { requestedBy: userId }, userId); const tenants = await Tenant.find({}); // Log successful tenants list fetch elkService.logBusinessEvent('tenants_list_fetched', { requestedBy: userId, tenantsCount: tenants.length }, userId); res.json(tenants); } catch (error) { console.error('Error fetching tenants:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenants_list_fetch', { userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; export const getTenantById = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } const tenantId = req.params.id; const userId = req.user!.id; // Log tenant fetch by ID request elkService.logBusinessEvent('tenant_by_id_requested', { tenantId, requestedBy: userId }, userId, tenantId); const tenant = await Tenant.findById(req.params.id); if (!tenant) { elkService.logBusinessEvent('tenant_by_id_failed', { tenantId, requestedBy: userId, reason: 'tenant_not_found' }, userId, tenantId); return res.status(404).json({ message: 'Tenant not found' }); } // Log successful tenant fetch by ID elkService.logBusinessEvent('tenant_by_id_fetched', { tenantId, tenantName: tenant.name, requestedBy: userId }, userId, tenantId); res.json(tenant); } catch (error) { console.error('Error fetching tenant:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenant_by_id_fetch', { tenantId: req.params.id, userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; export const createTenant = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } const { name, settings, usageStats, creditBalance } = req.body; const userId = req.user!.id; // Log tenant creation attempt elkService.logBusinessEvent('tenant_creation_attempted', { tenantName: name, createdBy: userId, tenantData: { name, settings: !!settings, usageStats: !!usageStats, creditBalance: !!creditBalance } }, userId); const newTenant = new Tenant({ name, isActive: false, // Tenant requires superadmin activation settings: settings || {}, usageStats: { evaluationsRemaining: usageStats?.evaluationsRemaining || 100, evaluationsUsed: usageStats?.evaluationsUsed || 0, lastEvaluationDate: usageStats?.lastEvaluationDate || null }, creditBalance: { availableCredits: creditBalance?.availableCredits ?? 100, totalCreditsUsed: creditBalance?.totalCreditsUsed ?? 0, lastCreditOperation: null } }); const savedTenant = await newTenant.save(); // Log successful tenant creation elkService.logBusinessEvent('tenant_created', { tenantId: savedTenant._id, tenantName: savedTenant.name, createdBy: userId }, userId, savedTenant._id.toString()); res.status(201).json(savedTenant); } catch (error) { console.error('Error creating tenant:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenant_creation', { userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; export const updateTenantById = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } const updatedTenant = await Tenant.findByIdAndUpdate( req.params.id, { $set: req.body }, { new: true, runValidators: true } ); if (!updatedTenant) { return res.status(404).json({ message: 'Tenant not found' }); } res.json(updatedTenant); } catch (error) { console.error('Error updating tenant:', error); res.status(500).json({ message: 'Internal server error' }); } }; export const deleteTenant = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } const tenantId = req.params.id; const userId = req.user!.id; // Log tenant deletion attempt elkService.logBusinessEvent('tenant_deletion_attempted', { tenantId, deletedBy: userId }, userId, tenantId); // Find the tenant const tenant = await Tenant.findById(req.params.id); if (!tenant) { elkService.logBusinessEvent('tenant_deletion_failed', { tenantId, deletedBy: userId, reason: 'tenant_not_found' }, userId, tenantId); return res.status(404).json({ message: 'Tenant not found' }); } // Check if there are users associated with this tenant const usersCount = await User.countDocuments({ tenant: req.params.id }); if (usersCount > 0) { elkService.logBusinessEvent('tenant_deletion_failed', { tenantId, deletedBy: userId, reason: 'has_associated_users', usersCount }, userId, tenantId); return res.status(400).json({ message: 'Cannot delete tenant with associated users. Please delete or reassign users first.' }); } // Delete the tenant await Tenant.findByIdAndDelete(req.params.id); // Log successful tenant deletion elkService.logBusinessEvent('tenant_deleted', { tenantId, tenantName: tenant.name, deletedBy: userId }, userId, tenantId); res.status(200).json({ message: 'Tenant deleted successfully' }); } catch (error) { console.error('Error deleting tenant:', error); elkService.logSystemError( error instanceof Error ? error : new Error('Unknown error'), 'tenant_deletion', { tenantId: req.params.id, userId: req.user?.id } ); res.status(500).json({ message: 'Internal server error' }); } }; export const getTenantUsers = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } // Find the tenant const tenant = await Tenant.findById(id); if (!tenant) { return res.status(404).json({ message: 'Tenant not found' }); } // Get all users for this tenant const users = await User.find({ tenant: id }).select('-password'); res.json(users); } catch (error) { console.error('Error fetching tenant users:', error); res.status(500).json({ message: 'Internal server error' }); } }; // Superadmin function to activate/deactivate tenants export const superAdminToggleTenantStatus = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; // Only superusers can use this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Access denied. Superuser role required.' }); } // Log superadmin tenant status toggle attempt elkService.logBusinessEvent('superadmin_tenant_status_toggle_attempted', { toggledBy: req.user!.id, targetTenantId: id }, req.user!.id, 'system'); // Find tenant const tenant = await Tenant.findById(id); if (!tenant) { elkService.logBusinessEvent('superadmin_tenant_status_toggle_failed', { toggledBy: req.user!.id, targetTenantId: id, reason: 'tenant_not_found' }, req.user!.id, 'system'); return res.status(404).json({ message: 'Tenant not found' }); } // Store original status for logging const originalStatus = tenant.isActive; // Toggle status tenant.isActive = !tenant.isActive; await tenant.save(); // Log successful status toggle elkService.logBusinessEvent('superadmin_tenant_status_toggled', { toggledBy: req.user!.id, targetTenantId: id, originalStatus, newStatus: tenant.isActive }, req.user!.id, 'system'); res.json({ id: tenant._id, isActive: tenant.isActive, message: tenant.isActive ? 'Tenant activated successfully' : 'Tenant deactivated successfully' }); } catch (error) { console.error('Error toggling tenant status:', error); elkService.logBusinessEvent('superadmin_tenant_status_toggle_failed', { toggledBy: req.user!.id, targetTenantId: req.params.id, reason: 'server_error', error: error instanceof Error ? error.message : 'Unknown error' }, req.user!.id, 'system'); res.status(500).json({ message: 'Internal server error' }); } }; export const getTenantStatistics = async (req: AuthenticatedRequest, res: Response) => { try { // Only superusers can access this endpoint if (req.user!.role !== 'superuser') { return res.status(403).json({ message: 'Forbidden - Superuser access required' }); } // Get all tenants with their usage stats const tenants = await Tenant.find({}); // Calculate real statistics from credit operations const totalTenants = tenants.length; // Get total operations that consumed credits (all types with negative creditsChanged) const creditConsumingOperations = await CreditOperation.find({ creditsChanged: { $lt: 0 } // Only deductions (actual credit consumption) }); // Calculate total evaluations from actual results const totalEvaluationsUsed = await EvaluationResult.countDocuments({}); // Calculate remaining credits for all tenants const totalEvaluationsRemaining = tenants.reduce((sum, tenant) => sum + (tenant.creditBalance?.availableCredits || 0), 0); const totalEvaluationsAllocated = totalEvaluationsUsed + totalEvaluationsRemaining; // Get tenant evaluation statistics from actual evaluation results const tenantEvaluationStats = await EvaluationResult.aggregate([ { $group: { _id: '$tenantId', evaluationsUsed: { $sum: 1 }, lastEvaluationDate: { $max: '$createdAt' } } } ]); // Also get credit operation stats for comparison const tenantCreditStats = await CreditOperation.aggregate([ { $match: { creditsChanged: { $lt: 0 } } }, { $addFields: { tenantIdString: { $toString: '$tenantId' } } }, { $group: { _id: '$tenantIdString', creditOperationsUsed: { $sum: 1 }, lastCreditOperation: { $max: '$createdAt' } } } ]); // Create maps for quick lookup const tenantStatsMap = new Map(); tenantEvaluationStats.forEach(stat => { tenantStatsMap.set(stat._id.toString(), { evaluationsUsed: stat.evaluationsUsed, lastEvaluationDate: stat.lastEvaluationDate }); }); const tenantCreditMap = new Map(); tenantCreditStats.forEach(stat => { tenantCreditMap.set(stat._id.toString(), { creditOperationsUsed: stat.creditOperationsUsed, lastCreditOperation: stat.lastCreditOperation }); }); // Get evaluation statistics per tenant using real credit operations data const evaluationStats = await Promise.all(tenants.map(async (tenant) => { const tenantId = tenant._id.toString(); // Count total evaluations from evaluationjobs collection const totalEvaluationsFromJobs = await EvaluationJob.aggregate([ { $match: { tenantId: tenantId, $and: [ { tenantId: { $ne: null } }, { tenantId: { $exists: true } } ] } }, { $group: { _id: null, totalEvaluations: { $sum: '$totalEvaluations' }, completedEvaluations: { $sum: '$completedEvaluations' }, failedEvaluations: { $sum: '$failedEvaluations' } } } ]); const jobStats = totalEvaluationsFromJobs[0] || { totalEvaluations: 0, completedEvaluations: 0, failedEvaluations: 0 }; // Get real evaluation data from tenantStatsMap (actual results) const realStats = tenantStatsMap.get(tenantId) || { evaluationsUsed: 0, lastEvaluationDate: null }; // Get credit operation data for comparison const creditStats = tenantCreditMap.get(tenantId) || { creditOperationsUsed: 0, lastCreditOperation: null }; return { tenantId, tenantName: tenant.name, totalEvaluationsFromJobs: jobStats.totalEvaluations, completedEvaluations: jobStats.completedEvaluations, failedEvaluations: jobStats.failedEvaluations, evaluationsUsed: realStats.evaluationsUsed, // From actual results creditOperationsUsed: creditStats.creditOperationsUsed, // From credit operations evaluationsRemaining: tenant.creditBalance?.availableCredits || 0, lastEvaluationDate: realStats.lastEvaluationDate, lastCreditOperation: creditStats.lastCreditOperation }; })); // Get active tenants (those who have used at least one evaluation) const activeTenants = tenantEvaluationStats.length; // Get tenants with recent activity (last 30 days) const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const recentlyActiveTenants = tenantEvaluationStats.filter(stat => stat.lastEvaluationDate && new Date(stat.lastEvaluationDate) > thirtyDaysAgo ).length; // Get usage distribution based on real credit operations const usageDistribution = { heavy: tenantEvaluationStats.filter(stat => stat.evaluationsUsed > 50).length, moderate: tenantEvaluationStats.filter(stat => stat.evaluationsUsed > 10 && stat.evaluationsUsed <= 50).length, light: tenantEvaluationStats.filter(stat => stat.evaluationsUsed > 0 && stat.evaluationsUsed <= 10).length, inactive: totalTenants - tenantEvaluationStats.length }; // Get top tenants by usage from credit operations const topTenantsLimit = Number(process.env.TOP_TENANTS_LIMIT || 5); const topTenants = tenantEvaluationStats .sort((a, b) => b.evaluationsUsed - a.evaluationsUsed) .slice(0, topTenantsLimit) .map(stat => { const tenant = tenants.find(t => t._id.toString() === stat._id.toString()); return { id: stat._id, name: tenant?.name || 'Unknown', evaluationsUsed: stat.evaluationsUsed, evaluationsRemaining: tenant?.creditBalance?.availableCredits || 0, lastEvaluationDate: stat.lastEvaluationDate }; }); // Get tenant creation trend (last 6 months) const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); const recentTenants = tenants.filter(tenant => new Date(tenant.createdAt) > sixMonthsAgo); const monthlyCreation = []; for (let i = 5; i >= 0; i--) { const monthStart = new Date(); monthStart.setMonth(monthStart.getMonth() - i); monthStart.setDate(1); monthStart.setHours(0, 0, 0, 0); const monthEnd = new Date(monthStart); monthEnd.setMonth(monthEnd.getMonth() + 1); const count = recentTenants.filter(tenant => { const createdAt = new Date(tenant.createdAt); return createdAt >= monthStart && createdAt < monthEnd; }).length; monthlyCreation.push({ month: monthStart.toLocaleDateString('it-IT', { month: 'short', year: 'numeric' }), count }); } const statistics = { overview: { totalTenants, activeTenants, recentlyActiveTenants, totalEvaluationsUsed, totalEvaluationsRemaining, totalEvaluationsAllocated, averageUsagePerTenant: totalTenants > 0 ? Math.round(totalEvaluationsUsed / totalTenants) : 0 }, usageDistribution, topTenants, monthlyCreation, evaluationStats: { perTenant: evaluationStats, totals: { totalEvaluationsFromJobs: evaluationStats.reduce((sum, stat) => sum + stat.totalEvaluationsFromJobs, 0), totalCompletedEvaluations: evaluationStats.reduce((sum, stat) => sum + stat.completedEvaluations, 0), totalFailedEvaluations: evaluationStats.reduce((sum, stat) => sum + stat.failedEvaluations, 0) } } }; res.json(statistics); } catch (error) { console.error('Error fetching tenant statistics:', error); res.status(500).json({ message: 'Internal server error' }); } };