Rewrote Sessions backend.
This commit is contained in:
parent
5c8ffa09fc
commit
3148da4403
23 changed files with 539 additions and 374 deletions
|
@ -3,9 +3,8 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
if(User::hasCurrent()) {
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
@ -38,7 +37,9 @@ $ipAddress = $_SERVER['REMOTE_ADDR'];
|
|||
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
$sessions = $msz->getSessions();
|
||||
$loginAttempts = $msz->getLoginAttempts();
|
||||
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
$siteIsPrivate = $cfg->getBoolean('private.enable');
|
||||
|
@ -80,6 +81,8 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
break;
|
||||
}
|
||||
|
||||
$clientInfo = ClientInfo::fromRequest();
|
||||
|
||||
$attemptsRemainingError = sprintf(
|
||||
"%d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
|
@ -90,7 +93,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
try {
|
||||
$userInfo = User::byUsernameOrEMailAddress($_POST['login']['username']);
|
||||
} catch(RuntimeException $ex) {
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest());
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
}
|
||||
|
@ -101,7 +104,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
}
|
||||
|
||||
if($userInfo->isDeleted() || !$userInfo->checkPassword($_POST['login']['password'])) {
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest(), $userInfo);
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
}
|
||||
|
@ -111,7 +114,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
|
||||
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userInfo->getId(), $loginPermVal)) {
|
||||
$notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest(), $userInfo);
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -123,11 +126,10 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
return;
|
||||
}
|
||||
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest(), $userInfo);
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::create($userInfo, $ipAddress, $countryCode);
|
||||
$sessionInfo->setCurrent();
|
||||
$sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
|
||||
} catch(RuntimeException $ex) {
|
||||
$notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
|
||||
break;
|
||||
|
|
|
@ -2,18 +2,15 @@
|
|||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(!UserSession::hasCurrent()) {
|
||||
if(!User::hasCurrent()) {
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
||||
if(CSRF::validateRequest()) {
|
||||
$msz->getSessions()->deleteSessions(sessionTokens: $authToken->getSessionToken());
|
||||
AuthToken::nukeCookie();
|
||||
UserSession::getCurrent()->delete();
|
||||
UserSession::unsetCurrent();
|
||||
User::unsetCurrent();
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
if(User::hasCurrent()) {
|
||||
url_redirect('settings-account');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
if(User::hasCurrent()) {
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
if(User::hasCurrent()) {
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
|
@ -16,8 +15,10 @@ $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|||
$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
|
||||
$notices = [];
|
||||
|
||||
$sessions = $msz->getSessions();
|
||||
$tfaSessions = $msz->getTFASessions();
|
||||
$loginAttempts = $msz->getLoginAttempts();
|
||||
|
||||
$remainingAttempts = $loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
$tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
|
||||
|
@ -58,22 +59,23 @@ while(!empty($twofactor)) {
|
|||
break;
|
||||
}
|
||||
|
||||
$clientInfo = ClientInfo::fromRequest();
|
||||
|
||||
if(!in_array($twofactor['code'], $userInfo->getValidTOTPTokens())) {
|
||||
$notices[] = sprintf(
|
||||
"Invalid two factor code, %d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
$remainingAttempts === 2 ? '' : 's'
|
||||
);
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest(), $userInfo);
|
||||
$loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
break;
|
||||
}
|
||||
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, ClientInfo::fromRequest(), $userInfo);
|
||||
$loginAttempts->recordAttempt(true, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
|
||||
$tfaSessions->deleteToken($tokenString);
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::create($userInfo, $ipAddress, $countryCode);
|
||||
$sessionInfo->setCurrent();
|
||||
$sessionInfo = $sessions->createSession($userInfo, $ipAddress, $countryCode, $userAgent, $clientInfo);
|
||||
} catch(RuntimeException $ex) {
|
||||
$notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
|
||||
break;
|
||||
|
@ -82,9 +84,8 @@ while(!empty($twofactor)) {
|
|||
$authToken = AuthToken::create($userInfo, $sessionInfo);
|
||||
$authToken->applyCookie($sessionInfo->getExpiresTime());
|
||||
|
||||
if(!is_local_url($redirect)) {
|
||||
if(!is_local_url($redirect))
|
||||
$redirect = url('index');
|
||||
}
|
||||
|
||||
redirect($redirect);
|
||||
return;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
|
||||
$postMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
|
||||
|
@ -10,7 +9,7 @@ $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) &
|
|||
|
||||
$postRequestVerified = CSRF::validateRequest();
|
||||
|
||||
if(!empty($postMode) && !UserSession::hasCurrent()) {
|
||||
if(!empty($postMode) && !User::hasCurrent()) {
|
||||
echo render_info('You must be logged in to manage posts.', 401);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
|
||||
$topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
|
||||
|
@ -78,7 +77,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
|
|||
return;
|
||||
}
|
||||
|
||||
if(!UserSession::hasCurrent()) {
|
||||
if(!User::hasCurrent()) {
|
||||
echo render_info('You must be logged in to manage posts.', 401);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ use Index\ByteFormat;
|
|||
use Misuzu\Parsers\Parser;
|
||||
use Misuzu\Profile\ProfileFields;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Users\Assets\UserBackgroundAsset;
|
||||
|
||||
$userId = !empty($_GET['u']) && is_string($_GET['u']) ? trim($_GET['u']) : 0;
|
||||
|
|
|
@ -3,11 +3,10 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
|
||||
if(!UserSession::hasCurrent()) {
|
||||
if(!User::hasCurrent()) {
|
||||
echo render_error(401);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,8 @@ use ZipArchive;
|
|||
use Index\XString;
|
||||
use Index\IO\FileStream;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(!UserSession::hasCurrent()) {
|
||||
if(!User::hasCurrent()) {
|
||||
echo render_error(401);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\UserSession;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
if(!UserSession::hasCurrent()) {
|
||||
if(!User::hasCurrent()) {
|
||||
echo render_error(401);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
if(!User::hasCurrent()) {
|
||||
echo render_error(401);
|
||||
|
@ -11,49 +10,52 @@ if(!User::hasCurrent()) {
|
|||
}
|
||||
|
||||
$errors = [];
|
||||
$sessions = $msz->getSessions();
|
||||
$currentUser = User::getCurrent();
|
||||
$currentSession = UserSession::getCurrent();
|
||||
$currentUserId = $currentUser->getId();
|
||||
$sessionActive = $currentSession->getId();;
|
||||
$activeSessionToken = $authToken->getSessionToken();
|
||||
|
||||
if(!empty($_POST['session']) && CSRF::validateRequest()) {
|
||||
$currentSessionKilled = false;
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
$sessionId = (string)filter_input(INPUT_POST, 'session');
|
||||
$activeSessionKilled = false;
|
||||
|
||||
if(is_array($_POST['session'])) {
|
||||
foreach($_POST['session'] as $sessionId) {
|
||||
$sessionId = (int)$sessionId;
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::byId($sessionId);
|
||||
} catch(RuntimeException $ex) {}
|
||||
|
||||
if(empty($sessionInfo) || $sessionInfo->getUserId() !== $currentUser->getId()) {
|
||||
$errors[] = "Session #{$sessionId} does not exist.";
|
||||
continue;
|
||||
} elseif($sessionInfo->getId() === $sessionActive) {
|
||||
$currentSessionKilled = true;
|
||||
}
|
||||
|
||||
$sessionInfo->delete();
|
||||
$msz->createAuditLog('PERSONAL_SESSION_DESTROY', [$sessionInfo->getId()]);
|
||||
}
|
||||
} elseif($_POST['session'] === 'all') {
|
||||
$currentSessionKilled = true;
|
||||
UserSession::purgeUser($currentUser);
|
||||
if($sessionId === 'all') {
|
||||
$activeSessionKilled = true;
|
||||
$sessions->deleteSessions(userInfos: $currentUser);
|
||||
$msz->createAuditLog('PERSONAL_SESSION_DESTROY_ALL');
|
||||
} else {
|
||||
try {
|
||||
$sessionInfo = $sessions->getSession(sessionId: $sessionId);
|
||||
} catch(RuntimeException $ex) {}
|
||||
|
||||
if(empty($sessionInfo) || $sessionInfo->getUserId() !== (string)$currentUser->getId()) {
|
||||
$errors[] = "That session doesn't exist.";
|
||||
break;
|
||||
}
|
||||
|
||||
$activeSessionKilled = $sessionInfo->getToken() === $activeSessionToken;
|
||||
$sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||
$msz->createAuditLog('PERSONAL_SESSION_DESTROY', [$sessionInfo->getId()]);
|
||||
}
|
||||
|
||||
if($currentSessionKilled) {
|
||||
if($activeSessionKilled) {
|
||||
url_redirect('index');
|
||||
return;
|
||||
}
|
||||
} else break;
|
||||
}
|
||||
|
||||
$pagination = new Pagination(UserSession::countAll($currentUser), 15);
|
||||
$pagination = new Pagination($sessions->countSessions(userInfo: $currentUser), 10);
|
||||
|
||||
$sessionList = [];
|
||||
$sessionInfos = $sessions->getSessions(userInfo: $currentUser, pagination: $pagination);
|
||||
|
||||
foreach($sessionInfos as $sessionInfo)
|
||||
$sessionList[] = [
|
||||
'info' => $sessionInfo,
|
||||
'active' => $sessionInfo->getToken() === $activeSessionToken,
|
||||
];
|
||||
|
||||
Template::render('settings.sessions', [
|
||||
'errors' => $errors,
|
||||
'session_list' => UserSession::all($pagination, $currentUser),
|
||||
'session_current' => $currentSession,
|
||||
'session_list' => $sessionList,
|
||||
'session_pagination' => $pagination,
|
||||
]);
|
||||
|
|
|
@ -3,7 +3,6 @@ namespace Misuzu;
|
|||
|
||||
use RuntimeException;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
require_once __DIR__ . '/../misuzu.php';
|
||||
|
||||
|
@ -95,20 +94,22 @@ if(!isset($authToken))
|
|||
$authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth') ?? '');
|
||||
|
||||
if($authToken->isValid()) {
|
||||
$sessions = $msz->getSessions();
|
||||
$authToken->setCurrent();
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::byToken($authToken->getSessionToken());
|
||||
if($sessionInfo->hasExpired()) {
|
||||
$sessionInfo->delete();
|
||||
} elseif($sessionInfo->getUserId() === $authToken->getUserId()) {
|
||||
$userInfo = $sessionInfo->getUser();
|
||||
if(!$userInfo->isDeleted()) {
|
||||
$sessionInfo->setCurrent();
|
||||
$userInfo->setCurrent();
|
||||
$sessionInfo->bump($_SERVER['REMOTE_ADDR']);
|
||||
$sessionInfo = $sessions->getSession(sessionToken: $authToken->getSessionToken());
|
||||
|
||||
if($sessionInfo->shouldBumpExpire())
|
||||
if($sessionInfo->hasExpired()) {
|
||||
$sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||
} elseif($sessionInfo->getUserId() === (string)$authToken->getUserId()) {
|
||||
$userInfo = User::byId((int)$sessionInfo->getUserId());
|
||||
|
||||
if(!$userInfo->isDeleted()) {
|
||||
$userInfo->setCurrent();
|
||||
|
||||
$sessions->updateSession(sessionInfo: $sessionInfo, remoteAddr: $_SERVER['REMOTE_ADDR']);
|
||||
if($sessionInfo->shouldBumpExpires())
|
||||
$authToken->applyCookie($sessionInfo->getExpiresTime());
|
||||
|
||||
// only allow impersonation when super user
|
||||
|
@ -128,11 +129,10 @@ if($authToken->isValid()) {
|
|||
}
|
||||
}
|
||||
} catch(RuntimeException $ex) {
|
||||
UserSession::unsetCurrent();
|
||||
User::unsetCurrent();
|
||||
}
|
||||
|
||||
if(UserSession::hasCurrent()) {
|
||||
if(User::hasCurrent()) {
|
||||
$userInfo->bumpActivity($_SERVER['REMOTE_ADDR']);
|
||||
} else
|
||||
AuthToken::nukeCookie();
|
||||
|
@ -140,7 +140,7 @@ if($authToken->isValid()) {
|
|||
|
||||
CSRF::init(
|
||||
$globals['csrf.secret'],
|
||||
(UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : ($_SERVER['REMOTE_ADDR'] ?? '::1'))
|
||||
(User::hasCurrent() ? $authToken->getSessionToken() : $_SERVER['REMOTE_ADDR'])
|
||||
);
|
||||
|
||||
if(!empty($userInfo)) {
|
||||
|
|
121
src/Auth/SessionInfo.php
Normal file
121
src/Auth/SessionInfo.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbResult;
|
||||
use Index\Net\IPAddress;
|
||||
use Misuzu\ClientInfo;
|
||||
|
||||
class SessionInfo {
|
||||
private string $id;
|
||||
private string $userId;
|
||||
private string $token;
|
||||
private string $firstRemoteAddr;
|
||||
private ?string $lastRemoteAddr;
|
||||
private string $userAgent;
|
||||
private string $clientInfo;
|
||||
private string $countryCode;
|
||||
private int $expires;
|
||||
private bool $bumpExpires;
|
||||
private int $created;
|
||||
private ?int $lastActive;
|
||||
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->id = (string)$result->getInteger(0);
|
||||
$this->userId = (string)$result->getInteger(1);
|
||||
$this->token = $result->getString(2);
|
||||
$this->firstRemoteAddr = $result->getString(3);
|
||||
$this->lastRemoteAddr = $result->isNull(4) ? null : $result->getString(4);
|
||||
$this->userAgent = $result->getString(5);
|
||||
$this->clientInfo = $result->getString(6);
|
||||
$this->countryCode = $result->getString(7);
|
||||
$this->expires = $result->getInteger(8);
|
||||
$this->bumpExpires = $result->getInteger(9) !== 0;
|
||||
$this->created = $result->getInteger(10);
|
||||
$this->lastActive = $result->isNull(11) ? null : $result->getInteger(11);
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUserId(): string {
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getFirstRemoteAddressRaw(): string {
|
||||
return $this->firstRemoteAddr;
|
||||
}
|
||||
|
||||
public function getFirstRemoteAddress(): IPAddress {
|
||||
return IPAddress::parse($this->firstRemoteAddr);
|
||||
}
|
||||
|
||||
public function hasLastRemoteAddress(): bool {
|
||||
return $this->lastRemoteAddr !== null;
|
||||
}
|
||||
|
||||
public function getLastRemoteAddressRaw(): string {
|
||||
return $this->lastRemoteAddr;
|
||||
}
|
||||
|
||||
public function getLastRemoteAddress(): ?IPAddress {
|
||||
return $this->lastRemoteAddr === null ? null : IPAddress::parse($this->lastRemoteAddr);
|
||||
}
|
||||
|
||||
public function getUserAgentString(): string {
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
public function getClientInfoRaw(): string {
|
||||
return $this->clientInfo;
|
||||
}
|
||||
|
||||
public function getClientInfo(): ClientInfo {
|
||||
return ClientInfo::decode($this->clientInfo);
|
||||
}
|
||||
|
||||
public function getCountryCode(): string {
|
||||
return $this->countryCode;
|
||||
}
|
||||
|
||||
public function getExpiresTime(): int {
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
public function getExpiresAt(): DateTime {
|
||||
return DateTime::fromUnixTimeSeconds($this->expires);
|
||||
}
|
||||
|
||||
public function shouldBumpExpires(): bool {
|
||||
return $this->bumpExpires;
|
||||
}
|
||||
|
||||
public function hasExpired(): bool {
|
||||
return $this->expires < time();
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTime {
|
||||
return DateTime::fromUnixTimeSeconds($this->created);
|
||||
}
|
||||
|
||||
public function hasLastActive(): bool {
|
||||
return $this->lastActive !== null;
|
||||
}
|
||||
|
||||
public function getLastActiveTime(): ?int {
|
||||
return $this->lastActive;
|
||||
}
|
||||
|
||||
public function getLastActiveAt(): ?DateTime {
|
||||
return $this->lastActive === null ? null : DateTime::fromUnixTimeSeconds($this->lastActive);
|
||||
}
|
||||
}
|
285
src/Auth/Sessions.php
Normal file
285
src/Auth/Sessions.php
Normal file
|
@ -0,0 +1,285 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Index\Data\DbStatementCache;
|
||||
use Index\Data\DbTools;
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Net\IPAddress;
|
||||
use Misuzu\ClientInfo;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class Sessions {
|
||||
private IDbConnection $dbConn;
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(IDbConnection $dbConn) {
|
||||
$this->dbConn = $dbConn;
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
// would like to un-hex this but need to make sure AuthToken doesn't have an aneurysm over it
|
||||
public static function generateToken(): string {
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
public function countSessions(
|
||||
User|string|null $userInfo = null
|
||||
): int {
|
||||
if($userInfo instanceof User)
|
||||
$userInfo = (string)$userInfo->getId();
|
||||
|
||||
$hasUserInfo = $userInfo !== null;
|
||||
|
||||
//$args = 0;
|
||||
$query = 'SELECT COUNT(*) FROM msz_sessions';
|
||||
if($hasUserInfo) {
|
||||
//++$args;
|
||||
$query .= ' WHERE user_id = ?';
|
||||
}
|
||||
|
||||
$args = 0;
|
||||
$stmt = $this->cache->get($query);
|
||||
if($hasUserInfo)
|
||||
$stmt->addParameter(++$args, $userInfo);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$count = 0;
|
||||
|
||||
if($result->next())
|
||||
$count = $result->getInteger(0);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function getSessions(
|
||||
User|string|null $userInfo = null,
|
||||
?Pagination $pagination = null
|
||||
): array {
|
||||
if($userInfo instanceof User)
|
||||
$userInfo = (string)$userInfo->getId();
|
||||
|
||||
$hasUserInfo = $userInfo !== null;
|
||||
$hasPagination = $pagination !== null;
|
||||
|
||||
//$args = 0;
|
||||
$query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
|
||||
if($hasUserInfo) {
|
||||
//++$args;
|
||||
$query .= ' WHERE user_id = ?';
|
||||
}
|
||||
$query .= ' ORDER BY session_active DESC';
|
||||
if($hasPagination)
|
||||
$query .= ' LIMIT ? OFFSET ?';
|
||||
|
||||
$args = 0;
|
||||
$stmt = $this->cache->get($query);
|
||||
if($hasUserInfo)
|
||||
$stmt->addParameter(++$args, $userInfo);
|
||||
if($hasPagination) {
|
||||
$stmt->addParameter(++$args, $pagination->getRange());
|
||||
$stmt->addParameter(++$args, $pagination->getOffset());
|
||||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
$sessions = [];
|
||||
|
||||
while($result->next())
|
||||
$sessions[] = new SessionInfo($result);
|
||||
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
public function getSession(
|
||||
?string $sessionId = null,
|
||||
?string $sessionToken = null
|
||||
): SessionInfo {
|
||||
if($sessionId === null && $sessionToken === null)
|
||||
throw new InvalidArgumentException('At least one argument must be specified.');
|
||||
if($sessionId !== null && $sessionToken !== null)
|
||||
throw new InvalidArgumentException('Only one argument may be specified.');
|
||||
|
||||
$hasSessionId = $sessionId !== null;
|
||||
$hasSessionToken = $sessionToken !== null;
|
||||
$value = null;
|
||||
|
||||
$query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
|
||||
if($hasSessionId) {
|
||||
$query .= ' WHERE session_id = ?';
|
||||
$value = $sessionId;
|
||||
} elseif($hasSessionToken) {
|
||||
$query .= ' WHERE session_key = ?';
|
||||
$value = $sessionToken;
|
||||
}
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
$stmt->addParameter(1, $value);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('Session not found.');
|
||||
|
||||
return new SessionInfo($result);
|
||||
}
|
||||
|
||||
public function createSession(
|
||||
User|string $userInfo,
|
||||
IPAddress|string $remoteAddr,
|
||||
string $countryCode,
|
||||
string $userAgentString,
|
||||
?ClientInfo $clientInfo = null
|
||||
): SessionInfo {
|
||||
if($userInfo instanceof User)
|
||||
$userInfo = (string)$userInfo->getId();
|
||||
if($remoteAddr instanceof IPAddress)
|
||||
$remoteAddr = (string)$remoteAddr;
|
||||
|
||||
$sessionToken = self::generateToken();
|
||||
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
|
||||
|
||||
$stmt = $this->cache->get('INSERT INTO msz_sessions (user_id, session_key, session_ip, session_user_agent, session_client_info, session_country, session_expires) VALUES (?, ?, INET6_ATON(?), ?, ?, ?, NOW() + INTERVAL 1 MONTH)');
|
||||
$stmt->addParameter(1, $userInfo);
|
||||
$stmt->addParameter(2, $sessionToken);
|
||||
$stmt->addParameter(3, $remoteAddr);
|
||||
$stmt->addParameter(4, $userAgentString);
|
||||
$stmt->addParameter(5, $clientInfo);
|
||||
$stmt->addParameter(6, $countryCode);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId());
|
||||
}
|
||||
|
||||
public function deleteSessions(
|
||||
SessionInfo|string|array|null $sessionInfos = null,
|
||||
string|array|null $sessionTokens = null,
|
||||
User|string|array|null $userInfos = null
|
||||
): void {
|
||||
$hasSessionInfos = $sessionInfos !== null;
|
||||
$hasSessionTokens = $sessionTokens !== null;
|
||||
$hasUserInfos = $userInfos !== null;
|
||||
|
||||
$args = 0;
|
||||
$query = 'DELETE FROM msz_sessions';
|
||||
|
||||
if($hasSessionInfos) {
|
||||
if(!is_array($sessionInfos))
|
||||
$sessionInfos = [$sessionInfos];
|
||||
|
||||
if(empty($sessionInfos))
|
||||
$hasSessionInfos = false;
|
||||
else {
|
||||
++$args;
|
||||
$query .= sprintf(
|
||||
' WHERE session_id IN (%s)',
|
||||
DbTools::prepareListString($sessionInfos)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if($hasSessionTokens) {
|
||||
if(!is_array($sessionTokens))
|
||||
$sessionTokens = [$sessionTokens];
|
||||
|
||||
if(empty($sessionTokens))
|
||||
$hasSessionTokens = false;
|
||||
else
|
||||
$query .= sprintf(
|
||||
' %s session_key IN (%s)',
|
||||
++$args > 1 ? 'OR' : 'WHERE',
|
||||
DbTools::prepareListString($sessionTokens)
|
||||
);
|
||||
}
|
||||
|
||||
if($hasUserInfos) {
|
||||
if(!is_array($userInfos))
|
||||
$userInfos = [$userInfos];
|
||||
|
||||
if(empty($userInfos))
|
||||
$hasUserInfos = false;
|
||||
else
|
||||
$query .= sprintf(
|
||||
' %s user_id IN (%s)',
|
||||
++$args > 1 ? 'OR' : 'WHERE',
|
||||
DbTools::prepareListString($userInfos)
|
||||
);
|
||||
}
|
||||
|
||||
if(!$hasSessionInfos && !$hasSessionTokens && !$hasUserInfos)
|
||||
throw new InvalidArgumentException('At least one argument must be specified.');
|
||||
|
||||
$args = 0;
|
||||
$stmt = $this->cache->get($query);
|
||||
|
||||
if($hasSessionInfos)
|
||||
foreach($sessionInfos as $sessionInfo) {
|
||||
if($sessionInfo instanceof SessionInfo)
|
||||
$sessionInfo = $sessionInfo->getId();
|
||||
elseif(!is_string($sessionInfo))
|
||||
throw new InvalidArgumentException('$sessionInfos must be strings or instances of SessionInfo.');
|
||||
|
||||
$stmt->addParameter(++$args, $sessionInfo);
|
||||
}
|
||||
|
||||
if($hasSessionTokens)
|
||||
foreach($sessionTokens as $sessionToken) {
|
||||
if(!is_string($sessionToken))
|
||||
throw new InvalidArgumentException('$sessionTokens must be strings.');
|
||||
|
||||
$stmt->addParameter(++$args, $sessionToken);
|
||||
}
|
||||
|
||||
if($hasUserInfos)
|
||||
foreach($userInfos as $userInfo) {
|
||||
if($userInfo instanceof User)
|
||||
$userInfo = (string)$userInfo->getId();
|
||||
elseif(!is_string($userInfo))
|
||||
throw new InvalidArgumentException('$userInfos must be strings or instances of User.');
|
||||
|
||||
$stmt->addParameter(++$args, $userInfo);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function updateSession(
|
||||
SessionInfo|string|null $sessionInfo = null,
|
||||
?string $sessionToken = null,
|
||||
IPAddress|string|null $remoteAddr = null
|
||||
): void {
|
||||
if($sessionInfo === null && $sessionToken === null)
|
||||
throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
|
||||
if($sessionInfo !== null && $sessionToken !== null)
|
||||
throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
|
||||
if($sessionInfo instanceof SessionInfo)
|
||||
$sessionInfo = $sessionInfo->getId();
|
||||
if($remoteAddr instanceof IPAddress)
|
||||
$remoteAddr = (string)$remoteAddr;
|
||||
|
||||
$hasSessionInfo = $sessionInfo !== null;
|
||||
$hasSessionToken = $sessionToken !== null;
|
||||
$value = null;
|
||||
|
||||
$query = 'UPDATE msz_sessions SET session_ip_last = COALESCE(INET6_ATON(?), session_ip_last), session_active = NOW(), session_expires = IF(session_expires_bump, NOW() + INTERVAL 1 MONTH, session_expires)';
|
||||
if($hasSessionInfo) {
|
||||
$query .= ' WHERE session_id = ?';
|
||||
$value = $sessionInfo;
|
||||
} elseif($hasSessionToken) {
|
||||
$query .= ' WHERE session_key = ?';
|
||||
$value = $sessionToken;
|
||||
} else throw new RuntimeException('Failsafe to prevent all sessions from being updated at once somehow.');
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
$stmt->addParameter(1, $remoteAddr);
|
||||
$stmt->addParameter(2, $value);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function purgeExpiredSessions(): void {
|
||||
$this->dbConn->execute('DELETE FROM msz_sessions WHERE session_expires <= NOW()');
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
use Index\IO\MemoryStream;
|
||||
use Index\Serialisation\UriBase64;
|
||||
use Misuzu\Auth\SessionInfo;
|
||||
use Misuzu\Users\User;
|
||||
|
||||
class AuthToken {
|
||||
private const EPOCH = 1682985600;
|
||||
|
@ -166,10 +166,10 @@ class AuthToken {
|
|||
return time() - self::EPOCH;
|
||||
}
|
||||
|
||||
public static function create(User $user, UserSession $session): self {
|
||||
public static function create(User $userInfo, SessionInfo $sessionInfo): self {
|
||||
$token = new AuthToken;
|
||||
$token->setUserId($user->getId());
|
||||
$token->setSessionToken($session->getToken());
|
||||
$token->setUserId($userInfo->getId());
|
||||
$token->setSessionToken($sessionInfo->getToken());
|
||||
return $token;
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,7 @@ class AuthToken {
|
|||
|
||||
// please never use the below functions beyond the scope of the sharpchat auth stuff
|
||||
// a better mechanism for keeping a global instance of this available
|
||||
// that isn't a $GLOBAL variable or static instance needs to be established, for User and UserSession as well
|
||||
// that isn't a $GLOBAL variable or static instance needs to be established, for User as well
|
||||
|
||||
private static $localToken = null;
|
||||
|
||||
|
|
|
@ -4,11 +4,10 @@ namespace Misuzu\Http\Handlers;
|
|||
use Misuzu\CSRF;
|
||||
use Misuzu\Template;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
final class ForumHandler extends Handler {
|
||||
public function markAsReadGET($response, $request) {
|
||||
if(!UserSession::hasCurrent() || !User::hasCurrent())
|
||||
if(!User::hasCurrent())
|
||||
return 403;
|
||||
|
||||
$forumId = (int)$request->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
@ -23,7 +22,7 @@ final class ForumHandler extends Handler {
|
|||
}
|
||||
|
||||
public function markAsReadPOST($response, $request) {
|
||||
if(!UserSession::hasCurrent() || !User::hasCurrent())
|
||||
if(!User::hasCurrent())
|
||||
return 403;
|
||||
|
||||
if(!$request->isFormContent())
|
||||
|
|
|
@ -7,11 +7,10 @@ use Misuzu\Pagination;
|
|||
use Misuzu\Template;
|
||||
use Misuzu\Comments\CommentsCategory;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
final class HomeHandler extends Handler {
|
||||
public function index($response, $request): void {
|
||||
if(UserSession::hasCurrent())
|
||||
if(User::hasCurrent())
|
||||
$this->home($response, $request);
|
||||
else
|
||||
$this->landing($response, $request);
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Misuzu;
|
|||
use Misuzu\Template;
|
||||
use Misuzu\Auth\LoginAttempts;
|
||||
use Misuzu\Auth\RecoveryTokens;
|
||||
use Misuzu\Auth\Sessions;
|
||||
use Misuzu\Auth\TwoFactorAuthSessions;
|
||||
use Misuzu\AuditLog\AuditLog;
|
||||
use Misuzu\Changelog\Changelog;
|
||||
|
@ -50,6 +51,7 @@ class MisuzuContext {
|
|||
private TwoFactorAuthSessions $tfaSessions;
|
||||
private Roles $roles;
|
||||
private Users $users;
|
||||
private Sessions $sessions;
|
||||
|
||||
public function __construct(IDbConnection $dbConn, IConfig $config) {
|
||||
$this->dbConn = $dbConn;
|
||||
|
@ -67,6 +69,7 @@ class MisuzuContext {
|
|||
$this->tfaSessions = new TwoFactorAuthSessions($this->dbConn);
|
||||
$this->roles = new Roles($this->dbConn);
|
||||
$this->users = new Users($this->dbConn);
|
||||
$this->sessions = new Sessions($this->dbConn);
|
||||
}
|
||||
|
||||
public function getDbConn(): IDbConnection {
|
||||
|
@ -146,6 +149,10 @@ class MisuzuContext {
|
|||
return $this->users;
|
||||
}
|
||||
|
||||
public function getSessions(): Sessions {
|
||||
return $this->sessions;
|
||||
}
|
||||
|
||||
private array $activeBansCache = [];
|
||||
|
||||
public function tryGetActiveBan(User|string|null $userInfo = null): ?BanInfo {
|
||||
|
@ -241,7 +248,7 @@ class MisuzuContext {
|
|||
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
|
||||
$this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST'));
|
||||
|
||||
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->bans, $this->emotes);
|
||||
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->bans, $this->emotes, $this->sessions);
|
||||
}
|
||||
|
||||
private function registerLegacyRedirects(): void {
|
||||
|
|
|
@ -6,24 +6,32 @@ use Index\Colour\Colour;
|
|||
use Index\Routing\IRouter;
|
||||
use Index\Http\HttpFx;
|
||||
use Misuzu\AuthToken;
|
||||
use Misuzu\Auth\Sessions;
|
||||
use Misuzu\Config\IConfig;
|
||||
use Misuzu\Emoticons\Emotes;
|
||||
use Misuzu\Users\Bans;
|
||||
|
||||
// Replace
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserSession;
|
||||
|
||||
final class SharpChatRoutes {
|
||||
private IConfig $config;
|
||||
private Bans $bans;
|
||||
private Emotes $emotes;
|
||||
private Sessions $sessions;
|
||||
private string $hashKey;
|
||||
|
||||
public function __construct(IRouter $router, IConfig $config, Bans $bans, Emotes $emotes) {
|
||||
public function __construct(
|
||||
IRouter $router,
|
||||
IConfig $config,
|
||||
Bans $bans,
|
||||
Emotes $emotes,
|
||||
Sessions $sessions
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->bans = $bans;
|
||||
$this->emotes = $emotes;
|
||||
$this->sessions = $sessions;
|
||||
$this->hashKey = $this->config->getString('hashKey', 'woomy');
|
||||
|
||||
// Simplify default error pages
|
||||
|
@ -119,22 +127,28 @@ final class SharpChatRoutes {
|
|||
if($request->getMethod() === 'OPTIONS')
|
||||
return 204;
|
||||
|
||||
if(!UserSession::hasCurrent() || !AuthToken::hasCurrent())
|
||||
return ['ok' => false];
|
||||
if(!AuthToken::hasCurrent())
|
||||
return ['ok' => false, 'err' => 'token'];
|
||||
|
||||
$token = AuthToken::getCurrent();
|
||||
$session = UserSession::getCurrent();
|
||||
if($session->getToken() !== $token->getSessionToken())
|
||||
return ['ok' => false];
|
||||
|
||||
$user = $session->getUser();
|
||||
$userId = $token->hasImpersonatedUserId() && $user->isSuper()
|
||||
try {
|
||||
$sessionInfo = $this->sessions->getSession(sessionToken: $token->getSessionToken());
|
||||
} catch(RuntimeException $ex) {
|
||||
return ['ok' => false, 'err' => 'session'];
|
||||
}
|
||||
|
||||
if($sessionInfo->hasExpired())
|
||||
return ['ok' => false, 'err' => 'expired'];
|
||||
|
||||
$userInfo = User::byId((int)$sessionInfo->getUserId());
|
||||
$userId = $token->hasImpersonatedUserId() && $userInfo->isSuper()
|
||||
? $token->getImpersonatedUserId()
|
||||
: $user->getId();
|
||||
: $userInfo->getId();
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'usr' => $userId,
|
||||
'usr' => (int)$userId,
|
||||
'tkn' => $token->pack(),
|
||||
];
|
||||
}
|
||||
|
@ -198,19 +212,19 @@ final class SharpChatRoutes {
|
|||
$authToken = $authTokenInfo->getSessionToken();
|
||||
|
||||
try {
|
||||
$sessionInfo = UserSession::byToken($authToken);
|
||||
$sessionInfo = $this->sessions->getSession(sessionToken: $authToken);
|
||||
} catch(RuntimeException $ex) {
|
||||
return ['success' => false, 'reason' => 'token'];
|
||||
}
|
||||
|
||||
if($sessionInfo->hasExpired()) {
|
||||
$sessionInfo->delete();
|
||||
$this->sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||
return ['success' => false, 'reason' => 'expired'];
|
||||
}
|
||||
|
||||
$sessionInfo->bump($ipAddress);
|
||||
$this->sessions->updateSession(sessionInfo: $sessionInfo, remoteAddr: $ipAddress);
|
||||
|
||||
$userInfo = $sessionInfo->getUser();
|
||||
$userInfo = User::byId((int)$sessionInfo->getUserId());
|
||||
if($authTokenInfo->hasImpersonatedUserId() && $userInfo->isSuper()) {
|
||||
$userInfoReal = $userInfo;
|
||||
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu\Users;
|
||||
|
||||
use RuntimeException;
|
||||
use Misuzu\ClientInfo;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Pagination;
|
||||
|
||||
class UserSession {
|
||||
public const TOKEN_SIZE = 64;
|
||||
public const LIFETIME = 60 * 60 * 24 * 31;
|
||||
|
||||
// Database fields
|
||||
private $session_id = -1;
|
||||
private $user_id = -1;
|
||||
private $session_key = '';
|
||||
private $session_ip = '::1';
|
||||
private $session_ip_last = null;
|
||||
private $session_user_agent = '';
|
||||
private $session_client_info = '';
|
||||
private $session_country = 'XX';
|
||||
private $session_expires = null;
|
||||
private $session_expires_bump = 1;
|
||||
private $session_created = null;
|
||||
private $session_active = null;
|
||||
|
||||
private $user = null;
|
||||
|
||||
private static $localSession = null;
|
||||
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `msz_sessions`';
|
||||
private const SELECT = '`session_id`, `user_id`, `session_key`, `session_user_agent`, `session_client_info`, `session_country`, `session_expires_bump`'
|
||||
. ', INET6_NTOA(`session_ip`) AS `session_ip`'
|
||||
. ', INET6_NTOA(`session_ip_last`) AS `session_ip_last`'
|
||||
. ', UNIX_TIMESTAMP(`session_created`) AS `session_created`'
|
||||
. ', UNIX_TIMESTAMP(`session_active`) AS `session_active`'
|
||||
. ', UNIX_TIMESTAMP(`session_expires`) AS `session_expires`';
|
||||
|
||||
public function getId(): int {
|
||||
return $this->session_id < 1 ? -1 : $this->session_id;
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function getUser(): User {
|
||||
if($this->user === null)
|
||||
$this->user = User::byId($this->getUserId());
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->session_key;
|
||||
}
|
||||
|
||||
public function getInitialRemoteAddress(): string {
|
||||
return $this->session_ip;
|
||||
}
|
||||
|
||||
public function getLastRemoteAddress(): string {
|
||||
return $this->session_ip_last ?? '';
|
||||
}
|
||||
public function hasLastRemoteAddress(): bool {
|
||||
return !empty($this->session_ip_last);
|
||||
}
|
||||
public function setLastRemoteAddress(string $remoteAddr): self {
|
||||
$this->session_ip_last = $remoteAddr;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUserAgent(): string {
|
||||
return $this->session_user_agent;
|
||||
}
|
||||
public function getClientInfo(): ClientInfo {
|
||||
return ClientInfo::decode($this->session_client_info);
|
||||
}
|
||||
|
||||
public function getCountry(): string {
|
||||
return $this->session_country;
|
||||
}
|
||||
public function getCountryName(): string {
|
||||
return get_country_name($this->getCountry());
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->session_created === null ? -1 : $this->session_created;
|
||||
}
|
||||
|
||||
public function getActiveTime(): int {
|
||||
return $this->session_active === null ? -1 : $this->session_active;
|
||||
}
|
||||
public function hasActiveTime(): bool {
|
||||
return $this->session_active !== null;
|
||||
}
|
||||
public function setActiveTime(int $timestamp): self {
|
||||
if($timestamp > $this->session_active)
|
||||
$this->session_active = $timestamp;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpiresTime(): int {
|
||||
return $this->session_expires === null ? -1 : $this->session_expires;
|
||||
}
|
||||
public function setExpiresTime(int $timestamp): self {
|
||||
$this->session_expires = $timestamp;
|
||||
return $this;
|
||||
}
|
||||
public function hasExpired(): bool {
|
||||
return $this->getExpiresTime() <= time();
|
||||
}
|
||||
|
||||
public function shouldBumpExpire(): bool {
|
||||
return boolval($this->session_expires_bump);
|
||||
}
|
||||
|
||||
public function bump(string $remoteAddr, int $timestamp = -1): void {
|
||||
if($timestamp < 0)
|
||||
$timestamp = time();
|
||||
|
||||
$this->setActiveTime($timestamp)
|
||||
->setLastRemoteAddress($remoteAddr);
|
||||
|
||||
if($this->shouldBumpExpire())
|
||||
$this->setExpiresTime($timestamp + self::LIFETIME);
|
||||
|
||||
$this->update();
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
DB::prepare('DELETE FROM `msz_sessions` WHERE `session_id` = :session')
|
||||
->bind('session', $this->getId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
public static function purgeUser(User $user): void {
|
||||
DB::prepare('DELETE FROM `msz_sessions` WHERE `user_id` = :user')
|
||||
->bind('user', $user->getId())
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function setCurrent(): void {
|
||||
self::$localSession = $this;
|
||||
}
|
||||
public static function unsetCurrent(): void {
|
||||
self::$localSession = null;
|
||||
}
|
||||
public static function getCurrent(): ?self {
|
||||
return self::$localSession;
|
||||
}
|
||||
public static function hasCurrent(): bool {
|
||||
return self::$localSession !== null;
|
||||
}
|
||||
|
||||
public static function generateToken(): string {
|
||||
return bin2hex(random_bytes(self::TOKEN_SIZE / 2));
|
||||
}
|
||||
|
||||
public function update(): void {
|
||||
DB::prepare(
|
||||
'UPDATE `msz_sessions`'
|
||||
. ' SET `session_active` = FROM_UNIXTIME(:active), `session_ip_last` = INET6_ATON(:remote_addr), `session_expires` = FROM_UNIXTIME(:expires)'
|
||||
. ' WHERE `session_id` = :session'
|
||||
) ->bind('active', $this->session_active)
|
||||
->bind('remote_addr', $this->session_ip_last)
|
||||
->bind('expires', $this->session_expires)
|
||||
->bind('session', $this->session_id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
public static function create(
|
||||
User $user,
|
||||
string $remoteAddr,
|
||||
string $countryCode,
|
||||
?string $userAgent = null,
|
||||
?ClientInfo $clientInfo = null
|
||||
): self {
|
||||
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') ?? '';
|
||||
$clientInfo ??= ClientInfo::parse($_SERVER);
|
||||
$token = self::generateToken();
|
||||
|
||||
$sessionId = DB::prepare(
|
||||
'INSERT INTO `msz_sessions`'
|
||||
. ' (`user_id`, `session_ip`, `session_country`, `session_user_agent`, `session_client_info`, `session_key`, `session_created`, `session_expires`)'
|
||||
. ' VALUES (:user, INET6_ATON(:remote_addr), :country, :user_agent, :client_info, :token, NOW(), NOW() + INTERVAL :expires SECOND)'
|
||||
) ->bind('user', $user->getId())
|
||||
->bind('remote_addr', $remoteAddr)
|
||||
->bind('country', $countryCode)
|
||||
->bind('user_agent', $userAgent)
|
||||
->bind('client_info', $clientInfo->encode())
|
||||
->bind('token', $token)
|
||||
->bind('expires', self::LIFETIME)
|
||||
->executeGetId();
|
||||
|
||||
if($sessionId < 1)
|
||||
throw new RuntimeException('Failed to create new session.');
|
||||
|
||||
return self::byId($sessionId);
|
||||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, 'COUNT(*)');
|
||||
}
|
||||
public static function countAll(?User $user = null): int {
|
||||
$getCount = DB::prepare(
|
||||
self::countQueryBase()
|
||||
. ($user === null ? '' : ' WHERE `user_id` = :user')
|
||||
);
|
||||
if($user !== null)
|
||||
$getCount->bind('user', $user->getId());
|
||||
return (int)$getCount->fetchColumn();
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, self::SELECT);
|
||||
}
|
||||
public static function byId(int $sessionId): self {
|
||||
$session = DB::prepare(self::byQueryBase() . ' WHERE `session_id` = :session_id')
|
||||
->bind('session_id', $sessionId)
|
||||
->fetchObject(self::class);
|
||||
|
||||
if(!$session)
|
||||
throw new RuntimeException('Could not find a session with that ID.');
|
||||
|
||||
return $session;
|
||||
}
|
||||
public static function byToken(string $token): self {
|
||||
$session = DB::prepare(self::byQueryBase() . ' WHERE `session_key` = :token')
|
||||
->bind('token', $token)
|
||||
->fetchObject(self::class);
|
||||
|
||||
if(!$session)
|
||||
throw new RuntimeException('Could not find a session with that token.');
|
||||
|
||||
return $session;
|
||||
}
|
||||
public static function all(?Pagination $pagination = null, ?User $user = null): array {
|
||||
$sessionsQuery = self::byQueryBase()
|
||||
. ($user === null ? '' : ' WHERE `user_id` = :user')
|
||||
. ' ORDER BY `session_created` DESC';
|
||||
|
||||
if($pagination !== null)
|
||||
$sessionsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getSessions = DB::prepare($sessionsQuery);
|
||||
|
||||
if($user !== null)
|
||||
$getSessions->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getSessions->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getSessions->fetchObjects(self::class);
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<div class="settings__sessions__list">
|
||||
{% for session in session_list %}
|
||||
{{ user_session(session, session_current.id == session.id) }}
|
||||
{{ user_session(session.info, session.active) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<div class="settings__session{% if is_current_session %} settings__session--current{% endif %}" id="session-{{ session.id }}">
|
||||
<div class="settings__session__container">
|
||||
<div class="settings__session__important">
|
||||
<div class="flag flag--{{ session.country|lower }} settings__session__flag" title="{{ session.countryName }}">{{ session.country }}</div>
|
||||
<div class="flag flag--{{ session.countryCode|lower }} settings__session__flag" title="{{ session.countryCode|country_name }}">{{ session.countryCode }}</div>
|
||||
|
||||
<div class="settings__session__description">
|
||||
{{ session.clientInfo }}
|
||||
|
@ -78,7 +78,7 @@
|
|||
|
||||
<form class="settings__session__actions" method="post" action="{{ url('settings-sessions') }}">
|
||||
{{ input_csrf() }}
|
||||
{{ input_hidden('session[]', session.id) }}
|
||||
{{ input_hidden('session', session.id) }}
|
||||
|
||||
<button class="settings__session__action" title="{{ is_current_session ? 'Logout' : 'End Session' }}">
|
||||
{% if is_current_session %}
|
||||
|
@ -96,7 +96,7 @@
|
|||
Created from IP
|
||||
</div>
|
||||
<div class="settings__session__detail__value">
|
||||
{{ session.initialRemoteAddress }}
|
||||
{{ session.firstRemoteAddress }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -122,20 +122,20 @@
|
|||
|
||||
<div class="settings__session__detail" title="{{ session.expiresTime|date('r') }}">
|
||||
<div class="settings__session__detail__title">
|
||||
Expires{% if not session.shouldBumpExpire %} (static){% endif %}
|
||||
Expires{% if not session.shouldBumpExpires %} (static){% endif %}
|
||||
</div>
|
||||
<time class="settings__session__detail__value" datetime="{{ session.expiresTime|date('c') }}">
|
||||
{{ session.expiresTime|time_format }}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
{% if session.hasActiveTime %}
|
||||
<div class="settings__session__detail" title="{{ session.activeTime|date('r') }}">
|
||||
{% if session.hasLastActive %}
|
||||
<div class="settings__session__detail" title="{{ session.lastActiveTime|date('r') }}">
|
||||
<div class="settings__session__detail__title">
|
||||
Last Active
|
||||
</div>
|
||||
<time class="settings__session__detail__value" datetime="{{ session.activeTime|date('c') }}">
|
||||
{{ session.activeTime|time_format }}
|
||||
<time class="settings__session__detail__value" datetime="{{ session.lastActiveTime|date('c') }}">
|
||||
{{ session.lastActiveTime|time_format }}
|
||||
</time>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -145,7 +145,7 @@
|
|||
User Agent
|
||||
</div>
|
||||
<div class="settings__session__detail__value">
|
||||
{{ session.userAgent is empty ? 'None' : session.userAgent }}
|
||||
{{ session.userAgentString }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue