requireAuth(); $this->validateRequired(['name', 'sector']); $name = trim($this->getParam('name')); $sector = $this->getParam('sector'); $vatNumber = $this->getParam('vat_number'); $fiscalCode = $this->getParam('fiscal_code'); // Valida P.IVA se fornita if ($vatNumber && !$this->validateVAT($vatNumber)) { $this->jsonError('Partita IVA non valida', 400, 'INVALID_VAT'); } Database::beginTransaction(); try { // Crea organizzazione $orgId = Database::insert('organizations', [ 'name' => $name, 'vat_number' => $vatNumber, 'fiscal_code' => $fiscalCode, 'sector' => $sector, 'employee_count' => $this->getParam('employee_count'), 'annual_turnover_eur' => $this->getParam('annual_turnover_eur'), 'country' => $this->getParam('country', 'IT'), 'city' => $this->getParam('city'), 'address' => $this->getParam('address'), 'website' => $this->getParam('website'), 'contact_email' => $this->getParam('contact_email'), 'contact_phone' => $this->getParam('contact_phone'), ]); // Auto-classifica entità NIS2 $entityType = $this->classifyNis2Entity( $sector, (int) $this->getParam('employee_count', 0), (float) $this->getParam('annual_turnover_eur', 0) ); Database::update('organizations', [ 'entity_type' => $entityType, ], 'id = ?', [$orgId]); // Aggiungi creatore come org_admin Database::insert('user_organizations', [ 'user_id' => $this->getCurrentUserId(), 'organization_id' => $orgId, 'role' => 'org_admin', 'is_primary' => 1, ]); // Inizializza controlli di compliance NIS2 $this->initializeComplianceControls($orgId); Database::commit(); $this->currentOrgId = $orgId; $this->logAudit('organization_created', 'organization', $orgId, [ 'name' => $name, 'sector' => $sector, 'entity_type' => $entityType ]); $this->jsonSuccess([ 'id' => $orgId, 'name' => $name, 'sector' => $sector, 'entity_type' => $entityType, ], 'Organizzazione creata', 201); } catch (Throwable $e) { Database::rollback(); throw $e; } } /** * GET /api/organizations/current */ public function getCurrent(): void { $this->requireOrgAccess(); $org = Database::fetchOne( 'SELECT * FROM organizations WHERE id = ?', [$this->getCurrentOrgId()] ); if (!$org) { $this->jsonError('Organizzazione non trovata', 404, 'ORG_NOT_FOUND'); } // Conta membri $memberCount = Database::count( 'user_organizations', 'organization_id = ?', [$this->getCurrentOrgId()] ); $org['member_count'] = $memberCount; $org['current_user_role'] = $this->currentOrgRole; $this->jsonSuccess($org); } /** * GET /api/organizations/list */ public function list(): void { $this->requireAuth(); if ($this->currentUser['role'] === 'super_admin') { $orgs = Database::fetchAll('SELECT * FROM organizations WHERE is_active = 1 ORDER BY name'); } else { $orgs = Database::fetchAll( 'SELECT o.*, uo.role as user_role, uo.is_primary FROM organizations o JOIN user_organizations uo ON uo.organization_id = o.id WHERE uo.user_id = ? AND o.is_active = 1 ORDER BY uo.is_primary DESC, o.name', [$this->getCurrentUserId()] ); } $this->jsonSuccess($orgs); } /** * PUT /api/organizations/{id} */ public function update(int $id): void { $this->requireOrgRole(['org_admin']); if ($id !== $this->getCurrentOrgId()) { $this->jsonError('ID organizzazione non corrisponde', 400, 'ORG_MISMATCH'); } $updates = []; $allowedFields = [ 'name', 'vat_number', 'fiscal_code', 'sector', 'employee_count', 'annual_turnover_eur', 'country', 'city', 'address', 'website', 'contact_email', 'contact_phone', 'voluntary_compliance', ]; foreach ($allowedFields as $field) { if ($this->hasParam($field)) { $updates[$field] = $this->getParam($field); } } if (empty($updates)) { $this->jsonError('Nessun campo da aggiornare', 400, 'NO_UPDATES'); } // Ri-classifica se cambiano settore, dipendenti o fatturato if (isset($updates['sector']) || isset($updates['employee_count']) || isset($updates['annual_turnover_eur'])) { $org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [$id]); $newEntityType = $this->classifyNis2Entity( $updates['sector'] ?? $org['sector'], (int) ($updates['employee_count'] ?? $org['employee_count']), (float) ($updates['annual_turnover_eur'] ?? $org['annual_turnover_eur']) ); $updates['entity_type'] = $newEntityType; // Reset voluntary compliance if no longer not_applicable if ($newEntityType !== 'not_applicable') { $updates['voluntary_compliance'] = 0; } } // Ensure voluntary_compliance is only set when entity_type is not_applicable if (isset($updates['voluntary_compliance'])) { $currentOrg = $org ?? Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$id]); $currentType = $updates['entity_type'] ?? $currentOrg['entity_type']; if ($currentType !== 'not_applicable') { $updates['voluntary_compliance'] = 0; } } Database::update('organizations', $updates, 'id = ?', [$id]); $this->logAudit('organization_updated', 'organization', $id, $updates); $this->jsonSuccess($updates, 'Organizzazione aggiornata'); } /** * GET /api/organizations/{id}/members */ public function listMembers(int $id): void { $this->requireOrgAccess(); $members = Database::fetchAll( 'SELECT u.id, u.email, u.full_name, u.phone, u.last_login_at, uo.role as org_role, uo.joined_at FROM user_organizations uo JOIN users u ON u.id = uo.user_id WHERE uo.organization_id = ? AND u.is_active = 1 ORDER BY uo.role, u.full_name', [$this->getCurrentOrgId()] ); $this->jsonSuccess($members); } /** * POST /api/organizations/{id}/invite */ public function inviteMember(int $id): void { $this->requireOrgRole(['org_admin']); $this->validateRequired(['email', 'role']); $email = strtolower(trim($this->getParam('email'))); $role = $this->getParam('role'); $validRoles = ['org_admin', 'compliance_manager', 'board_member', 'auditor', 'employee']; if (!in_array($role, $validRoles)) { $this->jsonError('Ruolo non valido', 400, 'INVALID_ROLE'); } // Trova utente per email $user = Database::fetchOne('SELECT id FROM users WHERE email = ?', [$email]); if (!$user) { $this->jsonError( 'Utente non registrato. L\'utente deve prima registrarsi sulla piattaforma.', 404, 'USER_NOT_FOUND' ); } // Verifica se già membro $existing = Database::fetchOne( 'SELECT id FROM user_organizations WHERE user_id = ? AND organization_id = ?', [$user['id'], $this->getCurrentOrgId()] ); if ($existing) { $this->jsonError('L\'utente è già membro di questa organizzazione', 409, 'ALREADY_MEMBER'); } Database::insert('user_organizations', [ 'user_id' => $user['id'], 'organization_id' => $this->getCurrentOrgId(), 'role' => $role, 'is_primary' => 0, ]); $this->logAudit('member_invited', 'organization', $this->getCurrentOrgId(), [ 'invited_user_id' => $user['id'], 'role' => $role ]); $this->jsonSuccess([ 'user_id' => $user['id'], 'role' => $role, ], 'Membro aggiunto'); } /** * DELETE /api/organizations/{id}/members/{userId} */ public function removeMember(int $orgId, int $userId): void { $this->requireOrgRole(['org_admin']); // Non puoi rimuovere te stesso if ($userId === $this->getCurrentUserId()) { $this->jsonError('Non puoi rimuovere te stesso dall\'organizzazione', 400, 'CANNOT_REMOVE_SELF'); } $deleted = Database::delete( 'user_organizations', 'user_id = ? AND organization_id = ?', [$userId, $this->getCurrentOrgId()] ); if ($deleted === 0) { $this->jsonError('Membro non trovato', 404, 'MEMBER_NOT_FOUND'); } $this->logAudit('member_removed', 'organization', $this->getCurrentOrgId(), [ 'removed_user_id' => $userId ]); $this->jsonSuccess(null, 'Membro rimosso'); } /** * POST /api/organizations/classify * Classifica se l'organizzazione è Essential o Important secondo NIS2 */ public function classifyEntity(): void { $this->validateRequired(['sector', 'employee_count', 'annual_turnover_eur']); $sector = $this->getParam('sector'); $employees = (int) $this->getParam('employee_count'); $turnover = (float) $this->getParam('annual_turnover_eur'); $entityType = $this->classifyNis2Entity($sector, $employees, $turnover); $this->jsonSuccess([ 'entity_type' => $entityType, 'sector' => $sector, 'employee_count' => $employees, 'annual_turnover_eur' => $turnover, 'allows_voluntary' => $entityType === 'not_applicable', 'explanation' => $this->getClassificationExplanation($entityType, $sector, $employees, $turnover), ]); } // ═══════════════════════════════════════════════════════════════════════ // METODI PRIVATI // ═══════════════════════════════════════════════════════════════════════ /** * Classifica entità NIS2 in base a settore, dipendenti e fatturato */ private function classifyNis2Entity(string $sector, int $employees, float $turnover): string { // Settori Essenziali (Allegato I) $essentialSectors = [ 'energy', 'transport', 'banking', 'health', 'water', 'digital_infra', 'public_admin', 'space', ]; // Settori Importanti (Allegato II) $importantSectors = [ 'manufacturing', 'postal', 'chemical', 'food', 'waste', 'ict_services', 'digital_providers', 'research', ]; // Soglie dimensionali $isLarge = $employees >= 250 || $turnover >= 50000000; $isMedium = ($employees >= 50 || $turnover >= 10000000) && !$isLarge; if (in_array($sector, $essentialSectors)) { if ($isLarge) return 'essential'; if ($isMedium) return 'important'; } if (in_array($sector, $importantSectors)) { if ($isLarge || $isMedium) return 'important'; } return 'not_applicable'; } /** * Genera spiegazione della classificazione */ private function getClassificationExplanation(string $type, string $sector, int $employees, float $turnover): string { $sizeLabel = $employees >= 250 ? 'grande impresa' : ($employees >= 50 ? 'media impresa' : 'piccola impresa'); return match ($type) { 'essential' => "L'organizzazione opera nel settore '{$sector}' (Allegato I NIS2) ed è classificata come {$sizeLabel}. Rientra tra le entità ESSENZIALI soggette a supervisione proattiva. Sanzioni fino a EUR 10M o 2% del fatturato globale.", 'important' => "L'organizzazione opera nel settore '{$sector}' ed è classificata come {$sizeLabel}. Rientra tra le entità IMPORTANTI soggette a supervisione reattiva. Sanzioni fino a EUR 7M o 1,4% del fatturato globale.", default => "In base ai parametri forniti ({$sizeLabel}, settore '{$sector}'), l'organizzazione NON rientra attualmente nell'ambito di applicazione della NIS2. Si consiglia comunque di adottare le best practice di cybersecurity.", }; } /** * Inizializza i controlli di compliance NIS2 per una nuova organizzazione */ private function initializeComplianceControls(int $orgId): void { $controls = [ ['NIS2-21.2.a', 'nis2', 'Politiche di analisi dei rischi e sicurezza dei sistemi informatici'], ['NIS2-21.2.b', 'nis2', 'Gestione degli incidenti'], ['NIS2-21.2.c', 'nis2', 'Continuità operativa e gestione delle crisi'], ['NIS2-21.2.d', 'nis2', 'Sicurezza della catena di approvvigionamento'], ['NIS2-21.2.e', 'nis2', 'Sicurezza acquisizione, sviluppo e manutenzione sistemi'], ['NIS2-21.2.f', 'nis2', 'Politiche e procedure per valutare efficacia misure'], ['NIS2-21.2.g', 'nis2', 'Pratiche di igiene informatica di base e formazione'], ['NIS2-21.2.h', 'nis2', 'Politiche e procedure relative alla crittografia'], ['NIS2-21.2.i', 'nis2', 'Sicurezza risorse umane, controllo accessi e gestione asset'], ['NIS2-21.2.j', 'nis2', 'Autenticazione multi-fattore e comunicazioni sicure'], ['NIS2-20.1', 'nis2', 'Governance: approvazione misure da parte degli organi di gestione'], ['NIS2-20.2', 'nis2', 'Formazione obbligatoria per gli organi di gestione'], ['NIS2-23.1', 'nis2', 'Notifica incidenti significativi al CSIRT (24h/72h/30gg)'], ]; foreach ($controls as [$code, $framework, $title]) { Database::insert('compliance_controls', [ 'organization_id' => $orgId, 'control_code' => $code, 'framework' => $framework, 'title' => $title, 'status' => 'not_started', ]); } } }