Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user sessions page #3278

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f225647
Save device information on login
supercrafter100 Oct 10, 2022
b7652fe
Users sessions page
supercrafter100 Oct 10, 2022
68905ea
PR comments & device detection improvements
supercrafter100 Oct 10, 2022
78e9530
Fix code-style
supercrafter100 Oct 10, 2022
22698f1
Store IP in session & display on the user sessions page
supercrafter100 Oct 10, 2022
ef04f2d
Merge branch 'develop' into feat/user-sessions
supercrafter100 Oct 10, 2022
5718dfe
Don't show session hash, add last seen
supercrafter100 Oct 10, 2022
3c1f860
Fix last seen translation, ...
supercrafter100 Oct 10, 2022
dd2d0b9
Unnecesary Output::getClean() - Partydragen
supercrafter100 Oct 10, 2022
cb6d276
Apparently I did use admin/sessions language term
supercrafter100 Oct 10, 2022
37ffa04
Show location of ip
supercrafter100 Oct 10, 2022
3843682
Update custom/panel_templates/Default/core/users_sessions.tpl
supercrafter100 Oct 11, 2022
0bd16e6
Update custom/languages/en_UK.json
supercrafter100 Oct 11, 2022
5bc92a5
Update modules/Core/pages/panel/users_sessions.php
supercrafter100 Oct 11, 2022
ed3cfe0
Merge branch 'develop' into feat/user-sessions
supercrafter100 Oct 11, 2022
648f3b4
Update modules/Core/pages/panel/users_sessions.php
supercrafter100 Oct 11, 2022
a6dd683
use country the second time
supercrafter100 Oct 16, 2022
c1c7ba9
Merge branch 'develop' into feat/user-sessions
supercrafter100 Oct 16, 2022
06577e0
Merge branch 'develop' into feat/user-sessions
tadhgboyle Jan 20, 2023
11984d7
Merge branch 'develop' into feat/user-sessions
tadhgboyle Mar 7, 2023
5b05c05
Merge branch 'develop' into feat/user-sessions
tadhgboyle Mar 7, 2023
e0e425b
Merge branch 'develop' into feat/user-sessions
tadhgboyle Mar 7, 2023
36f9757
wip
tadhgboyle Jan 26, 2023
be3ed83
wip usercp sessions view
tadhgboyle Mar 8, 2023
80b109e
wip
tadhgboyle Mar 8, 2023
86fce39
wip - show ip to user
tadhgboyle Mar 8, 2023
c1ce07f
wip
tadhgboyle Mar 8, 2023
c98258f
wip
tadhgboyle Mar 11, 2023
84c91f1
Merge branch 'develop' into user-sessions-page
tadhgboyle May 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions composer.json
Expand Up @@ -30,6 +30,8 @@
"robmorgan/phinx": "^0.12.13",
"symfony/http-foundation": "^5.4.17",
"joypixels/emoji-toolkit": "^7.0",
"geoip2/geoip2": "^2.13",
"jenssegers/agent": "^2.6",
"php-di/php-di": "^6.4"
},
"require-dev": {
Expand Down
Binary file added core/assets/GeoLite2-Country.mmdb
Binary file not shown.
16 changes: 14 additions & 2 deletions core/classes/Core/User.php
Expand Up @@ -78,12 +78,14 @@ public function __construct(string $user = null, string $field = 'id') {
$hash = Session::get($this->_sessionName);
if ($this->find($hash, 'hash')) {
$this->_isLoggedIn = true;
$this->_db->update('users_session', ['hash', $hash], ['last_seen' => date('U')]);
}
}
if (Session::exists($this->_admSessionName)) {
$hash = Session::get($this->_admSessionName);
if ($this->find($hash, 'hash')) {
$this->_isAdmLoggedIn = true;
$this->_db->update('users_session', ['hash', $hash], ['last_seen' => date('U')]);
}
}
} else {
Expand Down Expand Up @@ -266,14 +268,17 @@ private function _commonLogin(?string $username, ?string $password, bool $rememb
}
} else if ($this->checkCredentials($username, $password, $method) === true) {
// Valid credentials
// TODO: job to remove old sessions?
$hash = SecureRandom::alphanumeric();

$this->_db->insert('users_session', [
'user_id' => $this->data()->id,
'hash' => $hash,
'remember_me' => $remember,
'active' => 1,
'login_method' => $is_admin ? 'admin' : $method
'active' => true,
'login_method' => $is_admin ? 'admin' : $method,
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'ip' => HttpUtils::getRemoteAddress()
]);

Session::put($sessionName, $hash);
Expand Down Expand Up @@ -478,6 +483,13 @@ public function hasPermission(string $permission): bool {
return false;
}

public function getActiveSessions(): array {
return DB::getInstance()->query(
'SELECT * FROM nl2_users_session WHERE user_id = ? AND active = 1 ORDER BY last_seen DESC', [
$this->data()->id
])->results();
}

/**
* Log the user out from all other sessions.
*/
Expand Down
31 changes: 31 additions & 0 deletions core/classes/Misc/HttpUtils.php
@@ -1,5 +1,7 @@
<?php

use GeoIp2\Database\Reader;
use GeoIp2\Exception\GeoIp2Exception;
use Symfony\Component\HttpFoundation\IpUtils;

/**
Expand All @@ -12,6 +14,8 @@
*/
class HttpUtils {

private static Reader $_geoIpReader;

/**
* Get the client's true IP address, using proxy headers if necessary.
*
Expand Down Expand Up @@ -242,4 +246,31 @@ public static function getHeader(string $header_name): ?string {
return null;
}

public static function getIpCountry(string $ip): string {
if (in_array($ip, ['localhost', '127.0.0.1', '::1'])) {
return 'Unknown';
}

$cache = new Cache(['name' => 'nameless', 'extension' => '.cache', 'path' => ROOT_PATH . '/cache/']);
$cache->setCache('ip_location');

if ($cache->isCached($ip)) {
return $cache->retrieve($ip);
}

$reader = self::$_geoIpReader ??= new Reader(ROOT_PATH . '/core/assets/GeoLite2-Country.mmdb');

try {
$record = $reader->country($ip);
} catch (GeoIp2Exception $e) {
return 'Unknown';
}

$country = $record->country->name;

$cache->store($ip, $country, 3600);

return $country;
}

}
3 changes: 2 additions & 1 deletion core/init.php
Expand Up @@ -391,9 +391,10 @@
$cc_nav->add('cc_connections', $language->get('user', 'connections'), URL::build('/user/connections'));
$cc_nav->add('cc_settings', $language->get('user', 'profile_settings'), URL::build('/user/settings'));
$cc_nav->add('cc_oauth', $language->get('admin', 'oauth'), URL::build('/user/oauth'));
$cc_nav->add('cc_sessions', $language->get('general', 'sessions'), URL::build('/user/sessions'));

// Placeholders enabled?
if (Util::getSetting('placeholders') === '1') {
if (Util::getSetting('mc_integration') && Util::getSetting('placeholders') === '1') {
$cc_nav->add('cc_placeholders', $language->get('user', 'placeholders'), URL::build('/user/placeholders'));
}

Expand Down
Expand Up @@ -10,7 +10,7 @@ public function change(): void
$table = $this->table('nl2_users_session');
$table->addColumn('remember_me', 'boolean', ['default' => false]);
$table->addColumn('active', 'boolean', ['default' => false]);
$table->addColumn('device_name', 'string', ['length' => 256, 'null' => true, 'default' => null]);
$table->addColumn('user_agent', 'string', ['length' => 256, 'null' => true, 'default' => null]);
$table->addColumn('last_seen', 'integer', ['length' => 11, 'null' => true, 'default' => null]);
$table->addColumn('login_method', 'string', ['length' => 32]);
$table->addIndex('hash', ['unique' => true]);
Expand Down
14 changes: 14 additions & 0 deletions core/migrations/20221010152624_add_ip_to_user_sessions.php
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class AddIpToUserSessions extends AbstractMigration
{
public function change(): void
{
$table = $this->table('nl2_users_session');
$table->addColumn('ip', 'string', ['length' => 128, 'null' => true]);
$table->update();
}
}
8 changes: 8 additions & 0 deletions custom/languages/en_UK.json
Expand Up @@ -331,6 +331,7 @@
"admin/is_verified": "Is Verified?",
"admin/keywords": "Keywords",
"admin/layout": "Layout",
"admin/last_seen": "Last Seen",
"admin/leaderboard_settings": "Leaderboard Settings",
"admin/leave_port_empty_for_srv": "You can leave the port empty if it is 25565, or if your domain uses an SRV record.",
"admin/left": "Left",
Expand Down Expand Up @@ -708,6 +709,7 @@
"admin/user_management": "User Management",
"admin/user_new_profile_post_hook_info": "New profile post",
"admin/user_profile_post_reply_hook_info": "Profile post reply",
"admin/user_sessions": "User Login Sessions",
"admin/user_unlink_integration_hook_info": "User unlink integration",
"admin/user_updated_successfully": "User updated successfully.",
"admin/user_validated_successfully": "User validated successfully.",
Expand All @@ -727,6 +729,7 @@
"admin/view_site": "View Site",
"admin/viewing_email_error": "Viewing error",
"admin/viewing_integrations_for_x": "Viewing user integrations for {{user}}",
"admin/viewing_sessions_for_x": "Viewing login sessions for {{user}}",
"admin/viewing_query_error": "Viewing Query Error",
"admin/views_x": "Views: {{views}}",
"admin/warning": "Warning",
Expand Down Expand Up @@ -814,6 +817,7 @@
"general/debug_link_cannot_generate": "Cannot generate debug link, check console for errors.",
"general/debug_link_copied": "Debug link copied to clipboard!",
"general/delete": "Delete",
"general/device": "Device",
"general/edit": "Edit",
"general/error": "Error",
"general/frame": "Frame",
Expand All @@ -835,6 +839,7 @@
"general/log_out": "Log Out",
"general/log_out_click": "Click here to log out",
"general/log_out_complete": "Logout successful. Click {{linkStart}}here{{linkEnd}} to continue.",
"general/logout_session_successfully": "Session successfully logged out.",
"general/more": "More",
"general/next": "Next",
"general/no": "No",
Expand Down Expand Up @@ -877,6 +882,8 @@
"general/search": "Search",
"general/server_offline": "The server is currently offline.",
"general/server_status": "Server Status",
"general/session": "Session",
"general/sessions": "Sessions",
"general/sign_in": "Log In",
"general/social": "Social",
"general/spoiler": "Spoiler",
Expand Down Expand Up @@ -1188,6 +1195,7 @@
"user/latest_profile_posts": "Latest Profile Posts",
"user/leave_conversation": "Leave Conversation",
"user/like": "Like",
"user/logout_other_sessions": "Log Out other sessions",
"user/log_in_with": "Log In with {{provider}}",
"user/max_pm_10_users": "You can only send a message to a maximum of 10 users",
"user/mcname_maximum_20": "Your Minecraft username must be a maximum of 20 characters.",
Expand Down
114 changes: 114 additions & 0 deletions custom/panel_templates/Default/core/users_sessions.tpl
@@ -0,0 +1,114 @@
{include file='header.tpl'}

<body id="page-top">

<!-- Wrapper -->
<div id="wrapper">

<!-- Sidebar -->
{include file='sidebar.tpl'}

<!-- Content Wrapper -->
<div id="content-wrapper" class="d-flex flex-column">

<!-- Main content -->
<div id="content">

<!-- Topbar -->
{include file='navbar.tpl'}

<!-- Begin Page Content -->
<div class="container-fluid">

<!-- Page heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">{$USERS}</h1>
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{$PANEL_INDEX}">{$DASHBOARD}</a></li>
<li class="breadcrumb-item active">{$USER_MANAGEMENT}</li>
<li class="breadcrumb-item active">{$USERS}</li>
</ol>
</div>

<!-- Update Notification -->
{include file='includes/update.tpl'}

<div class="card shadow mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-9">
<h5 style="margin-top: 7px; margin-bottom: 7px;">{$VIEWING_USER_SESSIONS}</h5>
</div>
<div class="col-md-3">
<span class="float-md-right">
<a href="{$BACK_LINK}" class="btn btn-warning">{$BACK}</a>
</span>
</div>
</div>
<hr />

<!-- Success and Error Alerts -->
{include file='includes/alerts.tpl'}

<div class="table-responsive">
<table class="table table-borderless table-striped">
<thead>
<tr>
<th></th>
<th>{$DEVICE}</th>
<th>{$IP_ADDRESS}</th>
<th>{$LAST_SEEN}</th>
<th>{$LOGIN_METHOD}</th>
<th></th>
</tr>
</thead>
<tbody>
{foreach $SESSIONS as $session}
<tr>
<td class="text-center">
<i class="fas fa-xl {if $session.device_type === 'tablet'}fa-tablet{elseif $session.device_type === 'phone'}fa-mobile{else}fa-desktop{/if}"></i>
</td>
<td>
{$session.device_os} &middot; {$session.device_browser} {$session.device_browser_version}
</td>
<td>
{$session.ip}
</td>
<td>
{if $session.is_current}
<span class="badge badge-success">This device</span>
{else}
<span {if $session.last_seen_short !== 'Unknown'}data-toggle="tooltip" data-title="{$session.last_seen_long}"{/if}>
{$session.last_seen_short}
</span>
{/if}
</td>
<td>
{$session.method|ucfirst}
</td>
<td class="text-right">
{if !$session.is_current}
<form action="" method="post">
<input type="hidden" name="action" value="logout">
<input type="hidden" name="token" value="{$TOKEN}">
<input type="hidden" name="sid" value="{$session.id}">
<button type="submit" class="btn btn-danger btn-sm">{$LOGOUT}</button>
</form>
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

{include file='footer.tpl'}
</div>
</div>

{include file='scripts.tpl'}
</body>