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; } }