nis2-agile/application/controllers/BaseController.php
Cristiano Benassati ae78a2f7f4 [CORE] Initial project scaffold - NIS2 Agile Compliance Platform
Complete MVP implementation including:
- PHP 8.4 backend with Front Controller pattern (80+ API endpoints)
- Multi-tenant architecture with organization_id isolation
- JWT authentication (HS256, 2h access + 7d refresh tokens)
- 14 controllers: Auth, Organization, Assessment, Dashboard, Risk,
  Incident, Policy, SupplyChain, Training, Asset, Audit, Admin
- AI Service integration (Anthropic Claude API) for gap analysis,
  risk suggestions, policy generation, incident classification
- NIS2 gap analysis questionnaire (~80 questions, 10 categories)
- MySQL schema (20 tables) with NIS2 Art. 21 compliance controls
- NIS2 Art. 23 incident reporting workflow (24h/72h/30d)
- Frontend: login, register, dashboard, assessment wizard, org setup
- Docker configuration (PHP-FPM + Nginx + MySQL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:50:18 +01:00

577 lines
18 KiB
PHP

<?php
/**
* NIS2 Agile - Base Controller
*
* Classe base per tutti i controller.
* Gestisce autenticazione, multi-tenancy, risposte JSON, validazione.
*/
require_once APP_PATH . '/config/database.php';
class BaseController
{
protected ?array $currentUser = null;
protected ?int $currentOrgId = null;
protected ?string $currentOrgRole = null;
// ═══════════════════════════════════════════════════════════════════════
// RISPOSTE JSON
// ═══════════════════════════════════════════════════════════════════════
/**
* Invia risposta JSON di successo
*/
protected function jsonSuccess($data = null, string $message = 'OK', int $statusCode = 200): void
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'message' => $message,
'data' => $data,
], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Invia risposta JSON di errore
*/
protected function jsonError(string $message, int $statusCode = 400, ?string $errorCode = null, ?array $data = null): void
{
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
$response = [
'success' => false,
'message' => $message,
];
if ($errorCode) {
$response['error_code'] = $errorCode;
}
if ($data) {
$response['data'] = $data;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Invia risposta paginata
*/
protected function jsonPaginated(array $items, int $total, int $page, int $perPage): void
{
$this->jsonSuccess([
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'pages' => ceil($total / $perPage),
]);
}
// ═══════════════════════════════════════════════════════════════════════
// PARAMETRI RICHIESTA
// ═══════════════════════════════════════════════════════════════════════
/**
* Ottiene parametro dalla richiesta (GET, POST o JSON body)
*/
protected function getParam(string $key, $default = null)
{
if (isset($_REQUEST[$key])) {
return $_REQUEST[$key];
}
$jsonBody = $this->getJsonBody();
if (isset($jsonBody[$key])) {
return $jsonBody[$key];
}
return $default;
}
/**
* Verifica se un parametro esiste
*/
protected function hasParam(string $key): bool
{
if (isset($_REQUEST[$key])) {
return true;
}
$jsonBody = $this->getJsonBody();
return isset($jsonBody[$key]);
}
/**
* Ottiene tutti i parametri dalla richiesta
*/
protected function getAllParams(): array
{
$params = $_REQUEST;
$jsonBody = $this->getJsonBody();
return array_merge($params, $jsonBody);
}
/**
* Ottiene il body JSON della richiesta
*/
protected function getJsonBody(): array
{
static $jsonBody = null;
if ($jsonBody === null) {
$input = file_get_contents('php://input');
$jsonBody = json_decode($input, true) ?? [];
}
return $jsonBody;
}
/**
* Ottiene parametri di paginazione
*/
protected function getPagination(int $defaultPerPage = 20): array
{
$page = max(1, (int) $this->getParam('page', 1));
$perPage = min(100, max(1, (int) $this->getParam('per_page', $defaultPerPage)));
$offset = ($page - 1) * $perPage;
return ['page' => $page, 'per_page' => $perPage, 'offset' => $offset];
}
// ═══════════════════════════════════════════════════════════════════════
// VALIDAZIONE
// ═══════════════════════════════════════════════════════════════════════
/**
* Valida parametri obbligatori
*/
protected function validateRequired(array $required): void
{
$missing = [];
foreach ($required as $field) {
$value = $this->getParam($field);
if ($value === null || $value === '') {
$missing[] = $field;
}
}
if (!empty($missing)) {
$this->jsonError(
'Campi obbligatori mancanti: ' . implode(', ', $missing),
400,
'MISSING_REQUIRED_FIELDS'
);
}
}
/**
* Valida formato email
*/
protected function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Valida Partita IVA italiana
*/
protected function validateVAT(string $vat): bool
{
$vat = preg_replace('/\s+/', '', $vat);
$vat = preg_replace('/^IT/i', '', $vat);
if (!preg_match('/^\d{11}$/', $vat)) {
return false;
}
$sum = 0;
for ($i = 0; $i < 11; $i++) {
$digit = (int) $vat[$i];
if ($i % 2 === 0) {
$sum += $digit;
} else {
$double = $digit * 2;
$sum += ($double > 9) ? $double - 9 : $double;
}
}
return ($sum % 10) === 0;
}
/**
* Valida Codice Fiscale italiano
*/
protected function validateFiscalCode(string $cf): bool
{
$cf = strtoupper(trim($cf));
return (bool) preg_match('/^[A-Z0-9]{16}$/', $cf);
}
/**
* Valida password secondo policy
*/
protected function validatePassword(string $password): array
{
$errors = [];
if (strlen($password) < PASSWORD_MIN_LENGTH) {
$errors[] = 'La password deve essere di almeno ' . PASSWORD_MIN_LENGTH . ' caratteri';
}
if (PASSWORD_REQUIRE_UPPERCASE && !preg_match('/[A-Z]/', $password)) {
$errors[] = 'La password deve contenere almeno una lettera maiuscola';
}
if (PASSWORD_REQUIRE_NUMBER && !preg_match('/[0-9]/', $password)) {
$errors[] = 'La password deve contenere almeno un numero';
}
if (PASSWORD_REQUIRE_SPECIAL && !preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
$errors[] = 'La password deve contenere almeno un carattere speciale';
}
return $errors;
}
// ═══════════════════════════════════════════════════════════════════════
// AUTENTICAZIONE JWT
// ═══════════════════════════════════════════════════════════════════════
/**
* Richiede autenticazione JWT
*/
protected function requireAuth(): void
{
$token = $this->getBearerToken();
if (!$token) {
$this->jsonError('Token di autenticazione mancante', 401, 'MISSING_TOKEN');
}
$payload = $this->verifyJWT($token);
if (!$payload) {
$this->jsonError('Token non valido o scaduto', 401, 'INVALID_TOKEN');
}
$user = Database::fetchOne(
'SELECT * FROM users WHERE id = ? AND is_active = 1',
[$payload['user_id']]
);
if (!$user) {
$this->jsonError('Utente non trovato o disabilitato', 401, 'USER_NOT_FOUND');
}
$this->currentUser = $user;
}
/**
* Richiede ruolo super_admin
*/
protected function requireSuperAdmin(): void
{
$this->requireAuth();
if ($this->currentUser['role'] !== 'super_admin') {
$this->jsonError('Accesso riservato ai super amministratori', 403, 'SUPER_ADMIN_REQUIRED');
}
}
// ═══════════════════════════════════════════════════════════════════════
// MULTI-TENANCY
// ═══════════════════════════════════════════════════════════════════════
/**
* Richiede accesso all'organizzazione corrente
*/
protected function requireOrgAccess(): void
{
$this->requireAuth();
$orgId = $this->resolveOrgId();
if (!$orgId) {
$this->jsonError('Organizzazione non selezionata', 403, 'NO_ORG');
}
// Super admin ha accesso a tutto
if ($this->currentUser['role'] === 'super_admin') {
$this->currentOrgId = $orgId;
$this->currentOrgRole = 'super_admin';
return;
}
// Verifica membership
$membership = Database::fetchOne(
'SELECT role FROM user_organizations WHERE user_id = ? AND organization_id = ?',
[$this->getCurrentUserId(), $orgId]
);
if (!$membership) {
$this->jsonError('Accesso non autorizzato a questa organizzazione', 403, 'ORG_ACCESS_DENIED');
}
$this->currentOrgId = $orgId;
$this->currentOrgRole = $membership['role'];
}
/**
* Richiede ruolo minimo nell'organizzazione
*/
protected function requireOrgRole(array $allowedRoles): void
{
$this->requireOrgAccess();
if ($this->currentOrgRole === 'super_admin') {
return;
}
if (!in_array($this->currentOrgRole, $allowedRoles)) {
$this->jsonError(
'Ruolo insufficiente. Richiesto: ' . implode(' o ', $allowedRoles),
403,
'INSUFFICIENT_ROLE'
);
}
}
/**
* Risolve l'ID organizzazione dalla richiesta
*/
protected function resolveOrgId(): ?int
{
// 1. Header X-Organization-Id
$orgId = $_SERVER['HTTP_X_ORGANIZATION_ID'] ?? null;
if ($orgId) {
return (int) $orgId;
}
// 2. Query parameter org_id
$orgId = $this->getParam('org_id');
if ($orgId) {
return (int) $orgId;
}
// 3. Organizzazione primaria dell'utente
$primary = Database::fetchOne(
'SELECT organization_id FROM user_organizations WHERE user_id = ? AND is_primary = 1',
[$this->getCurrentUserId()]
);
return $primary ? (int) $primary['organization_id'] : null;
}
/**
* Ottiene utente corrente
*/
protected function getCurrentUser(): ?array
{
return $this->currentUser;
}
/**
* Ottiene ID utente corrente
*/
protected function getCurrentUserId(): ?int
{
return $this->currentUser ? (int) $this->currentUser['id'] : null;
}
/**
* Ottiene ID organizzazione corrente
*/
protected function getCurrentOrgId(): ?int
{
return $this->currentOrgId;
}
// ═══════════════════════════════════════════════════════════════════════
// JWT TOKEN MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════
/**
* Estrae Bearer token dall'header Authorization
*/
protected function getBearerToken(): ?string
{
$headers = $this->getAuthorizationHeader();
if ($headers && preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
return $matches[1];
}
if (isset($_GET['token']) && !empty($_GET['token'])) {
return $_GET['token'];
}
return null;
}
/**
* Ottiene header Authorization
*/
private function getAuthorizationHeader(): ?string
{
if (isset($_SERVER['Authorization'])) {
return $_SERVER['Authorization'];
}
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
return $_SERVER['HTTP_AUTHORIZATION'];
}
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
if (isset($headers['Authorization'])) {
return $headers['Authorization'];
}
}
return null;
}
/**
* Genera JWT token
*/
protected function generateJWT(int $userId, array $extraData = []): string
{
$header = json_encode([
'typ' => 'JWT',
'alg' => JWT_ALGORITHM,
]);
$payload = json_encode(array_merge([
'user_id' => $userId,
'iat' => time(),
'exp' => time() + JWT_EXPIRES_IN,
], $extraData));
$base64Header = $this->base64UrlEncode($header);
$base64Payload = $this->base64UrlEncode($payload);
$signature = hash_hmac('sha256', "$base64Header.$base64Payload", JWT_SECRET, true);
$base64Signature = $this->base64UrlEncode($signature);
return "$base64Header.$base64Payload.$base64Signature";
}
/**
* Verifica JWT token
*/
protected function verifyJWT(string $token): ?array
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
[$base64Header, $base64Payload, $base64Signature] = $parts;
$signature = $this->base64UrlDecode($base64Signature);
$expectedSignature = hash_hmac('sha256', "$base64Header.$base64Payload", JWT_SECRET, true);
if (!hash_equals($signature, $expectedSignature)) {
return null;
}
$payload = json_decode($this->base64UrlDecode($base64Payload), true);
if (!$payload) {
return null;
}
if (isset($payload['exp']) && $payload['exp'] < time()) {
return null;
}
return $payload;
}
/**
* Genera refresh token
*/
protected function generateRefreshToken(int $userId): string
{
$token = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', time() + JWT_REFRESH_EXPIRES_IN);
Database::insert('refresh_tokens', [
'user_id' => $userId,
'token' => hash('sha256', $token),
'expires_at' => $expiresAt,
]);
return $token;
}
private function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private function base64UrlDecode(string $data): string
{
return base64_decode(strtr($data, '-_', '+/'));
}
// ═══════════════════════════════════════════════════════════════════════
// AUDIT LOGGING
// ═══════════════════════════════════════════════════════════════════════
/**
* Registra azione nell'audit log
*/
protected function logAudit(string $action, ?string $entityType = null, ?int $entityId = null, ?array $details = null): void
{
Database::insert('audit_logs', [
'user_id' => $this->getCurrentUserId(),
'organization_id' => $this->currentOrgId,
'action' => $action,
'entity_type' => $entityType,
'entity_id' => $entityId,
'details' => $details ? json_encode($details) : null,
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
]);
}
// ═══════════════════════════════════════════════════════════════════════
// UTILITY
// ═══════════════════════════════════════════════════════════════════════
/**
* Sanitizza stringa per output
*/
protected function sanitize(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
/**
* Ottiene metodo HTTP della richiesta
*/
protected function getMethod(): string
{
return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
}
/**
* Genera codice univoco
*/
protected function generateCode(string $prefix, int $length = 6): string
{
$number = str_pad(mt_rand(0, pow(10, $length) - 1), $length, '0', STR_PAD_LEFT);
return $prefix . '-' . $number;
}
}