false, 'message' => 'Not Found']); exit; } // ═══════════════════════════════════════════════════════════════════════════ // API ROUTING // ═══════════════════════════════════════════════════════════════════════════ $parts = explode('/', $path); array_shift($parts); // Rimuovi "api" $controllerName = $parts[0] ?? 'index'; $actionName = $parts[1] ?? 'index'; $resourceId = isset($parts[2]) ? $parts[2] : null; $subAction = $parts[3] ?? null; $subResourceId = isset($parts[4]) ? $parts[4] : null; // Mappa controller $controllerMap = [ 'auth' => 'AuthController', 'organizations' => 'OrganizationController', 'assessments' => 'AssessmentController', 'dashboard' => 'DashboardController', 'risks' => 'RiskController', 'incidents' => 'IncidentController', 'policies' => 'PolicyController', 'supply-chain' => 'SupplyChainController', 'training' => 'TrainingController', 'assets' => 'AssetController', 'audit' => 'AuditController', 'admin' => 'AdminController', 'onboarding' => 'OnboardingController', 'ncr' => 'NonConformityController', 'services' => 'ServicesController', 'webhooks' => 'WebhookController', 'whistleblowing'=> 'WhistleblowingController', 'normative' => 'NormativeController', ]; if (!isset($controllerMap[$controllerName])) { http_response_code(404); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => 'Endpoint non trovato', 'error_code' => 'NOT_FOUND', ]); exit; } $controllerClass = $controllerMap[$controllerName]; $controllerFile = APP_PATH . "/controllers/{$controllerClass}.php"; if (!file_exists($controllerFile)) { http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => 'Controller non disponibile', ]); exit; } require_once $controllerFile; // ═══════════════════════════════════════════════════════════════════════════ // MAPPA AZIONI PER METODO HTTP // ═══════════════════════════════════════════════════════════════════════════ $method = $_SERVER['REQUEST_METHOD']; // Converti action name con trattini in camelCase $actionName = str_replace('-', '', lcfirst(ucwords($actionName, '-'))); $actionMap = [ // ── AuthController ────────────────────────────── 'auth' => [ 'POST:register' => 'register', 'POST:login' => 'login', 'POST:logout' => 'logout', 'POST:refresh' => 'refresh', 'GET:me' => 'me', 'PUT:profile' => 'updateProfile', 'POST:changePassword' => 'changePassword', ], // ── OrganizationController ────────────────────── 'organizations' => [ 'POST:create' => 'create', 'GET:current' => 'getCurrent', 'GET:list' => 'list', 'PUT:{id}' => 'update', 'GET:{id}/members' => 'listMembers', 'POST:{id}/invite' => 'inviteMember', 'DELETE:{id}/members/{subId}' => 'removeMember', 'POST:classify' => 'classifyEntity', ], // ── AssessmentController ──────────────────────── 'assessments' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'GET:{id}/questions' => 'getQuestions', 'POST:{id}/respond' => 'saveResponse', 'POST:{id}/complete' => 'complete', 'GET:{id}/report' => 'getReport', 'POST:{id}/aiAnalyze' => 'aiAnalyze', ], // ── DashboardController ───────────────────────── 'dashboard' => [ 'GET:overview' => 'overview', 'GET:complianceScore' => 'complianceScore', 'GET:upcomingDeadlines' => 'deadlines', 'GET:recentActivity' => 'recentActivity', 'GET:riskHeatmap' => 'riskHeatmap', ], // ── RiskController ────────────────────────────── 'risks' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'DELETE:{id}' => 'delete', 'POST:{id}/treatments' => 'addTreatment', 'PUT:treatments/{subId}' => 'updateTreatment', 'GET:matrix' => 'getRiskMatrix', 'POST:aiSuggest' => 'aiSuggestRisks', ], // ── IncidentController ────────────────────────── 'incidents' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'POST:{id}/timeline' => 'addTimelineEvent', 'POST:{id}/earlyWarning' => 'sendEarlyWarning', 'POST:{id}/notification' => 'sendNotification', 'POST:{id}/finalReport' => 'sendFinalReport', 'POST:{id}/aiClassify' => 'aiClassify', ], // ── PolicyController ──────────────────────────── 'policies' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'DELETE:{id}' => 'delete', 'POST:{id}/approve' => 'approve', 'POST:aiGenerate' => 'aiGeneratePolicy', 'GET:templates' => 'getTemplates', ], // ── SupplyChainController ─────────────────────── 'supply-chain' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'DELETE:{id}' => 'delete', 'POST:{id}/assess' => 'assessSupplier', 'GET:riskOverview' => 'riskOverview', ], // ── TrainingController ────────────────────────── 'training' => [ 'GET:courses' => 'listCourses', 'POST:courses' => 'createCourse', 'GET:assignments' => 'myAssignments', 'POST:assign' => 'assignCourse', 'PUT:assignments/{subId}' => 'updateAssignment', 'GET:complianceStatus' => 'complianceStatus', ], // ── AssetController ───────────────────────────── 'assets' => [ 'GET:list' => 'list', 'POST:create' => 'create', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'DELETE:{id}' => 'delete', 'GET:dependencyMap' => 'dependencyMap', ], // ── AuditController ───────────────────────────── 'audit' => [ 'GET:controls' => 'listControls', 'PUT:controls/{subId}' => 'updateControl', 'POST:evidence/upload' => 'uploadEvidence', 'GET:evidence/list' => 'listEvidence', 'GET:report' => 'generateReport', 'GET:logs' => 'getAuditLogs', 'GET:iso27001Mapping' => 'getIsoMapping', 'GET:executiveReport' => 'executiveReport', 'GET:export' => 'export', 'GET:chainVerify' => 'chainVerify', 'GET:exportCertified' => 'exportCertified', ], // ── AdminController ───────────────────────────── 'admin' => [ 'GET:organizations' => 'listOrganizations', 'GET:users' => 'listUsers', 'GET:stats' => 'platformStats', ], // ── OnboardingController ────────────────────── 'onboarding' => [ 'POST:uploadVisura' => 'uploadVisura', 'POST:fetchCompany' => 'fetchCompany', 'POST:complete' => 'complete', ], // ── NonConformityController (NCR & CAPA) ─────── 'ncr' => [ 'POST:create' => 'create', 'GET:list' => 'list', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'POST:{id}/capa' => 'addCapa', 'PUT:capa/{subId}' => 'updateCapa', 'POST:fromAssessment' => 'fromAssessment', 'GET:stats' => 'stats', 'POST:{id}/sync' => 'syncExternal', 'POST:webhook' => 'webhook', ], // ── ServicesController (API pubblica) ────────── 'services' => [ 'POST:provision' => 'provision', 'POST:token' => 'token', 'POST:sso' => 'sso', 'GET:status' => 'status', 'GET:complianceSummary' => 'complianceSummary', 'GET:risksFeed' => 'risksFeed', 'GET:incidentsFeed' => 'incidentsFeed', 'GET:controlsStatus' => 'controlsStatus', 'GET:assetsCritical' => 'assetsCritical', 'GET:suppliersRisk' => 'suppliersRisk', 'GET:policiesApproved' => 'policiesApproved', 'GET:openapi' => 'openapi', ], // ── WebhookController (CRUD keys + subscriptions) ── 'webhooks' => [ 'GET:apiKeys' => 'listApiKeys', 'POST:apiKeys' => 'createApiKey', 'DELETE:apiKeys/{subId}' => 'deleteApiKey', 'GET:subscriptions' => 'listSubscriptions', 'POST:subscriptions' => 'createSubscription', 'PUT:subscriptions/{subId}' => 'updateSubscription', 'DELETE:subscriptions/{subId}' => 'deleteSubscription', 'POST:subscriptions/{subId}/test' => 'testSubscription', 'GET:deliveries' => 'listDeliveries', 'POST:retry' => 'processRetry', ], // ── WhistleblowingController (Art.32 NIS2) ───── 'whistleblowing' => [ 'POST:submit' => 'submit', 'GET:list' => 'list', 'GET:{id}' => 'get', 'PUT:{id}' => 'update', 'POST:{id}/assign' => 'assign', 'POST:{id}/close' => 'close', 'GET:stats' => 'stats', 'GET:trackAnonymous' => 'trackAnonymous', ], // ── NormativeController (Feed NIS2/ACN) ───────── 'normative' => [ 'GET:list' => 'list', 'GET:{id}' => 'get', 'POST:{id}/ack' => 'acknowledge', 'GET:pending' => 'pending', 'GET:stats' => 'stats', 'POST:create' => 'create', ], ]; // ═══════════════════════════════════════════════════════════════════════════ // RISOLUZIONE AZIONE // ═══════════════════════════════════════════════════════════════════════════ $actions = $actionMap[$controllerName] ?? []; $resolvedAction = null; $callArgs = []; // Helper: convert kebab-case to camelCase (same logic as $actionName conversion) $toCamel = function (string $s): string { return str_replace('-', '', lcfirst(ucwords($s, '-'))); }; // Costruisci candidati pattern → argomenti (ordine: più specifico prima) $candidates = []; if (is_numeric($actionName)) { // Il primo segmento è un ID numerico: // /controller/123 → METHOD:{id} // /controller/123/sub → METHOD:{id}/sub // /controller/123/sub/456 → METHOD:{id}/sub/{subId} $numericId = (int) $actionName; $sub = $resourceId !== null ? $toCamel($resourceId) : null; $subId = $subAction !== null && is_numeric($subAction) ? (int) $subAction : null; if ($sub !== null && $subId !== null) { $candidates[] = ['p' => "{$method}:{id}/{$sub}/{subId}", 'a' => [$numericId, $subId]]; } if ($sub !== null) { $candidates[] = ['p' => "{$method}:{id}/{$sub}", 'a' => [$numericId]]; } $candidates[] = ['p' => "{$method}:{id}", 'a' => [$numericId]]; } else { // Il primo segmento è un'azione nominale: // /controller/action → METHOD:action // /controller/action/123 → METHOD:action/{subId} oppure METHOD:{id} // /controller/action/sub → METHOD:action/sub // /controller/action/123/sub → METHOD:{id}/sub // /controller/action/123/sub/456 → METHOD:{id}/sub/{subId} if ($subAction !== null && $resourceId !== null && is_numeric($resourceId)) { $rid = (int) $resourceId; $camelSub = $toCamel($subAction); if ($subResourceId !== null && is_numeric($subResourceId)) { $sid = (int) $subResourceId; $candidates[] = ['p' => "{$method}:{id}/{$camelSub}/{subId}", 'a' => [$rid, $sid]]; } // e.g. POST:subscriptions/{subId}/test $candidates[] = ['p' => "{$method}:{$actionName}/{subId}/{$camelSub}", 'a' => [$rid]]; $candidates[] = ['p' => "{$method}:{id}/{$camelSub}", 'a' => [$rid]]; $candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]]; } if ($resourceId !== null && $subAction === null) { if (is_numeric($resourceId)) { // /controller/action/123 → potrebbe essere action/{subId} o {id} $rid = (int) $resourceId; $candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]]; $candidates[] = ['p' => "{$method}:{id}", 'a' => [$rid]]; } else { // /controller/action/subAction → nome composto (es: evidence/upload) $camelResource = $toCamel($resourceId); $candidates[] = ['p' => "{$method}:{$actionName}/{$camelResource}", 'a' => []]; } } // /controller/action $candidates[] = ['p' => "{$method}:{$actionName}", 'a' => []]; } // Cerca primo match foreach ($candidates as $candidate) { if (isset($actions[$candidate['p']])) { $resolvedAction = $actions[$candidate['p']]; $callArgs = $candidate['a']; break; } } if (!$resolvedAction) { http_response_code(404); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => "Azione non trovata: {$method} /{$controllerName}/{$actionName}", 'error_code' => 'ACTION_NOT_FOUND', ]); exit; } // ═══════════════════════════════════════════════════════════════════════════ // ESECUZIONE // ═══════════════════════════════════════════════════════════════════════════ try { $controller = new $controllerClass(); if (!method_exists($controller, $resolvedAction)) { http_response_code(501); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => "Metodo '{$resolvedAction}' non implementato", ]); exit; } // Chiama con gli argomenti risolti $controller->$resolvedAction(...$callArgs); } catch (RuntimeException $e) { // Rate limit exceeded (429) if ($e->getCode() === 429) { http_response_code(429); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => $e->getMessage(), 'error_code' => 'RATE_LIMITED', ]); exit; } throw $e; } catch (PDOException $e) { error_log('[DB_ERROR] ' . $e->getMessage()); http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => APP_DEBUG ? 'Errore database: ' . $e->getMessage() : 'Errore interno del server', ]); } catch (Throwable $e) { error_log('[ERROR] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'message' => APP_DEBUG ? $e->getMessage() : 'Errore interno del server', ]); }