getClientIP(); RateLimitService::check("register:{$ip}", RATE_LIMIT_AUTH_REGISTER); RateLimitService::increment("register:{$ip}"); // $ip defined above via getClientIP() $this->validateRequired(['email', 'password', 'full_name']); $email = strtolower(trim($this->getParam('email'))); $password = $this->getParam('password'); $fullName = trim($this->getParam('full_name')); $phone = $this->getParam('phone'); $userType = $this->getParam('user_type', 'azienda'); // 'azienda' | 'consultant' $role = ($userType === 'consultant') ? 'consultant' : 'employee'; // Validazione email if (!$this->validateEmail($email)) { $this->jsonError('Formato email non valido', 400, 'INVALID_EMAIL'); } // Validazione password $passwordErrors = $this->validatePassword($password); if (!empty($passwordErrors)) { $this->jsonError( implode('. ', $passwordErrors), 400, 'WEAK_PASSWORD' ); } // Verifica email duplicata $existing = Database::fetchOne('SELECT id FROM users WHERE email = ?', [$email]); if ($existing) { $this->jsonError('Email giĆ  registrata', 409, 'EMAIL_EXISTS'); } // Crea utente $userId = Database::insert('users', [ 'email' => $email, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'full_name' => $fullName, 'phone' => $phone, 'role' => $role, 'is_active' => 1, ]); // Genera tokens $accessToken = $this->generateJWT($userId); $refreshToken = $this->generateRefreshToken($userId); // Audit log $this->currentUser = ['id' => $userId]; $this->logAudit('user_registered', 'user', $userId, ['user_type' => $userType]); $this->jsonSuccess([ 'user' => [ 'id' => $userId, 'email' => $email, 'full_name' => $fullName, 'role' => $role, ], 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'expires_in' => JWT_EXPIRES_IN, ], 'Registrazione completata', 201); } /** * POST /api/auth/login */ public function login(): void { // Rate limiting $ip = $this->getClientIP(); RateLimitService::check("login:{$ip}", RATE_LIMIT_AUTH_LOGIN); RateLimitService::increment("login:{$ip}"); $this->validateRequired(['email', 'password']); $email = strtolower(trim($this->getParam('email'))); $password = $this->getParam('password'); // Trova utente $user = Database::fetchOne( 'SELECT * FROM users WHERE email = ? AND is_active = 1', [$email] ); if (!$user || !password_verify($password, $user['password_hash'])) { $this->jsonError('Credenziali non valide', 401, 'INVALID_CREDENTIALS'); } // Aggiorna ultimo login Database::update('users', [ 'last_login_at' => date('Y-m-d H:i:s'), ], 'id = ?', [$user['id']]); // Genera tokens $accessToken = $this->generateJWT((int) $user['id']); $refreshToken = $this->generateRefreshToken((int) $user['id']); // Carica organizzazioni $organizations = Database::fetchAll( 'SELECT uo.organization_id, uo.role, uo.is_primary, o.name, o.sector, o.entity_type FROM user_organizations uo JOIN organizations o ON o.id = uo.organization_id WHERE uo.user_id = ? AND o.is_active = 1', [$user['id']] ); $this->currentUser = $user; $this->logAudit('user_login', 'user', (int) $user['id']); $this->jsonSuccess([ 'user' => [ 'id' => (int) $user['id'], 'email' => $user['email'], 'full_name' => $user['full_name'], 'role' => $user['role'], 'preferred_language' => $user['preferred_language'], ], 'organizations' => $organizations, 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'expires_in' => JWT_EXPIRES_IN, ], 'Login effettuato'); } /** * POST /api/auth/logout */ public function logout(): void { $this->requireAuth(); // Invalida tutti i refresh token dell'utente Database::delete('refresh_tokens', 'user_id = ?', [$this->getCurrentUserId()]); $this->logAudit('user_logout', 'user', $this->getCurrentUserId()); $this->jsonSuccess(null, 'Logout effettuato'); } /** * POST /api/auth/refresh */ public function refresh(): void { $this->validateRequired(['refresh_token']); $refreshToken = $this->getParam('refresh_token'); $hashedToken = hash('sha256', $refreshToken); // Transazione atomica per evitare race condition (double-spend del refresh token) Database::beginTransaction(); try { // SELECT FOR UPDATE: blocca il record per tutta la transazione $tokenRecord = Database::fetchOne( 'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW() FOR UPDATE', [$hashedToken] ); if (!$tokenRecord) { Database::rollback(); $this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN'); } // Elimina vecchio token atomicamente Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]); // Genera nuovi tokens $userId = (int) $tokenRecord['user_id']; $accessToken = $this->generateJWT($userId); $newRefreshToken = $this->generateRefreshToken($userId); Database::commit(); } catch (Throwable $e) { Database::rollback(); throw $e; } $this->jsonSuccess([ 'access_token' => $accessToken, 'refresh_token' => $newRefreshToken, 'expires_in' => JWT_EXPIRES_IN, ], 'Token rinnovato'); } /** * GET /api/auth/me */ public function me(): void { $this->requireAuth(); $user = $this->getCurrentUser(); // Carica organizzazioni $organizations = Database::fetchAll( 'SELECT uo.organization_id, uo.role as org_role, uo.is_primary, o.name, o.sector, o.entity_type, o.subscription_plan FROM user_organizations uo JOIN organizations o ON o.id = uo.organization_id WHERE uo.user_id = ? AND o.is_active = 1', [$user['id']] ); $this->jsonSuccess([ 'id' => (int) $user['id'], 'email' => $user['email'], 'full_name' => $user['full_name'], 'phone' => $user['phone'], 'role' => $user['role'], 'preferred_language' => $user['preferred_language'], 'last_login_at' => $user['last_login_at'], 'created_at' => $user['created_at'], 'organizations' => $organizations, ]); } /** * PUT /api/auth/profile */ public function updateProfile(): void { $this->requireAuth(); $updates = []; if ($this->hasParam('full_name')) { $updates['full_name'] = trim($this->getParam('full_name')); } if ($this->hasParam('phone')) { $updates['phone'] = $this->getParam('phone'); } if ($this->hasParam('preferred_language')) { $lang = $this->getParam('preferred_language'); if (in_array($lang, ['it', 'en', 'fr', 'de'])) { $updates['preferred_language'] = $lang; } } if (empty($updates)) { $this->jsonError('Nessun campo da aggiornare', 400, 'NO_UPDATES'); } Database::update('users', $updates, 'id = ?', [$this->getCurrentUserId()]); $this->logAudit('profile_updated', 'user', $this->getCurrentUserId(), $updates); $this->jsonSuccess($updates, 'Profilo aggiornato'); } /** * POST /api/auth/change-password */ public function changePassword(): void { $this->requireAuth(); $this->validateRequired(['current_password', 'new_password']); $currentPassword = $this->getParam('current_password'); $newPassword = $this->getParam('new_password'); // Verifica password attuale if (!password_verify($currentPassword, $this->currentUser['password_hash'])) { $this->jsonError('Password attuale non corretta', 400, 'WRONG_PASSWORD'); } // Validazione nuova password $errors = $this->validatePassword($newPassword); if (!empty($errors)) { $this->jsonError(implode('. ', $errors), 400, 'WEAK_PASSWORD'); } Database::update('users', [ 'password_hash' => password_hash($newPassword, PASSWORD_DEFAULT), ], 'id = ?', [$this->getCurrentUserId()]); // Invalida tutti i refresh token (force re-login) Database::delete('refresh_tokens', 'user_id = ?', [$this->getCurrentUserId()]); $this->logAudit('password_changed', 'user', $this->getCurrentUserId()); $this->jsonSuccess(null, 'Password modificata. Effettua nuovamente il login.'); } }