db = $db; $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } /** * Registra un nuovo utente */ public function register(string $username, string $email, string $password, string $fullName = null): bool { try { $passwordHash = password_hash($password, PASSWORD_ARGON2ID); $stmt = $this->db->prepare(" INSERT INTO users (username, email, password_hash, full_name) VALUES (:username, :email, :password_hash, :full_name) "); $result = $stmt->execute([ 'username' => $username, 'email' => $email, 'password_hash' => $passwordHash, 'full_name' => $fullName ]); if ($result) { $userId = $this->db->lastInsertId(); $this->logActivity($userId, $username, 'REGISTER', "Nuovo utente registrato: $username"); } return $result; } catch (PDOException $e) { error_log("Errore registrazione: " . $e->getMessage()); return false; } } /** * Login utente con username/email e password */ public function login(string $identifier, string $password, bool $rememberMe = true): array { // Verifica blocco per troppi tentativi if ($this->isAccountLocked($identifier)) { return [ 'success' => false, 'message' => 'Account temporaneamente bloccato per troppi tentativi falliti' ]; } // Cerca utente per username o email $stmt = $this->db->prepare(" SELECT id, username, email, password_hash, is_active, full_name FROM users WHERE (username = :identifier OR email = :identifier) AND is_active = 1 "); $stmt->execute(['identifier' => $identifier]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user || !password_verify($password, $user['password_hash'])) { $this->logFailedAttempt($identifier); $this->logActivity(null, $identifier, 'LOGIN_FAILED', "Tentativo di login fallito"); return [ 'success' => false, 'message' => 'Credenziali non valide' ]; } // Login riuscito - pulisce tentativi falliti $this->clearFailedAttempts($identifier); // Aggiorna last_login $stmt = $this->db->prepare("UPDATE users SET last_login = NOW() WHERE id = :id"); $stmt->execute(['id' => $user['id']]); // Crea token di autenticazione se richiesto if ($rememberMe) { $this->createAuthToken($user['id']); } // Salva in sessione dati base $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; $_SESSION['full_name'] = $user['full_name']; $this->logActivity($user['id'], $user['username'], 'LOGIN', "Login effettuato"); return [ 'success' => true, 'user' => [ 'id' => $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'full_name' => $user['full_name'] ] ]; } /** * Re-autenticazione con solo password (quando cookie è scaduto ma username è memorizzato) */ public function reAuthenticate(string $username, string $password): array { return $this->login($username, $password, true); } /** * Verifica validità sessione/cookie */ public function checkAuth(): ?array { // Prima controlla se esiste sessione attiva if (isset($_SESSION['user_id'])) { return $this->getUserById($_SESSION['user_id']); } // Altrimenti verifica cookie if (isset($_COOKIE[$this->cookieName])) { return $this->validateAuthToken($_COOKIE[$this->cookieName]); } return null; } /** * Crea token di autenticazione e imposta cookie */ private function createAuthToken(int $userId): bool { try { // Genera selector e token casuali $selector = bin2hex(random_bytes(16)); $token = bin2hex(random_bytes(32)); $tokenHash = hash('sha256', $token); $expiresAt = date('Y-m-d H:i:s', time() + $this->cookieLifetime); // Salva nel database $stmt = $this->db->prepare(" INSERT INTO auth_tokens (user_id, token, selector, expires_at, ip_address, user_agent) VALUES (:user_id, :token, :selector, :expires_at, :ip, :ua) "); $stmt->execute([ 'user_id' => $userId, 'token' => $tokenHash, 'selector' => $selector, 'expires_at' => $expiresAt, 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null ]); // Imposta cookie (selector:token) $cookieValue = $selector . ':' . $token; setcookie( $this->cookieName, $cookieValue, time() + $this->cookieLifetime, '/', '', true, // Secure (solo HTTPS in produzione) true // HttpOnly ); return true; } catch (Exception $e) { error_log("Errore creazione token: " . $e->getMessage()); return false; } } /** * Valida token di autenticazione dal cookie */ private function validateAuthToken(string $cookieValue): ?array { try { $parts = explode(':', $cookieValue); if (count($parts) !== 2) { return null; } [$selector, $token] = $parts; $tokenHash = hash('sha256', $token); // Cerca token nel database $stmt = $this->db->prepare(" SELECT at.*, u.id, u.username, u.email, u.full_name, u.is_active FROM auth_tokens at JOIN users u ON at.user_id = u.id WHERE at.selector = :selector AND at.expires_at > NOW() AND u.is_active = 1 "); $stmt->execute(['selector' => $selector]); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!$result || !hash_equals($result['token'], $tokenHash)) { $this->deleteAuthToken($selector); return null; } // Token valido - rigenera sessione $_SESSION['user_id'] = $result['id']; $_SESSION['username'] = $result['username']; $_SESSION['full_name'] = $result['full_name']; // Aggiorna last_login $stmt = $this->db->prepare("UPDATE users SET last_login = NOW() WHERE id = :id"); $stmt->execute(['id' => $result['id']]); $this->logActivity($result['id'], $result['username'], 'AUTO_LOGIN', "Login automatico via cookie"); return [ 'id' => $result['id'], 'username' => $result['username'], 'email' => $result['email'], 'full_name' => $result['full_name'] ]; } catch (Exception $e) { error_log("Errore validazione token: " . $e->getMessage()); return null; } } /** * Logout - elimina sessione e cookie */ public function logout(): void { $userId = $_SESSION['user_id'] ?? null; $username = $_SESSION['username'] ?? 'unknown'; // Elimina token dal database se presente if (isset($_COOKIE[$this->cookieName])) { $parts = explode(':', $_COOKIE[$this->cookieName]); if (count($parts) === 2) { $this->deleteAuthToken($parts[0]); } } // Elimina cookie setcookie($this->cookieName, '', time() - 3600, '/', '', true, true); // Distrugge sessione session_unset(); session_destroy(); if ($userId) { $this->logActivity($userId, $username, 'LOGOUT', "Logout effettuato"); } } /** * Elimina token dal database */ private function deleteAuthToken(string $selector): void { try { $stmt = $this->db->prepare("DELETE FROM auth_tokens WHERE selector = :selector"); $stmt->execute(['selector' => $selector]); } catch (Exception $e) { error_log("Errore eliminazione token: " . $e->getMessage()); } } /** * Ottiene utente per ID */ public function getUserById(int $userId): ?array { $stmt = $this->db->prepare(" SELECT id, username, email, full_name, is_active, created_at, last_login FROM users WHERE id = :id AND is_active = 1 "); $stmt->execute(['id' => $userId]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ?: null; } /** * Verifica se account è bloccato per troppi tentativi */ private function isAccountLocked(string $identifier): bool { $stmt = $this->db->prepare(" SELECT COUNT(*) as attempts FROM failed_login_attempts WHERE username = :identifier AND ip_address = :ip AND attempted_at > DATE_SUB(NOW(), INTERVAL :lockout SECOND) "); $stmt->execute([ 'identifier' => $identifier, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'lockout' => $this->lockoutTime ]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result['attempts'] >= $this->maxLoginAttempts; } /** * Registra tentativo di login fallito */ private function logFailedAttempt(string $identifier): void { try { $stmt = $this->db->prepare(" INSERT INTO failed_login_attempts (username, ip_address) VALUES (:username, :ip) "); $stmt->execute([ 'username' => $identifier, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '' ]); } catch (Exception $e) { error_log("Errore log tentativo fallito: " . $e->getMessage()); } } /** * Pulisce tentativi falliti dopo login riuscito */ private function clearFailedAttempts(string $identifier): void { try { $stmt = $this->db->prepare(" DELETE FROM failed_login_attempts WHERE username = :username AND ip_address = :ip "); $stmt->execute([ 'username' => $identifier, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '' ]); } catch (Exception $e) { error_log("Errore pulizia tentativi: " . $e->getMessage()); } } /** * Log attività utente */ public function logActivity(?int $userId, string $username, string $action, string $description = null): void { try { $stmt = $this->db->prepare(" INSERT INTO user_activity_log (user_id, username, action, description, ip_address, user_agent) VALUES (:user_id, :username, :action, :description, :ip, :ua) "); $stmt->execute([ 'user_id' => $userId, 'username' => $username, 'action' => $action, 'description' => $description, 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null ]); } catch (Exception $e) { error_log("Errore log attività: " . $e->getMessage()); } } /** * Pulizia token scaduti (da eseguire periodicamente) */ public function cleanExpiredTokens(): int { try { $stmt = $this->db->prepare("DELETE FROM auth_tokens WHERE expires_at < NOW()"); $stmt->execute(); return $stmt->rowCount(); } catch (Exception $e) { error_log("Errore pulizia token: " . $e->getMessage()); return 0; } } /** * Ottiene username da cookie (per form di re-autenticazione) */ public function getRememberedUsername(): ?string { if (!isset($_COOKIE[$this->cookieName])) { return null; } $parts = explode(':', $_COOKIE[$this->cookieName]); if (count($parts) !== 2) { return null; } try { $stmt = $this->db->prepare(" SELECT u.username FROM auth_tokens at JOIN users u ON at.user_id = u.id WHERE at.selector = :selector "); $stmt->execute(['selector' => $parts[0]]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result['username'] ?? null; } catch (Exception $e) { return null; } } }