Marwan Alwali a710d1c4d8 update
2025-09-11 19:01:55 +03:00

510 lines
20 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}User Sessions - Session Management{% endblock %}
{% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'accounts:user_list' %}">Users</a></li>
<li class="breadcrumb-item active">Sessions</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
User Sessions
<small>Active Session Management</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">Session Management</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<!-- Filters and Search -->
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Search Sessions</label>
<div class="input-group">
<input type="text" class="form-control" id="search-input" placeholder="Username, IP address...">
<button class="btn btn-outline-secondary" type="button" id="search-btn">
<i class="fa fa-search"></i>
</button>
</div>
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select class="form-select" id="status-filter">
<option value="">All Sessions</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Time Range</label>
<select class="form-select" id="time-filter">
<option value="">All Time</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-outline-danger" onclick="terminateAllSessions()">
<i class="fa fa-sign-out-alt me-2"></i>Terminate All
</button>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-primary text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">ACTIVE SESSIONS</div>
<div class="fs-18px fw-900 text-white" id="active-sessions">-</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-users fa-lg text-white"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-success text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">UNIQUE USERS</div>
<div class="fs-18px fw-900 text-white" id="unique-users">-</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-user fa-lg text-white"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-warning text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">EXPIRED TODAY</div>
<div class="fs-18px fw-900 text-white" id="expired-today">-</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-clock fa-lg text-white"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-info text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">AVG DURATION</div>
<div class="fs-18px fw-900 text-white" id="avg-duration">-</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-stopwatch fa-lg text-white"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary" id="bulk-actions-btn" disabled>
<i class="fa fa-tasks me-2"></i>Bulk Actions
</button>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" id="bulk-dropdown" disabled>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item text-danger" href="#" onclick="bulkTerminate()">
<i class="fa fa-sign-out-alt me-2"></i>Terminate Selected
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="bulkExport()">
<i class="fa fa-download me-2"></i>Export Selected
</a></li>
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary" onclick="refreshTable()">
<i class="fa fa-refresh me-2"></i>Refresh
</button>
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fa fa-download me-2"></i>Export
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportData('csv')">
<i class="fa fa-file-csv me-2"></i>Export as CSV
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportData('excel')">
<i class="fa fa-file-excel me-2"></i>Export as Excel
</a></li>
</ul>
</div>
</div>
<!-- Sessions Table -->
<div class="table-responsive">
<table id="sessions-table" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="30">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select-all">
</div>
</th>
<th>User</th>
<th>Session Key</th>
<th>IP Address</th>
<th>User Agent</th>
<th>Created</th>
<th>Last Activity</th>
<th>Status</th>
<th width="120">Actions</th>
</tr>
</thead>
<tbody>
<!-- Data will be loaded via AJAX -->
</tbody>
</table>
</div>
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {
var table;
// Initialize DataTable
table = $('#sessions-table').DataTable({
processing: true,
serverSide: true,
ajax: {
url: '{% url "accounts:session_list_api" %}',
data: function(d) {
d.search_value = $('#search-input').val();
d.status = $('#status-filter').val();
d.time_range = $('#time-filter').val();
}
},
columns: [
{
data: 'session_key',
orderable: false,
render: function(data, type, row) {
return '<div class="form-check"><input class="form-check-input session-checkbox" type="checkbox" value="' + data + '"></div>';
}
},
{
data: 'user',
render: function(data, type, row) {
if (data) {
return '<div>' +
'<div class="fw-bold">' + data.full_name + '</div>' +
'<div class="text-muted small">' + data.username + '</div>' +
'</div>';
} else {
return '<span class="text-muted">Anonymous</span>';
}
}
},
{
data: 'session_key',
render: function(data, type, row) {
return '<code class="small">' + data.substring(0, 16) + '...</code>';
}
},
{
data: 'ip_address',
render: function(data, type, row) {
return data || '<span class="text-muted">Unknown</span>';
}
},
{
data: 'user_agent',
render: function(data, type, row) {
if (data) {
var browser = getBrowserInfo(data);
return '<div>' +
'<div class="small">' + browser.name + ' ' + browser.version + '</div>' +
'<div class="text-muted small">' + browser.os + '</div>' +
'</div>';
} else {
return '<span class="text-muted">Unknown</span>';
}
}
},
{
data: 'created_at',
render: function(data, type, row) {
return moment(data).format('MMM DD, YYYY HH:mm');
}
},
{
data: 'last_activity',
render: function(data, type, row) {
return moment(data).fromNow();
}
},
{
data: 'is_active',
render: function(data, type, row) {
if (data) {
return '<span class="badge bg-success">Active</span>';
} else {
return '<span class="badge bg-secondary">Expired</span>';
}
}
},
{
data: 'session_key',
orderable: false,
render: function(data, type, row) {
var actions = '<div class="btn-group btn-group-sm">';
actions += '<button class="btn btn-outline-primary" onclick="viewSession(\'' + data + '\')" title="View">';
actions += '<i class="fa fa-eye"></i></button>';
if (row.is_active) {
actions += '<button class="btn btn-outline-danger" onclick="terminateSession(\'' + data + '\')" title="Terminate">';
actions += '<i class="fa fa-sign-out-alt"></i></button>';
}
actions += '</div>';
return actions;
}
}
],
order: [[6, 'desc']],
pageLength: 25,
responsive: true,
language: {
processing: '<div class="d-flex justify-content-center"><div class="spinner-border" role="status"></div></div>'
}
});
// Load stats
loadStats();
// Filter event handlers
$('#search-input, #status-filter, #time-filter').on('change keyup', function() {
table.draw();
});
// Select all checkbox
$('#select-all').on('change', function() {
$('.session-checkbox').prop('checked', this.checked);
updateBulkActions();
});
// Individual checkbox change
$(document).on('change', '.session-checkbox', function() {
updateBulkActions();
});
// Auto-refresh every 30 seconds
setInterval(function() {
table.ajax.reload(null, false);
loadStats();
}, 30000);
});
function loadStats() {
$.ajax({
url: '{% url "accounts:session_stats" %}',
success: function(data) {
$('#active-sessions').text(data.active_sessions);
$('#unique-users').text(data.unique_users);
$('#expired-today').text(data.expired_today);
$('#avg-duration').text(data.avg_duration);
}
});
}
function updateBulkActions() {
var checkedCount = $('.session-checkbox:checked').length;
$('#bulk-actions-btn, #bulk-dropdown').prop('disabled', checkedCount === 0);
}
function viewSession(sessionKey) {
window.location.href = '{% url "accounts:session_detail" "SESSION_KEY" %}'.replace('SESSION_KEY', sessionKey);
}
function terminateSession(sessionKey) {
if (confirm('Are you sure you want to terminate this session?')) {
$.ajax({
url: '{% url "accounts:session_terminate" "SESSION_KEY" %}'.replace('SESSION_KEY', sessionKey),
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
toastr.success('Session terminated successfully');
$('#sessions-table').DataTable().ajax.reload();
} else {
toastr.error('Failed to terminate session');
}
},
error: function() {
toastr.error('An error occurred while terminating the session');
}
});
}
}
function terminateAllSessions() {
if (confirm('Are you sure you want to terminate ALL active sessions? This will log out all users.')) {
$.ajax({
url: '{% url "accounts:session_terminate_all" %}',
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
toastr.success('All sessions terminated successfully');
$('#sessions-table').DataTable().ajax.reload();
loadStats();
} else {
toastr.error('Failed to terminate all sessions');
}
},
error: function() {
toastr.error('An error occurred while terminating sessions');
}
});
}
}
function bulkTerminate() {
var selectedSessions = $('.session-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedSessions.length === 0) {
toastr.warning('Please select sessions to terminate');
return;
}
if (confirm('Are you sure you want to terminate ' + selectedSessions.length + ' session(s)?')) {
$.ajax({
url: '{% url "accounts:session_bulk_terminate" %}',
method: 'POST',
data: {
'session_keys': selectedSessions,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
toastr.success(response.message);
$('#sessions-table').DataTable().ajax.reload();
$('#select-all').prop('checked', false);
updateBulkActions();
} else {
toastr.error('Failed to terminate sessions');
}
},
error: function() {
toastr.error('An error occurred during bulk termination');
}
});
}
}
function bulkExport() {
var selectedSessions = $('.session-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedSessions.length === 0) {
toastr.warning('Please select sessions to export');
return;
}
window.location.href = '{% url "accounts:session_export" %}?sessions=' + selectedSessions.join(',');
}
function exportData(format) {
window.location.href = '{% url "accounts:session_export" %}?format=' + format;
}
function refreshTable() {
$('#sessions-table').DataTable().ajax.reload();
loadStats();
toastr.info('Table refreshed');
}
function getBrowserInfo(userAgent) {
// Simple browser detection
var browser = { name: 'Unknown', version: '', os: 'Unknown' };
if (userAgent.includes('Chrome')) {
browser.name = 'Chrome';
var match = userAgent.match(/Chrome\/(\d+)/);
browser.version = match ? match[1] : '';
} else if (userAgent.includes('Firefox')) {
browser.name = 'Firefox';
var match = userAgent.match(/Firefox\/(\d+)/);
browser.version = match ? match[1] : '';
} else if (userAgent.includes('Safari')) {
browser.name = 'Safari';
var match = userAgent.match(/Version\/(\d+)/);
browser.version = match ? match[1] : '';
}
if (userAgent.includes('Windows')) {
browser.os = 'Windows';
} else if (userAgent.includes('Mac')) {
browser.os = 'macOS';
} else if (userAgent.includes('Linux')) {
browser.os = 'Linux';
} else if (userAgent.includes('Android')) {
browser.os = 'Android';
} else if (userAgent.includes('iOS')) {
browser.os = 'iOS';
}
return browser;
}
</script>
{% endblock %}