rateLimitPolicy = $rateLimitPolicy ?? new LoginRateLimitPolicy(); } public function checkRateLimit(string $ip): int { $this->loginAttemptRepository->deleteExpired(); $row = $this->loginAttemptRepository->findByIp($ip); if ($row === null || $row['locked_until'] === null) { return 0; } $lockedUntil = new \DateTime($row['locked_until']); $now = new \DateTime(); if ($lockedUntil <= $now) { return 0; } $secondsLeft = $lockedUntil->getTimestamp() - $now->getTimestamp(); return max(1, (int) ceil($secondsLeft / 60)); } public function recordFailure(string $ip): void { $this->loginAttemptRepository->recordFailure($ip, $this->rateLimitPolicy->maxAttempts(), $this->rateLimitPolicy->lockMinutes()); } public function resetRateLimit(string $ip): void { $this->loginAttemptRepository->resetForIp($ip); } public function authenticate(string $username, string $plainPassword): ?User { $user = $this->userRepository->findByUsername(mb_strtolower(trim($username))); if ($user === null) { return null; } if (!password_verify(trim($plainPassword), $user->getPasswordHash())) { return null; } return $user; } public function changePassword(int $userId, string $currentPassword, string $newPassword): void { $user = $this->userRepository->findById($userId); if ($user === null) { throw new NotFoundException('Utilisateur', $userId); } if (!password_verify(trim($currentPassword), $user->getPasswordHash())) { throw new \InvalidArgumentException('Mot de passe actuel incorrect'); } if (mb_strlen(trim($newPassword)) < 8) { throw new WeakPasswordException(); } $newHash = password_hash(trim($newPassword), PASSWORD_BCRYPT, ['cost' => 12]); $this->userRepository->updatePassword($userId, $newHash); } public function isLoggedIn(): bool { return $this->sessionManager->isAuthenticated(); } public function login(User $user): void { $this->sessionManager->setUser($user->getId(), $user->getUsername(), $user->getRole()); } public function logout(): void { $this->sessionManager->destroy(); } }