402 lines
14 KiB
PHP
Executable File
402 lines
14 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* UserManager - Gestione utenti e autenticazione tramite cookie
|
|
* PHP 8+ con MariaDB
|
|
*/
|
|
|
|
class UserManager {
|
|
private PDO $db;
|
|
private string $cookieName = 'auth_token';
|
|
private int $cookieLifetime = 2592000; // 30 giorni in secondi
|
|
private int $maxLoginAttempts = 5;
|
|
private int $lockoutTime = 900; // 15 minuti in secondi
|
|
|
|
public function __construct(PDO $db) {
|
|
$this->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;
|
|
}
|
|
}
|
|
} |