2023-07-22 16:37:57 +00:00
|
|
|
<?php
|
|
|
|
namespace Misuzu\Auth;
|
|
|
|
|
2023-07-27 23:49:55 +00:00
|
|
|
use Index\TimeSpan;
|
2023-07-22 16:37:57 +00:00
|
|
|
use Index\Data\DbStatementCache;
|
|
|
|
use Index\Data\IDbConnection;
|
|
|
|
use Index\Net\IPAddress;
|
|
|
|
use Misuzu\ClientInfo;
|
|
|
|
use Misuzu\Pagination;
|
|
|
|
use Misuzu\Users\User;
|
|
|
|
|
|
|
|
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,
|
|
|
|
User|string|null $userInfo = null,
|
|
|
|
IPAddress|string|null $remoteAddr = null,
|
|
|
|
TimeSpan|int|null $timeRange = null
|
|
|
|
): int {
|
|
|
|
if($userInfo instanceof User)
|
|
|
|
$userInfo = (string)$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,
|
|
|
|
User|string|null $userInfo = null,
|
|
|
|
IPAddress|string|null $remoteAddr = null,
|
|
|
|
TimeSpan|int|null $timeRange = null,
|
|
|
|
?Pagination $pagination = null
|
|
|
|
): array {
|
|
|
|
if($userInfo instanceof User)
|
|
|
|
$userInfo = (string)$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();
|
|
|
|
|
|
|
|
$result = $stmt->getResult();
|
|
|
|
$attempts = [];
|
|
|
|
|
|
|
|
while($result->next())
|
|
|
|
$attempts[] = new LoginAttemptInfo($result);
|
|
|
|
|
|
|
|
return $attempts;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function recordAttempt(
|
|
|
|
bool $success,
|
|
|
|
IPAddress|string $remoteAddr,
|
|
|
|
string $countryCode,
|
|
|
|
string $userAgentString,
|
|
|
|
?ClientInfo $clientInfo = null,
|
|
|
|
User|string|null $userInfo = null
|
|
|
|
): void {
|
|
|
|
if($remoteAddr instanceof IPAddress)
|
|
|
|
$remoteAddr = (string)$remoteAddr;
|
|
|
|
if($userInfo instanceof User)
|
|
|
|
$userInfo = (string)$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();
|
|
|
|
}
|
|
|
|
}
|