311 lines
13 KiB
PHP
311 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\SystemSettings;
|
|
|
|
use App\Helpers\SessionHelper;
|
|
use App\Services\Monitoring\SystemMonitoringService;
|
|
use App\Support\DataTable;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Redis;
|
|
|
|
class SessionManagerController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
// Middleware handled in web.php
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
if (DataTable::isDataTableRequest($request)) {
|
|
return $this->dataTable($request);
|
|
}
|
|
|
|
$stats = $this->getStatsData();
|
|
|
|
return view('pages.system_settings.session-manager', compact('stats'));
|
|
}
|
|
|
|
protected function getDriver()
|
|
{
|
|
return config('session.driver');
|
|
}
|
|
|
|
public function getStats()
|
|
{
|
|
return response()->json($this->getStatsData());
|
|
}
|
|
|
|
protected function getStatsData()
|
|
{
|
|
$driver = $this->getDriver();
|
|
|
|
if ($driver === 'database') {
|
|
$activeCutoff = now()->subMinutes(30)->timestamp;
|
|
|
|
return [
|
|
'total' => DB::table('sessions')->count(),
|
|
'active' => DB::table('sessions')->where('last_activity', '>=', $activeCutoff)->count(),
|
|
'users' => DB::table('sessions')->whereNotNull('user_id')->count(),
|
|
'guests' => DB::table('sessions')->whereNull('user_id')->count(),
|
|
'unique_ips' => DB::table('sessions')->distinct('ip_address')->count('ip_address'),
|
|
];
|
|
}
|
|
|
|
if ($driver === 'redis') {
|
|
$service = app(SystemMonitoringService::class);
|
|
$total = $service->getActiveUsers();
|
|
|
|
// For Redis, we'd need to iterate all to get specific user/guest breakdown
|
|
// For performance, we'll return total for now or a limited set
|
|
return [
|
|
'total' => $total,
|
|
'active' => $total,
|
|
'users' => 'N/A (Redis)',
|
|
'guests' => 'N/A (Redis)',
|
|
'unique_ips' => 'N/A (Redis)',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'total' => 0, 'active' => 0, 'users' => 0, 'guests' => 0, 'unique_ips' => 0,
|
|
];
|
|
}
|
|
|
|
protected function dataTable(Request $request)
|
|
{
|
|
$driver = $this->getDriver();
|
|
$sessions = [];
|
|
$recordsTotal = 0;
|
|
$recordsFiltered = 0;
|
|
$activeCutoff = now()->subMinutes(30)->timestamp;
|
|
$idleCutoff = now()->subMinutes(5)->timestamp;
|
|
$currentSessionId = session()->getId();
|
|
|
|
if ($driver === 'database') {
|
|
$query = DB::table('sessions')
|
|
->leftJoin('users', 'sessions.user_id', '=', 'users.id')
|
|
->select(
|
|
'sessions.*',
|
|
'users.email as user_email',
|
|
'users.name as user_name'
|
|
);
|
|
|
|
$recordsTotal = DB::table('sessions')->count();
|
|
|
|
// Status Filter
|
|
if ($status = DataTable::columnSearch($request, 0)) {
|
|
$operator = $status === 'active' ? '>=' : '<';
|
|
$query->where('sessions.last_activity', $operator, $activeCutoff);
|
|
}
|
|
|
|
// User Filter
|
|
if ($userSearch = DataTable::columnSearch($request, 1)) {
|
|
$query->where(function ($q) use ($userSearch) {
|
|
$q->where('users.email', 'like', "%{$userSearch}%")
|
|
->orWhere('users.name', 'like', "%{$userSearch}%");
|
|
});
|
|
}
|
|
|
|
// IP Filter
|
|
if ($ipSearch = DataTable::columnSearch($request, 3)) {
|
|
$query->where('sessions.ip_address', 'like', "%{$ipSearch}%");
|
|
}
|
|
|
|
// Global Search
|
|
if ($globalSearch = DataTable::globalSearch($request)) {
|
|
$query->where(function ($q) use ($globalSearch) {
|
|
$q->where('users.email', 'like', "%{$globalSearch}%")
|
|
->orWhere('users.name', 'like', "%{$globalSearch}%")
|
|
->orWhere('sessions.id', 'like', "%{$globalSearch}%")
|
|
->orWhere('sessions.ip_address', 'like', "%{$globalSearch}%");
|
|
});
|
|
}
|
|
|
|
$recordsFiltered = (clone $query)->count();
|
|
[$orderIndex, $orderDirection] = DataTable::order($request, 4, 'desc');
|
|
|
|
$sortColumn = match ($orderIndex) {
|
|
0 => 'sessions.last_activity',
|
|
1 => 'users.name',
|
|
3 => 'sessions.ip_address',
|
|
4 => 'sessions.last_activity',
|
|
default => 'sessions.last_activity',
|
|
};
|
|
|
|
$sessions = $query
|
|
->orderBy($sortColumn, $orderDirection)
|
|
->skip(DataTable::start($request))
|
|
->take(DataTable::length($request))
|
|
->get();
|
|
} else {
|
|
// REDIS DRIVER LOGIC
|
|
$connection = config('session.connection') ?? 'default';
|
|
$redis = Redis::connection($connection);
|
|
$sessionCookie = config('session.cookie', 'laravel_session');
|
|
$prefix = config('database.redis.options.prefix', '');
|
|
|
|
// Optimization: Get keys once
|
|
$patterns = [$sessionCookie.':*'];
|
|
$keys = [];
|
|
foreach ($patterns as $p) {
|
|
$searchPattern = str_replace($prefix, '', $p);
|
|
$keys = array_merge($keys, $redis->keys($searchPattern));
|
|
}
|
|
$keys = array_unique($keys);
|
|
$recordsTotal = count($keys);
|
|
$recordsFiltered = $recordsTotal;
|
|
|
|
$start = DataTable::start($request);
|
|
$length = DataTable::length($request);
|
|
$pagedKeys = array_slice($keys, $start, $length);
|
|
|
|
$tempSessions = [];
|
|
$userIds = [];
|
|
|
|
foreach ($pagedKeys as $key) {
|
|
$pureKey = str_replace($prefix, '', $key);
|
|
$data = $redis->get($pureKey);
|
|
|
|
if ($data) {
|
|
try {
|
|
$unserialized = unserialize($data, ['allowed_classes' => false]);
|
|
if (is_string($unserialized)) {
|
|
$unserialized = unserialize($unserialized, ['allowed_classes' => false]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
continue;
|
|
}
|
|
|
|
$parts = explode(':', $key);
|
|
$sessionId = end($parts);
|
|
|
|
if (isset($unserialized['user_id'])) {
|
|
$userIds[] = $unserialized['user_id'];
|
|
}
|
|
|
|
$tempSessions[] = [
|
|
'id' => $sessionId,
|
|
'user_id' => $unserialized['user_id'] ?? null,
|
|
'ip_address' => $unserialized['ip_address'] ?? 'N/A',
|
|
'user_agent' => $unserialized['user_agent'] ?? 'N/A',
|
|
'last_activity' => $unserialized['last_activity'] ?? time(),
|
|
];
|
|
}
|
|
}
|
|
|
|
// Batch fetch users to prevent N+1
|
|
$users = ! empty($userIds) ? DB::table('users')->whereIn('id', array_unique($userIds))->get()->keyBy('id') : collect();
|
|
|
|
foreach ($tempSessions as $sessData) {
|
|
$user = $users->get($sessData['user_id']);
|
|
$sessions[] = (object) array_merge($sessData, [
|
|
'user_name' => $user->name ?? 'Guest',
|
|
'user_email' => $user->email ?? 'N/A',
|
|
]);
|
|
}
|
|
}
|
|
|
|
$rows = collect($sessions)->map(function ($session) use ($activeCutoff, $idleCutoff, $currentSessionId) {
|
|
$isCurrent = $session->id === $currentSessionId;
|
|
$userAgent = SessionHelper::parseUserAgent($session->user_agent);
|
|
|
|
// Status Logic
|
|
if ($isCurrent) {
|
|
$statusHtml = '<span class="badge text-bg-success border border-success px-3 py-2 small fw-bold">
|
|
<span class="spinner-grow spinner-grow-sm me-1" role="status"></span>'.__('LIVE').'
|
|
</span>';
|
|
} elseif ($session->last_activity >= $idleCutoff) {
|
|
$statusHtml = '<span class="badge text-bg-info px-3 py-2 small fw-bold">'.__('ACTIVE').'</span>';
|
|
} elseif ($session->last_activity >= $activeCutoff) {
|
|
$statusHtml = '<span class="badge text-bg-warning px-3 py-2 small fw-bold">'.__('IDLE').'</span>';
|
|
} else {
|
|
$statusHtml = '<span class="badge text-bg-secondary px-3 py-2 small">'.__('EXPIRED').'</span>';
|
|
}
|
|
|
|
// User Column
|
|
$userHtml = '<div class="d-flex align-items-center">
|
|
<div>
|
|
<div class="fw-bold small">'.e($session->user_name ?? 'Guest').'</div>
|
|
<div class="text-secondary extra-small">'.e($session->user_email ?? substr($session->id, 0, 8).'...').'</div>
|
|
</div>
|
|
</div>';
|
|
|
|
// Device Column
|
|
$deviceHtml = '<div class="d-flex align-items-center">
|
|
<i class="bi '.$userAgent['browser_icon'].' h5 mb-0 me-2 text-theme-1"></i>
|
|
<div>
|
|
<div class="small fw-medium">'.$userAgent['browser'].'</div>
|
|
<div class="extra-small text-secondary"><i class="bi '.$userAgent['os_icon'].' me-1"></i>'.$userAgent['os'].'</div>
|
|
</div>
|
|
</div>';
|
|
|
|
$timestamp = Carbon::createFromTimestamp($session->last_activity);
|
|
|
|
$actionsHtml = '<div class="text-end d-flex justify-content-end gap-2">';
|
|
$actionsHtml .= '<button class="btn btn-square btn-outline-theme btn-sm rounded-circle btn-detail-session"
|
|
data-activity=\''.e(json_encode([
|
|
'session' => $session,
|
|
'device' => $userAgent,
|
|
'is_current' => $isCurrent,
|
|
'time' => format_datetime($timestamp),
|
|
], JSON_HEX_APOS | JSON_HEX_QUOT)).'\'>
|
|
<i class="bi bi-eye"></i>
|
|
</button>';
|
|
|
|
if (! $isCurrent) {
|
|
if (auth()->user()->can('manage active sessions')) {
|
|
$actionsHtml .= '<button type="button" class="btn btn-square btn-outline-danger btn-sm rounded-circle btn-terminate-session"
|
|
data-id="'.e($session->id).'"
|
|
data-url="'.route('session-manager.terminate', $session->id).'">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>';
|
|
} else {
|
|
$actionsHtml .= '<button class="btn btn-square btn-light btn-sm rounded-circle" disabled title="Insufficient permission">
|
|
<i class="bi bi-shield-lock text-muted"></i>
|
|
</button>';
|
|
}
|
|
} else {
|
|
$actionsHtml .= '<button class="btn btn-square btn-light btn-sm rounded-circle" disabled title="Current session cannot be terminated">
|
|
<i class="bi bi-lock-fill text-muted"></i>
|
|
</button>';
|
|
}
|
|
$actionsHtml .= '</div>';
|
|
|
|
return [
|
|
$statusHtml,
|
|
$userHtml,
|
|
$deviceHtml,
|
|
'<code class="extra-small">'.e($session->ip_address).'</code>',
|
|
e($timestamp->diffForHumans()),
|
|
$actionsHtml,
|
|
$isCurrent, // For custom row coloring in JS
|
|
];
|
|
})->all();
|
|
|
|
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows);
|
|
}
|
|
|
|
public function destroy(Request $request, $id)
|
|
{
|
|
if ($id === session()->getId()) {
|
|
return response()->json(['success' => false, 'message' => __('Cannot terminate current session.')], 403);
|
|
}
|
|
|
|
$driver = $this->getDriver();
|
|
if ($driver === 'database') {
|
|
DB::table('sessions')->where('id', $id)->delete();
|
|
} elseif ($driver === 'redis') {
|
|
$connection = config('session.connection') ?? 'default';
|
|
$redis = Redis::connection($connection);
|
|
$sessionCookie = config('session.cookie', 'laravel_session');
|
|
$redis->del($sessionCookie.':'.$id);
|
|
}
|
|
|
|
return response()->json(['success' => true, 'message' => __('Session terminated successfully.')]);
|
|
}
|
|
}
|