feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
<?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.')]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user