Files
biiproject-kit-v1/app/Http/Controllers/SystemSettings/SessionManagerController.php
T

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.')]);
}
}