0, 'path' => '/', 'secure' => (!empty($_SERVER['HTTPS'])), 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } } public static function attempt(string $login, string $password): array { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; if (self::isRateLimited($login, $ip)) { return ['ok' => false, 'error' => 'Muitas tentativas. Aguarde alguns minutos e tente novamente.']; } $ldap = new Ldap(); $bindOk = $ldap->tryUserBind($login, $password); self::logAttempt($login, $ip, $bindOk); if (!$bindOk) { return ['ok' => false, 'error' => 'Usuário ou senha incorretos.']; } $admin = AdminRepository::findActiveByLogin($login); if (!$admin) { AuditLog::record($login, 'login_negado', null, 'usuário autenticado no AD mas sem permissão no sistema'); return ['ok' => false, 'error' => 'Seu usuário não tem permissão para acessar este sistema.']; } // Atualiza o nome em cache a partir do AD, sem depender da conta de serviço falhar. try { $ldap->bindService(); $entry = $ldap->findUserByLogin($login); $nome = $entry['displayname'][0] ?? null; if ($nome) { AdminRepository::touchLoginCache($login, $nome); $admin['nome'] = $nome; } } catch (Throwable $e) { // Não bloqueia o login por causa da conta de serviço. } self::start(); session_regenerate_id(true); $_SESSION['login'] = $login; $_SESSION['nome'] = $admin['nome'] ?? $login; $_SESSION['papel'] = $admin['papel']; AuditLog::record($login, 'login', null, 'login efetuado'); return ['ok' => true]; } public static function logout(): void { self::start(); $login = $_SESSION['login'] ?? 'desconhecido'; AuditLog::record($login, 'logout'); $_SESSION = []; session_destroy(); } public static function requireLogin(): void { self::start(); if (empty($_SESSION['login'])) { header('Location: index.php?erro=sessao_expirada'); exit; } } public static function requireAdmin(): void { self::requireLogin(); if (($_SESSION['papel'] ?? null) !== 'admin') { http_response_code(403); echo 'Acesso restrito a administradores.'; exit; } } public static function currentLogin(): ?string { return $_SESSION['login'] ?? null; } public static function currentName(): ?string { return $_SESSION['nome'] ?? null; } public static function isAdmin(): bool { return ($_SESSION['papel'] ?? null) === 'admin'; } private static function isRateLimited(string $login, string $ip): bool { $stmt = Database::pdo()->prepare( 'SELECT COUNT(*) FROM login_attempts WHERE login = :login AND ip = :ip AND sucesso = 0 AND criado_em >= (NOW() - INTERVAL :minutes MINUTE)' ); $stmt->bindValue('login', $login); $stmt->bindValue('ip', $ip); $stmt->bindValue('minutes', self::WINDOW_MINUTES, PDO::PARAM_INT); $stmt->execute(); return (int) $stmt->fetchColumn() >= self::MAX_ATTEMPTS; } private static function logAttempt(string $login, string $ip, bool $success): void { $stmt = Database::pdo()->prepare( 'INSERT INTO login_attempts (login, ip, sucesso) VALUES (:login, :ip, :sucesso)' ); $stmt->execute(['login' => $login, 'ip' => $ip, 'sucesso' => $success ? 1 : 0]); } }