diff --git a/database/2020_05_25_152331_sessions_table_fixes.php b/database/2020_05_25_152331_sessions_table_fixes.php
new file mode 100644
index 00000000..9820beb9
--- /dev/null
+++ b/database/2020_05_25_152331_sessions_table_fixes.php
@@ -0,0 +1,22 @@
+<?php
+namespace Misuzu\DatabaseMigrations\SessionsTableFixes;
+
+use PDO;
+
+function migrate_up(PDO $conn): void {
+    $conn->exec("
+        ALTER TABLE `msz_sessions`
+            CHANGE COLUMN `session_key` `session_key` BINARY(64) NOT NULL AFTER `user_id`,
+            CHANGE COLUMN `session_expires` `session_expires` TIMESTAMP NOT NULL DEFAULT DATE_ADD(NOW(), INTERVAL 1 MONTH) AFTER `session_country`,
+            ADD INDEX `sessions_created_index` (`session_created`);
+    ");
+}
+
+function migrate_down(PDO $conn): void {
+    $conn->exec("
+        ALTER TABLE `msz_sessions`
+            CHANGE COLUMN `session_key` `session_key` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_bin' AFTER `user_id`,
+            CHANGE COLUMN `session_expires` `session_expires` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP() AFTER `session_country`,
+            DROP INDEX `sessions_created_index`;
+    ");
+}
diff --git a/misuzu.php b/misuzu.php
index f7112a0e..41140f9f 100644
--- a/misuzu.php
+++ b/misuzu.php
@@ -8,6 +8,8 @@ use Misuzu\Net\GeoIP;
 use Misuzu\Net\IPAddress;
 use Misuzu\Users\User;
 use Misuzu\Users\UserNotFoundException;
+use Misuzu\Users\UserSession;
+use Misuzu\Users\UserSessionNotFoundException;
 
 define('MSZ_STARTUP', microtime(true));
 define('MSZ_ROOT', __DIR__);
@@ -418,19 +420,49 @@ MIG;
         exit;
     }
 
-    if(!empty($_COOKIE['msz_uid']) && !empty($_COOKIE['msz_sid'])
-        && ctype_digit($_COOKIE['msz_uid']) && ctype_xdigit($_COOKIE['msz_sid'])
-        && strlen($_COOKIE['msz_sid']) === 64) {
-        $_COOKIE['msz_auth'] = Base64::decode(user_session_cookie_pack($_COOKIE['msz_uid'], $_COOKIE['msz_sid']), true);
-        setcookie('msz_auth', $_COOKIE['msz_auth'], strtotime('1 year'), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
+    if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
+        $authToken = (new AuthToken)
+            ->setUserId(filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT) ?? 0)
+            ->setSessionToken(filter_input(INPUT_COOKIE, 'msz_sid', FILTER_SANITIZE_STRING) ?? '');
+
+        if($authToken->isValid())
+            setcookie('msz_auth', $authToken->pack(), strtotime('1 year'), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
+
         setcookie('msz_uid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
         setcookie('msz_sid', '', -3600, '/', '', !empty($_SERVER['HTTPS']), true);
     }
 
-    if(!empty($_COOKIE['msz_auth']) && is_string($_COOKIE['msz_auth'])) {
-        $cookieData = user_session_cookie_unpack(Base64::decode($_COOKIE['msz_auth'], true));
+    if(!isset($authToken))
+        $authToken = AuthToken::unpack(filter_input(INPUT_COOKIE, 'msz_auth', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? '');
+    if($authToken->isValid()) {
+        try {
+            $sessionInfo = $authToken->getSession();
+            if($sessionInfo->hasExpired()) {
+                $sessionInfo->delete();
+            } elseif($sessionInfo->getUserId() === $authToken->getUserId()) {
+                $userInfo = $sessionInfo->getUser();
+                if(!$userInfo->isDeleted()) {
+                    $sessionInfo->setCurrent();
+                    $userInfo->setCurrent();
 
-        if(!empty($cookieData) && user_session_start($cookieData['user_id'], $cookieData['session_token'])) {
+                    $sessionInfo->bump();
+
+                    if($sessionInfo->shouldBumpExpire())
+                        setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
+                }
+            }
+        } catch(UserNotFoundException $ex) {
+            UserSession::unsetCurrent();
+            User::unsetCurrent();
+        } catch(UserSessionNotFoundException $ex) {
+            UserSession::unsetCurrent();
+            User::unsetCurrent();
+        }
+
+        if(!UserSession::hasCurrent()) {
+            setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
+            setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
+        } else {
             $userDisplayInfo = DB::prepare('
                 SELECT
                     u.`user_id`, u.`username`, u.`user_background_settings`, u.`user_deleted`,
@@ -439,38 +471,19 @@ MIG;
                 LEFT JOIN `msz_roles` AS r
                 ON u.`display_role` = r.`role_id`
                 WHERE `user_id` = :user_id
-            ');
-            $userDisplayInfo->bind('user_id', $cookieData['user_id']);
-            $userDisplayInfo = $userDisplayInfo->fetch();
+            ')  ->bind('user_id', $userInfo->getId())
+                ->fetch();
 
-            if($userDisplayInfo) {
-                if(!is_null($userDisplayInfo['user_deleted'])) {
-                    setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
-                    setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
-                    user_session_stop(true);
-                    $userDisplayInfo = [];
-                } else {
-                    try {
-                        User::byId($cookieData['user_id'])->setCurrent();
-                    } catch(UserNotFoundException $ex) {}
+            user_bump_last_active($userInfo->getId());
 
-                    user_bump_last_active($cookieData['user_id']);
-                    user_session_bump_active(user_session_current('session_id'));
-
-                    if(user_session_current('session_expires_bump')) {
-                        setcookie('msz_auth', $_COOKIE['msz_auth'], strtotime('1 month'), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
-                    }
-
-                    $userDisplayInfo['perms'] = perms_get_user($userDisplayInfo['user_id']);
-                    $userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userDisplayInfo['user_id'], MSZ_WARN_BAN);
-                    $userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userDisplayInfo['user_id'], MSZ_WARN_SILENCE);
-                }
-            }
+            $userDisplayInfo['perms'] = perms_get_user($userInfo->getId());
+            $userDisplayInfo['ban_expiration'] = user_warning_check_expiration($userInfo->getId(), MSZ_WARN_BAN);
+            $userDisplayInfo['silence_expiration'] = $userDisplayInfo['ban_expiration'] > 0 ? 0 : user_warning_check_expiration($userInfo->getId(), MSZ_WARN_SILENCE);
         }
     }
 
     CSRF::setGlobalSecretKey(Config::get('csrf.secret', Config::TYPE_STR, 'soup'));
-    CSRF::setGlobalIdentity(empty($userDisplayInfo) ? IPAddress::remote() : $cookieData['session_token']);
+    CSRF::setGlobalIdentity(UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : IPAddress::remote());
 
     if(Config::get('private.enabled', Config::TYPE_BOOL)) {
         $onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login');
@@ -478,14 +491,16 @@ MIG;
         $misuzuBypassLockdown = !empty($misuzuBypassLockdown) || $onLoginPage;
 
         if(!$misuzuBypassLockdown) {
-            if(user_session_active()) {
+            if(UserSession::hasCurrent()) {
                 $privatePermCat = Config::get('private.perm.cat', Config::TYPE_STR);
                 $privatePermVal = Config::get('private.perm.val', Config::TYPE_INT);
 
                 if(!empty($privatePermCat) && $privatePermVal > 0) {
-                    if(!perms_check_user($privatePermCat, $userDisplayInfo['user_id'], $privatePermVal)) {
+                    if(!perms_check_user($privatePermCat, User::getCurrent()->getId(), $privatePermVal)) {
+                        // au revoir
                         unset($userDisplayInfo);
-                        user_session_stop(); // au revoir
+                        UserSession::unsetCurrent();
+                        User::unsetCurrent();
                     }
                 }
             } elseif(!$onLoginPage && !($onPasswordPage && Config::get('private.allow_password_reset', Config::TYPE_BOOL, true))) {
@@ -495,14 +510,13 @@ MIG;
         }
     }
 
-    if(!empty($userDisplayInfo)) {
+    if(!empty($userDisplayInfo)) // delete this
         Template::set('current_user', $userDisplayInfo);
-    }
 
     $inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage');
-    $hasManageAccess = !empty($userDisplayInfo['user_id'])
-        && !user_warning_check_restriction($userDisplayInfo['user_id'])
-        && perms_check_user(MSZ_PERMS_GENERAL, $userDisplayInfo['user_id'], MSZ_PERM_GENERAL_CAN_MANAGE);
+    $hasManageAccess = User::hasCurrent()
+        && !user_warning_check_restriction(User::getCurrent()->getId())
+        && perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_CAN_MANAGE);
     Template::set('has_manage_access', $hasManageAccess);
 
     if($inManageMode) {
@@ -511,6 +525,6 @@ MIG;
             exit;
         }
 
-        Template::set('manage_menu', manage_get_menu($userDisplayInfo['user_id'] ?? 0));
+        Template::set('manage_menu', manage_get_menu(User::getCurrent()->getId()));
     }
 }
diff --git a/public/auth/login.php b/public/auth/login.php
index 9f4ec1b8..73fd74b6 100644
--- a/public/auth/login.php
+++ b/public/auth/login.php
@@ -1,14 +1,17 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\AuthToken;
 use Misuzu\Net\IPAddress;
 use Misuzu\Users\User;
 use Misuzu\Users\UserNotFoundException;
 use Misuzu\Users\UserLoginAttempt;
+use Misuzu\Users\UserSession;
+use Misuzu\Users\UserSessionCreationFailedException;
 
 require_once '../../misuzu.php';
 
-if(user_session_active()) {
+if(UserSession::hasCurrent()) {
     url_redirect('index');
     return;
 }
@@ -23,7 +26,6 @@ $notices = [];
 $siteIsPrivate = Config::get('private.enable', Config::TYPE_BOOL);
 $loginPermCat = $siteIsPrivate ? Config::get('private.perm.cat', Config::TYPE_STR) : '';
 $loginPermVal = $siteIsPrivate ? Config::get('private.perm.val', Config::TYPE_INT) : 0;
-$ipAddress = IPAddress::remote();
 $remainingAttempts = UserLoginAttempt::remaining();
 
 while(!empty($_POST['login']) && is_array($_POST['login'])) {
@@ -32,7 +34,6 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
         break;
     }
 
-    $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
     $loginRedirect = empty($_POST['login']['redirect']) || !is_string($_POST['login']['redirect']) ? '' : $_POST['login']['redirect'];
 
     if(empty($_POST['login']['username']) || empty($_POST['login']['password'])
@@ -54,58 +55,56 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
     $loginFailedError = "Invalid username or password, {$attemptsRemainingError}.";
 
     try {
-        $userData = User::findForLogin($_POST['login']['username']);
+        $userInfo = User::findForLogin($_POST['login']['username']);
     } catch(UserNotFoundException $ex) {
         UserLoginAttempt::create(false);
         $notices[] = $loginFailedError;
         break;
     }
 
-    if(!$userData->hasPassword()) {
+    if(!$userInfo->hasPassword()) {
         $notices[] = 'Your password has been invalidated, please reset it.';
         break;
     }
 
-    if($userData->isDeleted() || !$userData->checkPassword($_POST['login']['password'])) {
-        UserLoginAttempt::create(false, $userData);
+    if($userInfo->isDeleted() || !$userInfo->checkPassword($_POST['login']['password'])) {
+        UserLoginAttempt::create(false, $userInfo);
         $notices[] = $loginFailedError;
         break;
     }
 
-    if($userData->passwordNeedsRehash()) {
-        $userData->setPassword($_POST['login']['password']);
+    if($userInfo->passwordNeedsRehash()) {
+        $userInfo->setPassword($_POST['login']['password']);
     }
 
-    if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userData->getId(), $loginPermVal)) {
+    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.";
-        UserLoginAttempt::create(true, $userData);
+        UserLoginAttempt::create(true, $userInfo);
         break;
     }
 
-    if($userData->hasTOTP()) {
+    if($userInfo->hasTOTP()) {
         url_redirect('auth-two-factor', [
-            'token' => user_auth_tfa_token_create($userData->getId()),
+            'token' => user_auth_tfa_token_create($userInfo->getId()),
         ]);
         return;
     }
 
-    UserLoginAttempt::create(true, $userData);
-    $sessionKey = user_session_create($userData->getId(), $ipAddress, $userAgent);
+    UserLoginAttempt::create(true, $userInfo);
 
-    if(empty($sessionKey)) {
+    try {
+        $sessionInfo = UserSession::create($userInfo);
+        $sessionInfo->setCurrent();
+    } catch(UserSessionCreationFailedException $ex) {
         $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
         break;
     }
 
-    user_session_start($userData->getId(), $sessionKey);
+    $authToken = AuthToken::create($userInfo, $sessionInfo);
+    setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
 
-    $cookieLife = strtotime(user_session_current('session_expires'));
-    $cookieValue = Base64::encode(user_session_cookie_pack($userData->getId(), $sessionKey), true);
-    setcookie('msz_auth', $cookieValue, $cookieLife, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
-
-    if(!is_local_url($loginRedirect)) {
+    if(!is_local_url($loginRedirect))
         $loginRedirect = url('index');
-    }
 
     redirect($loginRedirect);
     return;
diff --git a/public/auth/logout.php b/public/auth/logout.php
index c6e8d66f..e7640aa3 100644
--- a/public/auth/logout.php
+++ b/public/auth/logout.php
@@ -1,9 +1,12 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
+
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+if(!UserSession::hasCurrent()) {
     url_redirect('index');
     return;
 }
@@ -11,7 +14,9 @@ if(!user_session_active()) {
 if(CSRF::validateRequest()) {
     setcookie('msz_auth', '', -9001, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
     setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
-    user_session_stop(true);
+    UserSession::getCurrent()->delete();
+    UserSession::unsetCurrent();
+    User::unsetCurrent();
     url_redirect('index');
     return;
 }
diff --git a/public/auth/password.php b/public/auth/password.php
index ab6d0c1b..821a3c26 100644
--- a/public/auth/password.php
+++ b/public/auth/password.php
@@ -6,10 +6,11 @@ use Misuzu\AuditLog;
 use Misuzu\Net\IPAddress;
 use Misuzu\Users\User;
 use Misuzu\Users\UserLoginAttempt;
+use Misuzu\Users\UserSession;
 
 require_once '../../misuzu.php';
 
-if(user_session_active()) {
+if(UserSession::hasCurrent()) {
     url_redirect('settings-account');
     return;
 }
diff --git a/public/auth/register.php b/public/auth/register.php
index a961171b..b6f68ca6 100644
--- a/public/auth/register.php
+++ b/public/auth/register.php
@@ -5,10 +5,11 @@ use Misuzu\Net\IPAddress;
 use Misuzu\Net\IPAddressBlacklist;
 use Misuzu\Users\User;
 use Misuzu\Users\UserLoginAttempt;
+use Misuzu\Users\UserSession;
 
 require_once '../../misuzu.php';
 
-if(user_session_active()) {
+if(UserSession::hasCurrent()) {
     url_redirect('index');
     return;
 }
diff --git a/public/auth/twofactor.php b/public/auth/twofactor.php
index c7fdf5f0..231034b4 100644
--- a/public/auth/twofactor.php
+++ b/public/auth/twofactor.php
@@ -4,10 +4,12 @@ namespace Misuzu;
 use Misuzu\Net\IPAddress;
 use Misuzu\Users\User;
 use Misuzu\Users\UserLoginAttempt;
+use Misuzu\Users\UserSession;
+use Misuzu\Users\UserSessionCreationFailedException;
 
 require_once '../../misuzu.php';
 
-if(user_session_active()) {
+if(UserSession::hasCurrent()) {
     url_redirect('index');
     return;
 }
@@ -22,11 +24,11 @@ $tokenInfo = user_auth_tfa_token_info(
     )
 );
 
-$userData = User::byId($tokenInfo['user_id']);
+$userInfo = User::byId($tokenInfo['user_id']);
 
 // checking user_totp_key specifically because there's a fringe chance that
 //  there's a token present, but totp is actually disabled
-if(!$userData->hasTOTP()) {
+if(!$userInfo->hasTOTP()) {
     url_redirect('auth-login');
     return;
 }
@@ -50,30 +52,29 @@ while(!empty($twofactor)) {
         break;
     }
 
-    if(!in_array($twofactor['code'], $userData->getValidTOTPTokens())) {
+    if(!in_array($twofactor['code'], $userInfo->getValidTOTPTokens())) {
         $notices[] = sprintf(
             "Invalid two factor code, %d attempt%s remaining",
             $remainingAttempts - 1,
             $remainingAttempts === 2 ? '' : 's'
         );
-        UserLoginAttempt::create(false, $userData);
+        UserLoginAttempt::create(false, $userInfo);
         break;
     }
 
-    UserLoginAttempt::create(true, $userData);
-    $sessionKey = user_session_create($tokenInfo['user_id'], $ipAddress, $userAgent);
+    UserLoginAttempt::create(true, $userInfo);
+    user_auth_tfa_token_invalidate($tokenInfo['tfa_token']);
 
-    if(empty($sessionKey)) {
+    try {
+        $sessionInfo = UserSession::create($userInfo);
+        $sessionInfo->setCurrent();
+    } catch(UserSessionCreationFailedException $ex) {
         $notices[] = "Something broke while creating a session for you, please tell an administrator or developer about this!";
         break;
     }
 
-    user_auth_tfa_token_invalidate($tokenInfo['tfa_token']);
-    user_session_start($tokenInfo['user_id'], $sessionKey);
-
-    $cookieLife = strtotime(user_session_current('session_expires'));
-    $cookieValue = Base64::encode(user_session_cookie_pack($tokenInfo['user_id'], $sessionKey), true);
-    setcookie('msz_auth', $cookieValue, $cookieLife, '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
+    $authToken = AuthToken::create($userInfo, $sessionInfo);
+    setcookie('msz_auth', $authToken->pack(), $sessionInfo->getExpiresTime(), '/', '.' . $_SERVER['HTTP_HOST'], !empty($_SERVER['HTTPS']), true);
 
     if(!is_local_url($redirect)) {
         $redirect = url('index');
diff --git a/public/forum/forum.php b/public/forum/forum.php
index 4b698244..4e4d29eb 100644
--- a/public/forum/forum.php
+++ b/public/forum/forum.php
@@ -1,6 +1,8 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../misuzu.php';
 
 $forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
@@ -12,7 +14,8 @@ if($forumId === 0) {
 }
 
 $forum = forum_get($forumId);
-$forumUserId = user_session_current('user_id', 0);
+$forumUser = User::getCurrent();
+$forumUserId = $forumUser === null ? 0 : $forumUser->getId();
 
 if(empty($forum) || ($forum['forum_type'] == MSZ_FORUM_TYPE_LINK && empty($forum['forum_link']))) {
     echo render_error(404);
diff --git a/public/forum/index.php b/public/forum/index.php
index f2b2608d..e168c337 100644
--- a/public/forum/index.php
+++ b/public/forum/index.php
@@ -1,25 +1,27 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../misuzu.php';
 
 $indexMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $forumId = !empty($_GET['f']) && is_string($_GET['f']) ? (int)$_GET['f'] : 0;
 
+$currentUser = User::getCurrent();
+$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
+
 switch($indexMode) {
     case 'mark':
         url_redirect($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]);
         break;
 
     default:
-        $categories = forum_get_root_categories(user_session_current('user_id', 0));
+        $categories = forum_get_root_categories($currentUserId);
         $blankForum = count($categories) < 1;
 
         foreach($categories as $key => $category) {
-            $categories[$key]['forum_subforums'] = forum_get_children(
-                $category['forum_id'],
-                user_session_current('user_id', 0)
-            );
+            $categories[$key]['forum_subforums'] = forum_get_children($category['forum_id'], $currentUserId);
 
             foreach($categories[$key]['forum_subforums'] as $skey => $sub) {
                 if(!forum_may_have_children($sub['forum_type'])) {
@@ -27,10 +29,7 @@ switch($indexMode) {
                 }
 
                 $categories[$key]['forum_subforums'][$skey]['forum_subforums']
-                    = forum_get_children(
-                        $sub['forum_id'],
-                        user_session_current('user_id', 0)
-                    );
+                    = forum_get_children($sub['forum_id'], $currentUserId);
             }
         }
 
diff --git a/public/forum/leaderboard.php b/public/forum/leaderboard.php
index 90783cb5..97eda9b9 100644
--- a/public/forum/leaderboard.php
+++ b/public/forum/leaderboard.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_FORUM, user_session_current('user_id'), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_FORUM, User::getCurrent()->getId(), MSZ_PERM_FORUM_VIEW_LEADERBOARD)) {
     echo render_error(403);
     return;
 }
@@ -15,7 +17,7 @@ $leaderboardId = !empty($_GET['id']) && is_string($_GET['id'])
     : MSZ_FORUM_LEADERBOARD_CATEGORY_ALL;
 $leaderboardIdLength = strlen($leaderboardId);
 
-$leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null;
+$leaderboardYear  = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null;
 $leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) : null;
 
 $unrankedForums = !empty($_GET['allow_unranked']) ? [] : Config::get('forum_leader.unranked.forum', Config::TYPE_ARR);
diff --git a/public/forum/poll.php b/public/forum/poll.php
index 69bca4c2..fe3832ef 100644
--- a/public/forum/poll.php
+++ b/public/forum/poll.php
@@ -1,6 +1,8 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../misuzu.php';
 
 $redirect = !empty($_SERVER['HTTP_REFERER']) && empty($_SERVER['HTTP_X_MISUZU_XHR']) ? $_SERVER['HTTP_REFERER'] : '';
@@ -18,12 +20,14 @@ if(!CSRF::validateRequest()) {
     return;
 }
 
-if(!user_session_active()) {
+$currentUser = User::getCurrent();
+
+if($currentUser === null) {
     echo render_info_or_json($isXHR, 'You must be logged in to vote on polls.', 401);
     return;
 }
 
-$currentUserId = user_session_current('user_id', 0);
+$currentUserId = $currentUser->getId();
 
 if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
     echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
diff --git a/public/forum/post.php b/public/forum/post.php
index 9c0bf257..2663142c 100644
--- a/public/forum/post.php
+++ b/public/forum/post.php
@@ -2,6 +2,8 @@
 namespace Misuzu;
 
 use Misuzu\AuditLog;
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 
 require_once '../../misuzu.php';
 
@@ -23,12 +25,13 @@ if($isXHR) {
 
 $postRequestVerified = CSRF::validateRequest();
 
-if(!empty($postMode) && !user_session_active()) {
+if(!empty($postMode) && !UserSession::hasCurrent()) {
     echo render_info_or_json($isXHR, 'You must be logged in to manage posts.', 401);
     return;
 }
 
-$currentUserId = (int)user_session_current('user_id', 0);
+$currentUser = User::getCurrent():
+$currentUserId = $currentUser === null ? 0 : $currentUser->getId();
 
 if(user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
     echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
@@ -251,7 +254,7 @@ switch($postMode) {
             break;
         }
 
-        $postFind = forum_post_find($postInfo['post_id'], user_session_current('user_id', 0));
+        $postFind = forum_post_find($postInfo['post_id'], $currentUserId);
 
         if(empty($postFind)) {
             echo render_error(404);
diff --git a/public/forum/posting.php b/public/forum/posting.php
index 2827e514..efaf5908 100644
--- a/public/forum/posting.php
+++ b/public/forum/posting.php
@@ -3,15 +3,20 @@ namespace Misuzu;
 
 use Misuzu\Net\IPAddress;
 use Misuzu\Parsers\Parser;
+use Misuzu\Users\User;
 
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+$currentUser = User::getCurrent();
+
+if($currentUser === null) {
     echo render_error(401);
     return;
 }
 
-if(user_warning_check_restriction(user_session_current('user_id', 0))) {
+$currentUserId = $currentUser->getId();
+
+if(user_warning_check_restriction($currentUserId)) {
     echo render_error(403);
     return;
 }
@@ -83,7 +88,7 @@ if(empty($forum)) {
     return;
 }
 
-$perms = forum_perms_get_user($forum['forum_id'], user_session_current('user_id'))[MSZ_FORUM_PERMS_GENERAL];
+$perms = forum_perms_get_user($forum['forum_id'], $currentUserId)[MSZ_FORUM_PERMS_GENERAL];
 
 if($forum['forum_archived']
     || (!empty($topic['topic_locked']) && !perms_check($perms, MSZ_FORUM_PERM_LOCK_TOPIC))
@@ -121,7 +126,7 @@ if($mode === 'edit') {
         return;
     }
 
-    if(!perms_check($perms, $post['poster_id'] === user_session_current('user_id') ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) {
+    if(!perms_check($perms, $post['poster_id'] === $currentUserId ? MSZ_FORUM_PERM_EDIT_POST : MSZ_FORUM_PERM_EDIT_ANY_POST)) {
         echo render_error(403);
         return;
     }
@@ -142,7 +147,7 @@ if(!empty($_POST)) {
         $isEditingTopic = empty($topic) || ($mode === 'edit' && $post['is_opening_post']);
 
         if($mode === 'create') {
-            $timeoutCheck = max(1, forum_timeout($forumId, user_session_current('user_id')));
+            $timeoutCheck = max(1, forum_timeout($forumId, $currentUserId));
 
             if($timeoutCheck < 5) {
                 $notices[] = sprintf("You're posting too quickly! Please wait %s seconds before posting again.", number_format($timeoutCheck));
@@ -195,7 +200,7 @@ if(!empty($_POST)) {
                     } else {
                         $topicId = forum_topic_create(
                             $forum['forum_id'],
-                            user_session_current('user_id', 0),
+                            $currentUserId,
                             $topicTitle,
                             $topicType
                         );
@@ -204,13 +209,13 @@ if(!empty($_POST)) {
                     $postId = forum_post_create(
                         $topicId,
                         $forum['forum_id'],
-                        user_session_current('user_id', 0),
+                        $currentUserId,
                         IPAddress::remote(),
                         $postText,
                         $postParser,
                         $postSignature
                     );
-                    forum_topic_mark_read(user_session_current('user_id', 0), $topicId, $forum['forum_id']);
+                    forum_topic_mark_read($currentUserId, $topicId, $forum['forum_id']);
                     forum_count_increase($forum['forum_id'], empty($topic));
                     break;
 
@@ -248,7 +253,7 @@ if($mode === 'edit') { // $post is pretty much sure to be populated at this poin
     Template::set('posting_post', $post);
 }
 
-$displayInfo = forum_posting_info(user_session_current('user_id'));
+$displayInfo = forum_posting_info($currentUserId);
 
 Template::render('forum.posting', [
     'posting_breadcrumbs' => forum_get_breadcrumbs($forumId),
diff --git a/public/forum/topic-priority.php b/public/forum/topic-priority.php
index 7ac9b0aa..8f206158 100644
--- a/public/forum/topic-priority.php
+++ b/public/forum/topic-priority.php
@@ -1,14 +1,16 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../misuzu.php';
 
-if(!MSZ_DEBUG) {
+if(!MSZ_DEBUG)
     return;
-}
 
 $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
-$topicUserId = user_session_current('user_id', 0);
+$topicUser = User::getCurrent();
+$topicUserId = $topicUser === null ? 0 : $topicUser->getId();
 
 if($topicUserId < 1) {
     echo render_error(403);
@@ -48,6 +50,6 @@ if(!forum_has_priority_voting($topic['forum_type'])) {
     return;
 }
 
-forum_topic_priority_increase($topicId, user_session_current('user_id', 0));
+forum_topic_priority_increase($topicId, $topicUserId);
 
 url_redirect('forum-topic', ['topic' => $topicId]);
diff --git a/public/forum/topic.php b/public/forum/topic.php
index 409b1e6c..b06329df 100644
--- a/public/forum/topic.php
+++ b/public/forum/topic.php
@@ -2,6 +2,8 @@
 namespace Misuzu;
 
 use Misuzu\AuditLog;
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 
 require_once '../../misuzu.php';
 
@@ -10,7 +12,8 @@ $topicId = !empty($_GET['t']) && is_string($_GET['t']) ? (int)$_GET['t'] : 0;
 $moderationMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
 $submissionConfirmed = !empty($_GET['confirm']) && is_string($_GET['confirm']) && $_GET['confirm'] === '1';
 
-$topicUserId = user_session_current('user_id', 0);
+$topicUser = User::getCurrent();
+$topicUserId = $topicUser === null ? 0 : $this->getId();
 
 if($topicId < 1 && $postId > 0) {
     $postInfo = forum_post_find($postId, $topicUserId);
@@ -91,7 +94,7 @@ if(in_array($moderationMode, $validModerationModes, true)) {
 
     header(CSRF::header());
 
-    if(!user_session_active()) {
+    if(!UserSession::hasCurrent()) {
         echo render_info_or_json($isXHR, 'You must be logged in to manage posts.', 401);
         return;
     }
diff --git a/public/manage/changelog/change.php b/public/manage/changelog/change.php
index c7abeb6d..8d836e87 100644
--- a/public/manage/changelog/change.php
+++ b/public/manage/changelog/change.php
@@ -11,7 +11,7 @@ use Misuzu\Users\UserNotFoundException;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/changelog/index.php b/public/manage/changelog/index.php
index f0a15623..022a507c 100644
--- a/public/manage/changelog/index.php
+++ b/public/manage/changelog/index.php
@@ -2,10 +2,11 @@
 namespace Misuzu;
 
 use Misuzu\Changelog\ChangelogChange;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_CHANGES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/changelog/tag.php b/public/manage/changelog/tag.php
index 870d0270..497cba05 100644
--- a/public/manage/changelog/tag.php
+++ b/public/manage/changelog/tag.php
@@ -8,7 +8,7 @@ use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/changelog/tags.php b/public/manage/changelog/tags.php
index 98ad4d5a..297cea2a 100644
--- a/public/manage/changelog/tags.php
+++ b/public/manage/changelog/tags.php
@@ -2,10 +2,11 @@
 namespace Misuzu;
 
 use Misuzu\Changelog\ChangelogTag;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_CHANGELOG, User::getCurrent()->getId(), MSZ_PERM_CHANGELOG_MANAGE_TAGS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/forum/category.php b/public/manage/forum/category.php
index 897e9684..22a23f36 100644
--- a/public/manage/forum/category.php
+++ b/public/manage/forum/category.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/forum/index.php b/public/manage/forum/index.php
index 346dcce8..1cb0ca80 100644
--- a/public/manage/forum/index.php
+++ b/public/manage/forum/index.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_FORUM_MANAGE_FORUMS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/general/blacklist.php b/public/manage/general/blacklist.php
index cb3d6207..98c2538f 100644
--- a/public/manage/general/blacklist.php
+++ b/public/manage/general/blacklist.php
@@ -2,10 +2,11 @@
 namespace Misuzu;
 
 use Misuzu\Net\IPAddressBlacklist;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_BLACKLIST)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_BLACKLIST)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/general/emoticon.php b/public/manage/general/emoticon.php
index 9922ceb5..5a150015 100644
--- a/public/manage/general/emoticon.php
+++ b/public/manage/general/emoticon.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/general/emoticons.php b/public/manage/general/emoticons.php
index a85c9809..40417ecb 100644
--- a/public/manage/general/emoticons.php
+++ b/public/manage/general/emoticons.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/general/index.php b/public/manage/general/index.php
index 824fbb90..5cf0e996 100644
--- a/public/manage/general/index.php
+++ b/public/manage/general/index.php
@@ -144,11 +144,11 @@ $statistics = DB::query('
         FROM `msz_ip_blacklist`
     ) AS `stat_blacklist`,
     (
-        SELECT COUNT(`attempt_id`)
+        SELECT COUNT(*)
         FROM `msz_login_attempts`
     ) AS `stat_login_attempts_total`,
     (
-        SELECT COUNT(`attempt_id`)
+        SELECT COUNT(*)
         FROM `msz_login_attempts`
         WHERE `attempt_success` = 0
     ) AS `stat_login_attempts_failed`,
diff --git a/public/manage/general/logs.php b/public/manage/general/logs.php
index f8a766b9..a8bd18e3 100644
--- a/public/manage/general/logs.php
+++ b/public/manage/general/logs.php
@@ -3,10 +3,11 @@ namespace Misuzu;
 
 use Misuzu\AuditLog;
 use Misuzu\Pagination;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_VIEW_LOGS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_VIEW_LOGS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/general/settings.php b/public/manage/general/settings.php
index d37a3a53..e1843cd3 100644
--- a/public/manage/general/settings.php
+++ b/public/manage/general/settings.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_GENERAL, User::getCurrent()->getId(), MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/news/categories.php b/public/manage/news/categories.php
index f1b5b1b5..cb3a0b8a 100644
--- a/public/manage/news/categories.php
+++ b/public/manage/news/categories.php
@@ -2,10 +2,11 @@
 namespace Misuzu;
 
 use Misuzu\News\NewsCategory;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/news/category.php b/public/manage/news/category.php
index c660884f..12f34f3b 100644
--- a/public/manage/news/category.php
+++ b/public/manage/news/category.php
@@ -4,10 +4,11 @@ namespace Misuzu;
 use Misuzu\AuditLog;
 use Misuzu\News\NewsCategory;
 use Misuzu\News\NewsCategoryNotFoundException;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/news/post.php b/public/manage/news/post.php
index 08ccc46d..ccdb0b7e 100644
--- a/public/manage/news/post.php
+++ b/public/manage/news/post.php
@@ -5,10 +5,11 @@ use Misuzu\AuditLog;
 use Misuzu\News\NewsCategory;
 use Misuzu\News\NewsPost;
 use Misuzu\News\NewsPostNotFoundException;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_POSTS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) {
     echo render_error(403);
     return;
 }
@@ -31,7 +32,7 @@ if(!empty($_POST['post']) && CSRF::validateRequest()) {
         $isNew = true;
     }
 
-    $currentUserId = user_session_current('user_id');
+    $currentUserId = User::getCurrent()->getId();
     $postInfo->setTitle( $_POST['post']['title'])
         ->setText($_POST['post']['text'])
         ->setCategoryId($_POST['post']['category'])
diff --git a/public/manage/news/posts.php b/public/manage/news/posts.php
index 8ac3f2e3..faeb4dde 100644
--- a/public/manage/news/posts.php
+++ b/public/manage/news/posts.php
@@ -2,10 +2,11 @@
 namespace Misuzu;
 
 use Misuzu\News\NewsPost;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_POSTS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_NEWS, User::getCurrent()->getId(), MSZ_PERM_NEWS_MANAGE_POSTS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/users/index.php b/public/manage/users/index.php
index ce27eae0..c8387a1b 100644
--- a/public/manage/users/index.php
+++ b/public/manage/users/index.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_USERS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/users/role.php b/public/manage/users/role.php
index 564f043e..1beea5a5 100644
--- a/public/manage/users/role.php
+++ b/public/manage/users/role.php
@@ -1,17 +1,18 @@
 <?php
+// TODO: UNFUCK THIS FILE
 namespace Misuzu;
 
-// TODO: UNFUCK THIS FILE
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_ROLES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
     echo render_error(403);
     return;
 }
 
 $roleId = $_GET['r'] ?? null;
-$currentUserId = user_session_current('user_id');
+$currentUserId = User::getCurrent()->getId();
 /*$isSuperUser = user_check_super($currentUserId);
 $canEdit = $isSuperUser || user_check_authority($currentUserId, $userId);*/
 $canEditPerms = /*$canEdit && */perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
diff --git a/public/manage/users/roles.php b/public/manage/users/roles.php
index 02b14a40..a431e636 100644
--- a/public/manage/users/roles.php
+++ b/public/manage/users/roles.php
@@ -1,9 +1,11 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_ROLES)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_ROLES)) {
     echo render_error(403);
     return;
 }
diff --git a/public/manage/users/user.php b/public/manage/users/user.php
index 256927c5..f9e73df7 100644
--- a/public/manage/users/user.php
+++ b/public/manage/users/user.php
@@ -1,16 +1,18 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_USERS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)) {
     echo render_error(403);
     return;
 }
 
 $notices = [];
 $userId = (int)($_GET['u'] ?? 0);
-$currentUserId = user_session_current('user_id');
+$currentUserId = User::getCurrent()->getId();
 
 if($userId < 1) {
     echo render_error(404);
diff --git a/public/manage/users/warnings.php b/public/manage/users/warnings.php
index 53384c29..f87bcaa0 100644
--- a/public/manage/users/warnings.php
+++ b/public/manage/users/warnings.php
@@ -2,16 +2,17 @@
 namespace Misuzu;
 
 use Misuzu\Net\IPAddress;
+use Misuzu\Users\User;
 
 require_once '../../../misuzu.php';
 
-if(!perms_check_user(MSZ_PERMS_USER, user_session_current('user_id'), MSZ_PERM_USER_MANAGE_WARNINGS)) {
+if(!User::hasCurrent() || !perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_WARNINGS)) {
     echo render_error(403);
     return;
 }
 
 $notices = [];
-$currentUserId = user_session_current('user_id');
+$currentUserId = User::getCurrent()->getId();
 
 if(!empty($_POST['lookup']) && is_string($_POST['lookup'])) {
     url_redirect('manage-users-warnings', ['user' => user_id_from_username($_POST['lookup'])]);
diff --git a/public/members.php b/public/members.php
index 2a189e65..989256fb 100644
--- a/public/members.php
+++ b/public/members.php
@@ -1,6 +1,8 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../misuzu.php';
 
 $roleId = !empty($_GET['r']) && is_string($_GET['r']) ? (int)$_GET['r'] : MSZ_ROLE_MAIN;
@@ -75,10 +77,7 @@ if(empty($orderDir)) {
     return;
 }
 
-$canManageUsers = perms_check_user(
-    MSZ_PERMS_USER, user_session_current('user_id', 0),
-    MSZ_PERM_USER_MANAGE_USERS
-);
+$canManageUsers = perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS);
 
 $role = user_role_get($roleId);
 
@@ -153,7 +152,7 @@ $getUsers = DB::prepare(sprintf(
     $usersPagination->getRange()
 ));
 $getUsers->bind('role_id', $role['role_id']);
-$getUsers->bind('current_user_id', user_session_current('user_id', 0));
+$getUsers->bind('current_user_id', User::hasCurrent() ? User::getCurrent()->getId() : 0);
 $users = $getUsers->fetchAll();
 
 if(empty($users)) {
diff --git a/public/profile.php b/public/profile.php
index 82b72868..cb25fe23 100644
--- a/public/profile.php
+++ b/public/profile.php
@@ -4,6 +4,7 @@ namespace Misuzu;
 use Misuzu\Parsers\Parser;
 use Misuzu\Users\User;
 use Misuzu\Users\UserNotFoundException;
+use Misuzu\Users\UserSession;
 
 require_once '../misuzu.php';
 
@@ -21,15 +22,16 @@ try {
 
 $notices = [];
 
-$currentUserId = user_session_current('user_id', 0);
-$viewingAsGuest = $currentUserId === 0;
+$currentUser = User::getCurrent();
+$viewingAsGuest = $currentUser === null;
+$currentUserId = $viewingAsGuest ? 0 : $currentUser->getId();
 $viewingOwnProfile = $currentUserId === $profileUser->getId();
 
 $isBanned = user_warning_check_restriction($profileUser->getId());
 $userPerms = perms_get_user($currentUserId)[MSZ_PERMS_USER];
 $canManageWarnings = perms_check($userPerms, MSZ_PERM_USER_MANAGE_WARNINGS);
 $canEdit = !$isBanned
-    && user_session_active()
+    && UserSession::hasCurrent()
     && (
         $viewingOwnProfile
         || user_check_super($currentUserId)
@@ -297,7 +299,7 @@ $profileStats = DB::prepare(sprintf('
     WHERE `user_id` = :user_id
 ', MSZ_USER_RELATION_FOLLOW))->bind('user_id', $profileUser->getId())->fetch();
 
-$relationInfo = user_session_active()
+$relationInfo = UserSession::hasCurrent()
     ? user_relation_info($currentUserId, $profileUser->getId())
     : [];
 
diff --git a/public/relations.php b/public/relations.php
index 8e3bb363..a13ae3bf 100644
--- a/public/relations.php
+++ b/public/relations.php
@@ -1,6 +1,8 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\User;
+
 require_once '../misuzu.php';
 
 // basing whether or not this is an xhr request on whether a referrer header is present
@@ -22,14 +24,14 @@ if(!CSRF::validateRequest()) {
 
 header(CSRF::header());
 
-if(!user_session_active()) {
+$currentUser = User::getCurrent();
+
+if($currentUser === null) {
     echo render_info_or_json($isXHR, 'You must be logged in to manage relations.', 401);
     return;
 }
 
-$userId = (int)user_session_current('user_id');
-
-if(user_warning_check_expiration($userId, MSZ_WARN_BAN) > 0) {
+if(user_warning_check_expiration($currentUser->getId(), MSZ_WARN_BAN) > 0) {
     echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
     return;
 }
@@ -42,12 +44,12 @@ if(!user_relation_is_valid_type($relationType)) {
     return;
 }
 
-if($userId < 1 || $subjectId < 1) {
+if($currentUser->getId() < 1 || $subjectId < 1) {
     echo render_info_or_json($isXHR, "That user doesn't exist.", 400);
     return;
 }
 
-if(!user_relation_set($userId, $subjectId, $relationType)) {
+if(!user_relation_set($currentUser->getId(), $subjectId, $relationType)) {
     echo render_info_or_json($isXHR, "Failed to save relation.", 500);
     return;
 }
@@ -55,7 +57,7 @@ if(!user_relation_set($userId, $subjectId, $relationType)) {
 
 if(($relationType === MSZ_USER_RELATION_NONE || $relationType === MSZ_USER_RELATION_FOLLOW)
     && in_array($subjectId, Config::get('relations.replicate', Config::TYPE_ARR))) {
-    user_relation_set($subjectId, $userId, $relationType);
+    user_relation_set($subjectId, $currentUser->getId(), $relationType);
 }
 
 if(!$isXHR) {
@@ -64,7 +66,7 @@ if(!$isXHR) {
 }
 
 echo json_encode([
-    'user_id' => $userId,
+    'user_id' => $currentUser->getId(),
     'subject_id' => $subjectId,
     'relation_type' => $relationType,
 ]);
diff --git a/public/search.php b/public/search.php
index ee6e6da6..6a81afb9 100644
--- a/public/search.php
+++ b/public/search.php
@@ -2,13 +2,14 @@
 namespace Misuzu;
 
 use Misuzu\News\NewsPost;
+use Misuzu\Users\User;
 
 require_once '../misuzu.php';
 
 $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
 
 if(!empty($searchQuery)) {
-    $forumTopics = forum_topic_listing_search($searchQuery, user_session_current('user_id', 0));
+    $forumTopics = forum_topic_listing_search($searchQuery, User::hasCurrent() ? User::getCurrent()->getId() : 0);
     $forumPosts = forum_post_search($searchQuery);
     $newsPosts = NewsPost::bySearchQuery($searchQuery);
 
@@ -67,7 +68,7 @@ if(!empty($searchQuery)) {
         MSZ_USER_RELATION_FOLLOW
     ));
     $findUsers->bind('query', $searchQuery);
-    $findUsers->bind('current_user_id', user_session_current('user_id', 0));
+    $findUsers->bind('current_user_id', User::hasCurrent() ? User::getCurrent()->getId() : 0);
     $users = $findUsers->fetchAll();
 }
 
diff --git a/public/settings/account.php b/public/settings/account.php
index 935d7fb3..479ef0fd 100644
--- a/public/settings/account.php
+++ b/public/settings/account.php
@@ -3,12 +3,13 @@ namespace Misuzu;
 
 use Misuzu\AuditLog;
 use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 use chillerlan\QRCode\QRCode;
 use chillerlan\QRCode\QROptions;
 
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+if(!UserSession::hasCurrent()) {
     echo render_error(401);
     return;
 }
diff --git a/public/settings/data.php b/public/settings/data.php
index 4b4037ca..7b2d30f7 100644
--- a/public/settings/data.php
+++ b/public/settings/data.php
@@ -4,10 +4,11 @@ namespace Misuzu;
 use ZipArchive;
 use Misuzu\AuditLog;
 use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+if(!UserSession::hasCurrent()) {
     echo render_error(401);
     return;
 }
diff --git a/public/settings/index.php b/public/settings/index.php
index 7b25723f..865505c6 100644
--- a/public/settings/index.php
+++ b/public/settings/index.php
@@ -1,13 +1,13 @@
 <?php
 namespace Misuzu;
 
+use Misuzu\Users\UserSession;
+
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+if(!UserSession::hasCurrent()) {
     echo render_error(401);
     return;
 }
 
-// do something with this page
-
 url_redirect('settings-account');
diff --git a/public/settings/sessions.php b/public/settings/sessions.php
index fa7d6f6a..2e1064a6 100644
--- a/public/settings/sessions.php
+++ b/public/settings/sessions.php
@@ -2,17 +2,22 @@
 namespace Misuzu;
 
 use Misuzu\AuditLog;
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
+use Misuzu\Users\UserSessionNotFoundException;
 
 require_once '../../misuzu.php';
 
-if(!user_session_active()) {
+if(!User::hasCurrent()) {
     echo render_error(401);
     return;
 }
 
 $errors = [];
-$currentUserId = user_session_current('user_id');
-$sessionActive = user_session_current('session_id');
+$currentUser = User::getCurrent();
+$currentSession = UserSession::getCurrent();
+$currentUserId = $currentUser->getId();
+$sessionActive = $currentSession->getId();;
 
 if(!empty($_POST['session']) && CSRF::validateRequest()) {
     $currentSessionKilled = false;
@@ -20,23 +25,24 @@ if(!empty($_POST['session']) && CSRF::validateRequest()) {
     if(is_array($_POST['session'])) {
         foreach($_POST['session'] as $sessionId) {
             $sessionId = intval($sessionId);
-            $session = user_session_find($sessionId);
 
-            if(!$session || (int)$session['user_id'] !== $currentUserId) {
+            try {
+                $sessionInfo = UserSession::byId($sessionId);
+            } catch(UserSessionNotFoundException $ex) {}
+
+            if(empty($sessionInfo) || $sessionInfo->getUserId() !== $currentUser->getId()) {
                 $errors[] = "Session #{$sessionId} does not exist.";
                 continue;
-            } elseif((int)$session['session_id'] === $sessionActive) {
+            } elseif($sessionInfo->getId() === $sessionActive) {
                 $currentSessionKilled = true;
             }
 
-            user_session_delete($session['session_id']);
-            AuditLog::create(AuditLog::PERSONAL_SESSION_DESTROY, [
-                $session['session_id'],
-            ]);
+            $sessionInfo->delete();
+            AuditLog::create(AuditLog::PERSONAL_SESSION_DESTROY, [$sessionInfo->getId()]);
         }
     } elseif($_POST['session'] === 'all') {
         $currentSessionKilled = true;
-        user_session_purge_all($currentUserId);
+        UserSession::purgeUser($currentUser);
         AuditLog::create(AuditLog::PERSONAL_SESSION_DESTROY_ALL);
     }
 
@@ -46,17 +52,11 @@ if(!empty($_POST['session']) && CSRF::validateRequest()) {
     }
 }
 
-$sessionPagination = new Pagination(user_session_count($currentUserId), 15);
-
-$sessionList = user_session_list(
-    $sessionPagination->getOffset(),
-    $sessionPagination->getRange(),
-    $currentUserId
-);
+$pagination = new Pagination(UserSession::countAll($currentUser), 15);
 
 Template::render('settings.sessions', [
     'errors' => $errors,
-    'session_list' => $sessionList,
-    'session_active_id' => $sessionActive,
-    'session_pagination' => $sessionPagination,
+    'session_list' => UserSession::all($pagination, $currentUser),
+    'session_current' => $currentSession,
+    'session_pagination' => $pagination,
 ]);
diff --git a/public/user-assets.php b/public/user-assets.php
index fc52d4ac..73d07867 100644
--- a/public/user-assets.php
+++ b/public/user-assets.php
@@ -22,7 +22,7 @@ $canViewImages = !$userExists
     || !user_warning_check_expiration($userId, MSZ_WARN_BAN)
     || (
         parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_PATH) === url('user-profile')
-        && perms_check_user(MSZ_PERMS_USER, user_session_current('user_id', 0), MSZ_PERM_USER_MANAGE_USERS)
+        && perms_check_user(MSZ_PERMS_USER, User::hasCurrent() ? User::getCurrent()->getId() : 0, MSZ_PERM_USER_MANAGE_USERS)
     );
 
 switch($userAssetsMode) {
diff --git a/src/AuthToken.php b/src/AuthToken.php
new file mode 100644
index 00000000..90dda21b
--- /dev/null
+++ b/src/AuthToken.php
@@ -0,0 +1,90 @@
+<?php
+namespace Misuzu;
+
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
+
+class AuthToken {
+    public const VERSION = 1;
+    public const WIDTH = 37;
+
+    private $userId = -1;
+    private $sessionToken = '';
+
+    private $user = null;
+    private $session = null;
+
+    public function isValid(): bool {
+        return $this->getUserId() > 0
+            && !empty($this->getSessionToken());
+    }
+
+    public function getUserId(): int {
+        return $this->userId < 1 ? -1 : $this->userId;
+    }
+    public function setUserId(int $userId): self {
+        $this->user = null;
+        $this->userId = $userId;
+        return $this;
+    }
+    public function getUser(): User {
+        if($this->user === null)
+            $this->user = User::byId($this->getUserId());
+        return $this->user;
+    }
+    public function setUser(User $user): self {
+        $this->user = $user;
+        $this->userId = $user->getId();
+        return $this;
+    }
+
+    public function getSessionToken(): string {
+        return $this->sessionToken ?? '';
+    }
+    public function setSessionToken(string $token): self {
+        $this->session = null;
+        $this->sessionToken = $token;
+        return $this;
+    }
+    public function getSession(): UserSession {
+        if($this->session === null)
+            $this->session = UserSession::byToken($this->getSessionToken());
+        return $this->session;
+    }
+    public function setSession(UserSession $session): self {
+        $this->session = $session;
+        $this->sessionToken = $session->getToken();
+        return $this;
+    }
+
+    public function pack(bool $base64 = true): string {
+        $packed = pack('CNH*', self::VERSION, $this->getUserId(), $this->getSessionToken());
+        if($base64)
+            $packed = Base64::encode($packed, true);
+        return $packed;
+    }
+
+    public static function unpack(string $data, bool $base64 = true): self {
+        $obj = new static;
+
+        if(empty($data))
+            return $obj;
+        if($base64)
+            $data = Base64::decode($data, true);
+
+        $data = str_pad($data, self::WIDTH, "\x00");
+        $data = unpack('Cversion/Nuser/H*token', $data);
+
+        if($data['version'] >= 1)
+            $obj->setUserId($data['user'])
+                ->setSessionToken($data['token']);
+
+        return $obj;
+    }
+
+    public static function create(User $user, UserSession $session): self {
+        return (new static)
+            ->setUser($user)
+            ->setSession($session);
+    }
+}
diff --git a/src/Http/Filters/EnforceLogInFilter.php b/src/Http/Filters/EnforceLogInFilter.php
index e665b5ec..73fe69fa 100644
--- a/src/Http/Filters/EnforceLogInFilter.php
+++ b/src/Http/Filters/EnforceLogInFilter.php
@@ -3,10 +3,12 @@ namespace Misuzu\Http\Filters;
 
 use Misuzu\Http\HttpResponseMessage;
 use Misuzu\Http\HttpRequestMessage;
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 
 class EnforceLogInFilter implements FilterInterface {
     public function process(HttpRequestMessage $request): ?HttpResponseMessage {
-        if(!user_session_active())
+        if(!UserSession::hasCurrent() || !User::hasCurrent())
             return new HttpResponseMessage(403);
 
         return null;
diff --git a/src/Http/Filters/EnforceLogOutFilter.php b/src/Http/Filters/EnforceLogOutFilter.php
index 1a6bd182..1f4175cb 100644
--- a/src/Http/Filters/EnforceLogOutFilter.php
+++ b/src/Http/Filters/EnforceLogOutFilter.php
@@ -3,10 +3,12 @@ namespace Misuzu\Http\Filters;
 
 use Misuzu\Http\HttpResponseMessage;
 use Misuzu\Http\HttpRequestMessage;
+use Misuzu\Users\User;
+use Misuzu\Users\UserSession;
 
 class EnforceLogOutFilter implements FilterInterface {
     public function process(HttpRequestMessage $request): ?HttpResponseMessage {
-        if(user_session_active())
+        if(UserSession::hasCurrent() || User::hasCurrent())
             return new HttpResponseMessage(404);
 
         return null;
diff --git a/src/Http/Handlers/ForumHandler.php b/src/Http/Handlers/ForumHandler.php
index 35f46763..9afb40b1 100644
--- a/src/Http/Handlers/ForumHandler.php
+++ b/src/Http/Handlers/ForumHandler.php
@@ -4,6 +4,7 @@ namespace Misuzu\Http\Handlers;
 use HttpResponse;
 use HttpRequest;
 use Misuzu\CSRF;
+use Misuzu\Users\User;
 
 final class ForumHandler extends Handler {
     public function markAsReadGET(HttpResponse $response, HttpRequest $request): void {
@@ -20,7 +21,7 @@ final class ForumHandler extends Handler {
 
     public function markAsReadPOST(HttpResponse $response, HttpRequest $request) {
         $forumId = (int)$request->getBodyParam('forum', FILTER_SANITIZE_NUMBER_INT);
-        forum_mark_read($forumId, user_session_current('user_id'));
+        forum_mark_read($forumId, User::getCurrent()->getId());
 
         $response->redirect(
             url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
diff --git a/src/Http/Handlers/HomeHandler.php b/src/Http/Handlers/HomeHandler.php
index 562b21c1..5efe747e 100644
--- a/src/Http/Handlers/HomeHandler.php
+++ b/src/Http/Handlers/HomeHandler.php
@@ -8,6 +8,7 @@ use Misuzu\DB;
 use Misuzu\Pagination;
 use Misuzu\Changelog\ChangelogChange;
 use Misuzu\News\NewsPost;
+use Misuzu\Users\UserSession;
 
 final class HomeHandler extends Handler {
     public function index(HttpResponse $response, HttpRequest $request): void {
@@ -58,7 +59,7 @@ final class HomeHandler extends Handler {
 
         $changelog = ChangelogChange::all(new Pagination(10));
 
-        $birthdays = user_session_active() ? user_get_birthdays() : [];
+        $birthdays = UserSession::hasCurrent() ? user_get_birthdays() : [];
 
         $latestUser = DB::query('
             SELECT
diff --git a/src/Http/Handlers/SockChatHandler.php b/src/Http/Handlers/SockChatHandler.php
index 693a3fdd..ba8dd084 100644
--- a/src/Http/Handlers/SockChatHandler.php
+++ b/src/Http/Handlers/SockChatHandler.php
@@ -3,6 +3,7 @@ namespace Misuzu\Http\Handlers;
 
 use HttpResponse;
 use HttpRequest;
+use Misuzu\AuthToken;
 use Misuzu\Base64;
 use Misuzu\Config;
 use Misuzu\DB;
@@ -13,6 +14,8 @@ use Misuzu\Users\UserNotFoundException;
 use Misuzu\Users\UserChatToken;
 use Misuzu\Users\UserChatTokenNotFoundException;
 use Misuzu\Users\UserChatTokenCreationFailedException;
+use Misuzu\Users\UserSession;
+use Misuzu\Users\UserSessionNotFoundException;
 
 final class SockChatHandler extends Handler {
     private string $hashKey = 'woomy';
@@ -222,21 +225,27 @@ final class SockChatHandler extends Handler {
             //    $userId = $authInfo->user_id;
         } elseif($authMethod === 'SESS:') {
             $sessionToken = mb_substr($authInfo->token, 5);
-            $tokenData = user_session_cookie_unpack(
-                Base64::decode($sessionToken, true),
-                true
-            );
 
-            if(isset($tokenData['session_token']))
-                $sessionToken = $tokenData['session_token'];
+            $authToken = AuthToken::unpack($sessionToken);
+            if($authToken->isValid())
+                $sessionToken = $authToken->getSessionToken();
 
-            user_session_start($authInfo->user_id, $sessionToken);
+            try {
+                $sessionInfo = UserSession::byToken($sessionToken);
+            } catch(UserSessionNotFoundException $ex) {
+                return ['success' => false, 'reason' => 'token'];
+            }
 
-            if(user_session_active()) {
-                $userId = user_session_current('user_id');
-                user_bump_last_active($userId);
-                user_session_bump_active(user_session_current('session_id'));
-            } else return ['success' => false, 'reason' => 'expired'];
+            if($sessionInfo->getUserId() !== $userInfo->getId())
+                return ['success' => false, 'reason' => 'user'];
+
+            if($sessionInfo->hasExpired()) {
+                $sessionInfo->delete();
+                return ['success' => false, 'reason' => 'expired'];
+            }
+
+            $sessionInfo->bump();
+            user_bump_last_active($userInfo->getId());
         } else {
             try {
                 $token = UserChatToken::byExact($userInfo, $authInfo->token);
diff --git a/src/Users/User.php b/src/Users/User.php
index f904baeb..1b89526b 100644
--- a/src/Users/User.php
+++ b/src/Users/User.php
@@ -173,16 +173,19 @@ class User {
     public static function unsetCurrent(): void {
         self::$localUser = null;
     }
-    public static function getCurrent(): ?User {
+    public static function getCurrent(): ?self {
         return self::$localUser;
     }
+    public static function hasCurrent(): bool {
+        return self::$localUser !== null;
+    }
 
     public static function create(
         string $username,
         string $password,
         string $email,
         string $ipAddress
-    ): ?User {
+    ): ?self {
         $createUser = DB::prepare('
             INSERT INTO `msz_users` (
                 `username`, `password`, `email`, `register_ip`,
@@ -210,7 +213,7 @@ class User {
         return $memoizer;
     }
 
-    public static function byId(int $userId): ?User {
+    public static function byId(int $userId): ?self {
         return self::getMemoizer()->find($userId, function() use ($userId) {
             $user = DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id')
                 ->bind('user_id', $userId)
@@ -220,7 +223,7 @@ class User {
             return $user;
         });
     }
-    public static function findForLogin(string $usernameOrEmail): ?User {
+    public static function findForLogin(string $usernameOrEmail): ?self {
         $usernameOrEmailLower = mb_strtolower($usernameOrEmail);
         return self::getMemoizer()->find(function($user) use ($usernameOrEmailLower) {
             return mb_strtolower($user->getUsername())     === $usernameOrEmailLower
@@ -235,7 +238,7 @@ class User {
             return $user;
         });
     }
-    public static function findForProfile($userIdOrName): ?User {
+    public static function findForProfile($userIdOrName): ?self {
         $userIdOrNameLower = mb_strtolower($userIdOrName);
         return self::getMemoizer()->find(function($user) use ($userIdOrNameLower) {
             return $user->getId() == $userIdOrNameLower || mb_strtolower($user->getUsername()) === $userIdOrNameLower;
diff --git a/src/Users/UserLoginAttempt.php b/src/Users/UserLoginAttempt.php
index 6c77e474..a08bb3a5 100644
--- a/src/Users/UserLoginAttempt.php
+++ b/src/Users/UserLoginAttempt.php
@@ -67,7 +67,7 @@ class UserLoginAttempt {
     }
 
     public static function remaining(?string $remoteAddr = null): int {
-        $remoteAddr = $ipAddress ?? IPAddress::remote();
+        $remoteAddr = $remoteAddr ?? IPAddress::remote();
         return (int)DB::prepare(
             'SELECT 5 - COUNT(*)'
             . ' FROM `' . DB::PREFIX . self::TABLE . '`'
@@ -79,7 +79,7 @@ class UserLoginAttempt {
     }
 
     public static function create(bool $success, ?User $user = null, ?string $remoteAddr = null, string $userAgent = null): void {
-        $remoteAddr = $ipAddress ?? IPAddress::remote();
+        $remoteAddr = $remoteAddr ?? IPAddress::remote();
         $userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? '';
         $createLog = DB::prepare(
             'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `attempt_success`, `attempt_ip`, `attempt_country`, `attempt_user_agent`)'
diff --git a/src/Users/UserSession.php b/src/Users/UserSession.php
new file mode 100644
index 00000000..afaac3fd
--- /dev/null
+++ b/src/Users/UserSession.php
@@ -0,0 +1,256 @@
+<?php
+namespace Misuzu\Users;
+
+use Misuzu\DB;
+use Misuzu\Pagination;
+use Misuzu\Net\IPAddress;
+use WhichBrowser\Parser as UserAgentParser;
+
+class UserSessionException extends UsersException {}
+class UserSessionCreationFailedException extends UserSessionException {}
+class UserSessionNotFoundException extends UserSessionException {}
+
+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_country = 'XX';
+    private $session_expires = null;
+    private $session_expires_bump = 1;
+    private $session_created = null;
+    private $session_active = null;
+
+    private $user = null;
+    private $uaInfo = null;
+
+    private static $localSession = null;
+
+    public const TABLE = 'sessions';
+    private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
+    private const SELECT = '%1$s.`session_id`, %1$s.`user_id`, %1$s.`session_key`, %1$s.`session_user_agent`, %1$s.`session_country`, %1$s.`session_expires_bump`'
+        . ', INET6_NTOA(%1$s.`session_ip`) AS `session_ip`'
+        . ', INET6_NTOA(%1$s.`session_ip_last`) AS `session_ip_last`'
+        . ', UNIX_TIMESTAMP(%1$s.`session_created`) AS `session_created`'
+        . ', UNIX_TIMESTAMP(%1$s.`session_active`) AS `session_active`'
+        . ', UNIX_TIMESTAMP(%1$s.`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 getUserAgentInfo(): UserAgentParser {
+        if($this->uaInfo === null)
+            $this->uaInfo = new UserAgentParser($this->getUserAgent());
+        return $this->uaInfo;
+    }
+
+    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(bool $callUpdate = true, ?int $timestamp = null, ?string $remoteAddr = null): void {
+        $timestamp = $timestamp ?? time();
+        $remoteAddr = $remoteAddr ?? IPAddress::remote();
+
+        $this->setActiveTime($timestamp)
+            ->setLastRemoteAddress($remoteAddr);
+
+        if($this->shouldBumpExpire())
+            $this->setExpiresTime($timestamp + self::LIFETIME);
+
+        if($callUpdate)
+            $this->update();
+    }
+
+    public function delete(): void {
+        DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `session_id` = :session')
+            ->bind('session', $this->getId())
+            ->execute();
+    }
+
+    public static function purgeUser(User $user): void {
+        DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` 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 `' . DB::PREFIX . self::TABLE . '`'
+            . ' 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 = null, ?string $userAgent = null, ?string $token = null): self {
+        $remoteAddr = $remoteAddr ?? IPAddress::remote();
+        $userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? '';
+        $token = $token ?? self::generateToken();
+
+        $sessionId = DB::prepare(
+            'INSERT INTO `' . DB::PREFIX . self::TABLE . '`'
+            . ' (`user_id`, `session_ip`, `session_country`, `session_user_agent`, `session_key`, `session_created`, `session_expires`)'
+            . ' VALUES (:user, INET6_ATON(:remote_addr), :country, :user_agent, :token, NOW(), NOW() + INTERVAL :expires SECOND)'
+        )   ->bind('user', $user->getId())
+            ->bind('remote_addr', $remoteAddr)
+            ->bind('country', IPAddress::country($remoteAddr))
+            ->bind('user_agent', $userAgent)
+            ->bind('token', $token)
+            ->bind('expires', self::LIFETIME)
+            ->executeGetId();
+
+        if($sessionId < 1)
+            throw new UserSessionCreationFailedException;
+
+        return self::byId($sessionId);
+    }
+
+    private static function countQueryBase(): string {
+        return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
+    }
+    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, sprintf(self::SELECT, self::TABLE));
+    }
+    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 UserSessionNotFoundException;
+
+        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 UserSessionNotFoundException;
+
+        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);
+    }
+}
diff --git a/src/Users/session.php b/src/Users/session.php
index e50748a1..3e466145 100644
--- a/src/Users/session.php
+++ b/src/Users/session.php
@@ -1,211 +1,90 @@
 <?php
-define('MSZ_SESSION_KEY_SIZE', 64);
+// These functions are used by external scripts that hook into Misuzu.
+// They will remain in a backwards compatible manner for the time being.
 
-function user_session_create(
-    int $userId,
-    string $ipAddress,
-    string $userAgent
-): string {
-    $sessionKey = user_session_generate_key();
-
-    $createSession = \Misuzu\DB::prepare('
-        INSERT INTO `msz_sessions`
-            (
-                `user_id`, `session_ip`, `session_country`,
-                `session_user_agent`, `session_key`, `session_created`, `session_expires`
-            )
-        VALUES
-            (
-                :user_id, INET6_ATON(:session_ip), :session_country,
-                :session_user_agent, :session_key, NOW(), NOW() + INTERVAL 1 MONTH
-            )
-    ');
-    $createSession->bind('user_id', $userId);
-    $createSession->bind('session_ip', $ipAddress);
-    $createSession->bind('session_country', \Misuzu\Net\IPAddress::country($ipAddress));
-    $createSession->bind('session_user_agent', $userAgent);
-    $createSession->bind('session_key', $sessionKey);
-
-    return $createSession->execute() ? $sessionKey : '';
-}
-
-function user_session_find($sessionId, bool $byKey = false): array {
-    if(!$byKey && $sessionId < 1) {
-        return [];
+function user_session_create(int $userId, string $ipAddress, string $userAgent): string {
+    try {
+        $userInfo = \Misuzu\Users\User::byId($userId);
+    } catch(\Misuzu\Users\UserNotFoundException $ex) {
+        return '';
     }
 
-    $findSession = \Misuzu\DB::prepare(sprintf('
-        SELECT
-            `session_id`, `user_id`,
-            INET6_NTOA(`session_ip`) as `session_ip`,
-            INET6_NTOA(`session_ip_last`) as `session_ip_last`,
-            `session_country`, `session_user_agent`, `session_key`, `session_created`,
-            `session_expires`, `session_active`, `session_expires_bump`
-        FROM `msz_sessions`
-        WHERE `%s` = :session_id
-    ', $byKey ? 'session_key' : 'session_id'));
-    $findSession->bind('session_id', $sessionId);
-    return $findSession->fetch();
-}
-
-function user_session_delete(int $sessionId): void {
-    $deleteSession = \Misuzu\DB::prepare('
-        DELETE FROM `msz_sessions`
-        WHERE `session_id` = :session_id
-    ');
-    $deleteSession->bind('session_id', $sessionId);
-    $deleteSession->execute();
-}
-
-function user_session_generate_key(): string {
-    return bin2hex(random_bytes(MSZ_SESSION_KEY_SIZE / 2));
-}
-
-function user_session_purge_all(int $userId): void {
-    \Misuzu\DB::prepare('
-        DELETE FROM `msz_sessions`
-        WHERE `user_id` = :user_id
-    ')->execute([
-        'user_id' => $userId,
-    ]);
-}
-
-function user_session_count($userId = 0): int {
-    $getCount = \Misuzu\DB::prepare(sprintf('
-        SELECT COUNT(`session_id`)
-        FROM `msz_sessions`
-        %s
-    ', $userId < 1 ? '' : 'WHERE `user_id` = :user_id'));
-
-    if($userId >= 1) {
-        $getCount->bind('user_id', $userId);
+    try {
+        $sessionInfo = \Misuzu\Users\UserSession::create($userInfo, $ipAddress, $userAgent);
+    } catch(\Misuzu\Users\UserSessionCreationFailedException $ex) {
+        return '';
     }
 
-    return (int)$getCount->fetchColumn();
+    return $sessionInfo->getToken();
 }
 
-function user_session_list(int $offset, int $take, int $userId = 0): array {
-    $offset = max(0, $offset);
-    $take = max(1, $take);
-
-    $getSessions = \Misuzu\DB::prepare(sprintf('
-        SELECT
-            `session_id`, `session_country`, `session_user_agent`, `session_created`,
-            `session_expires`, `session_active`, `session_expires_bump`,
-            INET6_NTOA(`session_ip`) as `session_ip`,
-            INET6_NTOA(`session_ip_last`) as `session_ip_last`
-        FROM `msz_sessions`
-        WHERE %s
-        ORDER BY `session_id` DESC
-        LIMIT :offset, :take
-    ', $userId < 1 ? '1' : '`user_id` = :user_id'));
-
-    if($userId > 0) {
-        $getSessions->bind('user_id', $userId);
-    }
-
-    $getSessions->bind('offset', $offset);
-    $getSessions->bind('take', $take);
-
-    return $getSessions->fetchAll();
-}
-
-function user_session_bump_active(int $sessionId, string $ipAddress = null): void {
-    if($sessionId < 1) {
+function user_session_bump_active(int $sessionId, ?string $ipAddress = null): void {
+    try {
+        $sessionInfo = \Misuzu\Users\UserSession::byId($sessionId);
+    } catch(\Misuzu\Users\UserSessionNotFoundException $ex) {
         return;
     }
 
-    $bump = \Misuzu\DB::prepare('
-        UPDATE `msz_sessions`
-        SET `session_active` = NOW(),
-            `session_ip_last` = INET6_ATON(:last_ip),
-            `session_expires` = IF(`session_expires_bump`, NOW() + INTERVAL 1 MONTH, `session_expires`)
-        WHERE `session_id` = :session_id
-    ');
-    $bump->bind('session_id', $sessionId);
-    $bump->bind('last_ip', $ipAddress ?? \Misuzu\Net\IPAddress::remote());
-    $bump->execute();
-}
+    if($ipAddress !== null)
+        $sessionInfo->setLastRemoteAddress($ipAddress);
 
-// the functions below this line are imperative
-
-function user_session_data(?array $newData = null): array {
-    static $data = [];
-
-    if(!is_null($newData)) {
-        $data = $newData;
-    }
-
-    return $data;
+    $sessionInfo->bump();
 }
 
 function user_session_start(int $userId, string $sessionKey): bool {
-    $session = user_session_find($sessionKey, true);
+    $session = \Misuzu\Users\UserSession::getCurrent();
 
-    if(!$session || $session['user_id'] !== $userId) {
+    if($session !== null
+        && $session->getToken() === $sessionKey
+        && $session->getUserId() === $userId)
+        return true;
+
+    try {
+        $session = \Misuzu\Users\UserSession::byToken($sessionKey);
+    } catch(\Misuzu\Users\UserSessionNotFoundException $ex) {
         return false;
     }
 
-    if(time() >= strtotime($session['session_expires'])) {
-        user_session_delete($session['session_id']);
+    if($session->getUserId() !== $userId)
+        return false;
+
+    if($session->hasExpired()) {
+        $session->delete();
         return false;
     }
 
-    user_session_data($session);
+    $session->setCurrent();
     return true;
 }
 
-function user_session_stop(bool $delete = false): void {
-    if(empty(user_session_data())) {
-        return;
-    }
-
-    if($delete) {
-        user_session_delete(user_session_data()['session_id']);
-    }
-
-    user_session_data([]);
-}
-
 function user_session_current(?string $variable = null, $default = null) {
-    if(empty($variable)) {
-        return user_session_data() ?? [];
-    }
+    $getVar = !empty($variable);
+    $session = \Misuzu\Users\UserSession::getCurrent();
 
-    return user_session_data()[$variable] ?? $default;
+    if($session === null)
+        return $getVar ? $default : [];
+
+    $data = [
+        'session_id'           => $session->getId(),
+        'user_id'              => $session->getUserId(),
+        'session_ip'           => $session->getInitialRemoteAddress(),
+        'session_ip_last'      => $session->getLastRemoteAddress(),
+        'session_country'      => $session->getCountry(),
+        'session_user_agent'   => $session->getUserAgent(),
+        'session_key'          => $session->getToken(),
+        'session_created'      => $session->getCreatedTime(),
+        'session_expires'      => $session->getExpiresTime(),
+        'session_active'       => ($date = $session->getActiveTime()) < 0 ? null : $date,
+        'session_expires_bump' => $session->shouldBumpExpire() ? 1 : 0,
+    ];
+
+    if(!$getVar)
+        return $data;
+
+    return $data[$variable] ?? $default;
 }
 
 function user_session_active(): bool {
-    return !empty(user_session_data())
-        && time() < strtotime(user_session_data()['session_expires']);
-}
-
-define('MSZ_SESSION_COOKIE_VERSION', 1);
-// make sure to match this to the final fixed size of the cookie string
-// it'll pad older tokens out for backwards compatibility
-define('MSZ_SESSION_COOKIE_SIZE', 37);
-
-function user_session_cookie_pack(int $userId, string $sessionToken): ?string {
-    if(strlen($sessionToken) !== MSZ_SESSION_KEY_SIZE) {
-        return null;
-    }
-
-    return pack('CNH64', MSZ_SESSION_COOKIE_VERSION, $userId, $sessionToken);
-}
-
-function user_session_cookie_unpack(string $packed): array {
-    $packed = str_pad($packed, MSZ_SESSION_COOKIE_SIZE, "\x00");
-    $unpacked = unpack('Cversion/Nuser/H64token', $packed);
-
-    if($unpacked['version'] < 1 || $unpacked['version'] > MSZ_SESSION_COOKIE_VERSION) {
-        return [];
-    }
-
-    // Make sure this contains all fields with a default for version > 1 exclusive stuff
-    $data = [
-        'user_id' => $unpacked['user'],
-        'session_token' => $unpacked['token'],
-    ];
-
-    return $data;
+    return \Misuzu\Users\UserSession::hasCurrent()
+        && !\Misuzu\Users\UserSession::getCurrent()->hasExpired();
 }
diff --git a/src/manage.php b/src/manage.php
index 9b242f4e..d7e51da3 100644
--- a/src/manage.php
+++ b/src/manage.php
@@ -22,8 +22,8 @@ function manage_get_menu(int $userId): array {
         $menu['Users & Roles']['Users'] = url('manage-users');
     if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_ROLES))
         $menu['Users & Roles']['Roles'] = url('manage-roles');
-    if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_REPORTS))
-        $menu['Users & Roles']['Reports'] = url('manage-users-reports');
+    //if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_REPORTS))
+    //    $menu['Users & Roles']['Reports'] = url('manage-users-reports');
     if(perms_check_user(MSZ_PERMS_USER, $userId, MSZ_PERM_USER_MANAGE_WARNINGS))
         $menu['Users & Roles']['Warnings'] = url('manage-users-warnings');
 
diff --git a/templates/settings/sessions.twig b/templates/settings/sessions.twig
index c9f4e201..5c13bbb2 100644
--- a/templates/settings/sessions.twig
+++ b/templates/settings/sessions.twig
@@ -31,7 +31,7 @@
 
             <div class="settings__sessions__list">
                 {% for session in session_list %}
-                    {{ user_session(session, session.session_id == session_active_id) }}
+                    {{ user_session(session, session_current.id == session.id) }}
                 {% endfor %}
             </div>
 
diff --git a/templates/user/macros.twig b/templates/user/macros.twig
index 99ba6cf2..f188f85a 100644
--- a/templates/user/macros.twig
+++ b/templates/user/macros.twig
@@ -123,18 +123,18 @@
 {% macro user_session(session, is_current_session) %}
     {% from '_layout/input.twig' import input_hidden, input_csrf, input_checkbox_raw %}
 
-    <div class="settings__session{% if is_current_session %} settings__session--current{% endif %}" id="session-{{ session.session_id }}">
+    <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.session_country|lower }} settings__session__flag" title="{{ session.session_country|country_name }}">{{ session.session_country }}</div>
+                <div class="flag flag--{{ session.country|lower }} settings__session__flag" title="{{ session.countryName }}">{{ session.country }}</div>
 
                 <div class="settings__session__description">
-                    {{ session.session_user_agent|default('')|as_platform }}
+                    {{ session.userAgentInfo.toString }}
                 </div>
 
                 <form class="settings__session__actions" method="post" action="{{ url('settings-sessions') }}">
                     {{ input_csrf() }}
-                    {{ input_hidden('session[]', session.session_id) }}
+                    {{ input_hidden('session[]', session.id) }}
 
                     <button class="settings__session__action" title="{{ is_current_session ? 'Logout' : 'End Session' }}">
                         {% if is_current_session %}
@@ -152,46 +152,46 @@
                         Created from IP
                     </div>
                     <div class="settings__session__detail__value">
-                        {{ session.session_ip }}
+                        {{ session.initialRemoteAddress }}
                     </div>
                 </div>
 
-                {% if session.session_ip_last is not null %}
+                {% if session.hasLastRemoteAddress %}
                     <div class="settings__session__detail">
                         <div class="settings__session__detail__title">
                             Last used from IP
                         </div>
                         <div class="settings__session__detail__value">
-                            {{ session.session_ip_last }}
+                            {{ session.lastRemoteAddress }}
                         </div>
                     </div>
                 {% endif %}
 
-                <div class="settings__session__detail" title="{{ session.session_created|date('r') }}">
+                <div class="settings__session__detail" title="{{ session.createdTime|date('r') }}">
                     <div class="settings__session__detail__title">
                         Created
                     </div>
-                    <time class="settings__session__detail__value" datetime="{{ session.session_created|date('c') }}">
-                        {{ session.session_created|time_diff }}
+                    <time class="settings__session__detail__value" datetime="{{ session.createdTime|date('c') }}">
+                        {{ session.createdTime|time_diff }}
                     </time>
                 </div>
 
-                <div class="settings__session__detail" title="{{ session.session_expires|date('r') }}">
+                <div class="settings__session__detail" title="{{ session.expiresTime|date('r') }}">
                     <div class="settings__session__detail__title">
-                        Expires{% if not session.session_expires_bump %} (static){% endif %}
+                        Expires{% if not session.shouldBumpExpire %} (static){% endif %}
                     </div>
-                    <time class="settings__session__detail__value" datetime="{{ session.session_expires|date('c') }}">
-                        {{ session.session_expires|time_diff }}
+                    <time class="settings__session__detail__value" datetime="{{ session.expiresTime|date('c') }}">
+                        {{ session.expiresTime|time_diff }}
                     </time>
                 </div>
 
-                {% if session.session_active is not null %}
-                    <div class="settings__session__detail" title="{{ session.session_active|date('r') }}">
+                {% if session.hasActiveTime %}
+                    <div class="settings__session__detail" title="{{ session.activeTime|date('r') }}">
                         <div class="settings__session__detail__title">
                             Last Active
                         </div>
-                        <time class="settings__session__detail__value" datetime="{{ session.session_active|date('c') }}">
-                            {{ session.session_active|time_diff }}
+                        <time class="settings__session__detail__value" datetime="{{ session.activeTime|date('c') }}">
+                            {{ session.activeTime|time_diff }}
                         </time>
                     </div>
                 {% endif %}
@@ -201,7 +201,7 @@
                         User Agent
                     </div>
                     <div class="settings__session__detail__value">
-                        {{ session.session_user_agent|length > 0 ? session.session_user_agent : 'None' }}
+                        {{ session.userAgent is empty ? 'None' : session.userAgent }}
                     </div>
                 </div>
             </div>