misuzu/src/Auth/LoginAttempts.php

159 lines
5.7 KiB
PHP

<?php
namespace Misuzu\Auth;
use Index\TimeSpan;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class LoginAttempts {
public const REMAINING_MAX = 5;
public const REMAINING_WINDOW = 60 * 60;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function countAttempts(
?bool $success = null,
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
$hasTimeRange = $timeRange !== null;
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_login_attempts';
if($hasSuccess) {
++$args;
$query .= sprintf(' WHERE attempt_success %s 0', $success ? '<>' : '=');
}
if($hasUserInfo)
$query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasRemoteAddr)
$query .= sprintf(' %s attempt_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
if($hasTimeRange)
$query .= sprintf(' %s attempt_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE');
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasRemoteAddr)
$stmt->addParameter(++$args, $remoteAddr);
if($hasTimeRange)
$stmt->addParameter(++$args, $timeRange);
$stmt->execute();
$result = $stmt->getResult();
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function countRemainingAttempts(IPAddress|string $remoteAddr): int {
return self::REMAINING_MAX - $this->countAttempts(
success: false,
timeRange: self::REMAINING_WINDOW,
remoteAddr: $remoteAddr
);
}
public function getAttempts(
?bool $success = null,
UserInfo|string|null $userInfo = null,
IPAddress|string|null $remoteAddr = null,
TimeSpan|int|null $timeRange = null,
?Pagination $pagination = null
): iterable {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($timeRange instanceof TimeSpan)
$timeRange = (int)$timeRange->totalSeconds();
$hasSuccess = $success !== null;
$hasUserInfo = $userInfo !== null;
$hasRemoteAddr = $remoteAddr !== null;
$hasTimeRange = $timeRange !== null;
$hasPagination = $pagination !== null;
$args = 0;
$query = 'SELECT user_id, attempt_success, INET6_NTOA(attempt_ip), attempt_country, UNIX_TIMESTAMP(attempt_created), attempt_user_agent, attempt_client_info FROM msz_login_attempts';
if($hasSuccess) {
++$args;
$query .= sprintf(' WHERE attempt_success %s 0', $success ? '<>' : '=');
}
if($hasUserInfo)
$query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasRemoteAddr)
$query .= sprintf(' %s attempt_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
if($hasTimeRange)
$query .= sprintf(' %s attempt_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE');
$query .= ' ORDER BY attempt_created DESC';
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasRemoteAddr)
$stmt->addParameter(++$args, $remoteAddr);
if($hasTimeRange)
$stmt->addParameter(++$args, $timeRange);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return $stmt->getResult()->getIterator(LoginAttemptInfo::fromResult(...));
}
public function recordAttempt(
bool $success,
IPAddress|string $remoteAddr,
string $countryCode,
string $userAgentString,
?ClientInfo $clientInfo = null,
UserInfo|string|null $userInfo = null
): void {
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
$stmt = $this->cache->get('INSERT INTO msz_login_attempts (user_id, attempt_success, attempt_ip, attempt_country, attempt_user_agent, attempt_client_info) VALUES (?, ?, INET6_ATON(?), ?, ?, ?)');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $success ? 1 : 0);
$stmt->addParameter(3, $remoteAddr);
$stmt->addParameter(4, $countryCode);
$stmt->addParameter(5, $userAgentString);
$stmt->addParameter(6, $clientInfo);
$stmt->execute();
}
}