433 lines
21 KiB
HTML
433 lines
21 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Session Management - {{ block.super }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.session-filters {
|
|
background: #f8f9fa;
|
|
padding: 1rem;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.session-item {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.session-item:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.session-item.active {
|
|
border-left: 4px solid #28a745;
|
|
background: #f8fff9;
|
|
}
|
|
|
|
.session-item.expired {
|
|
border-left: 4px solid #dc3545;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.session-item.suspicious {
|
|
border-left: 4px solid #ffc107;
|
|
background: #fffbf0;
|
|
}
|
|
|
|
.session-meta {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.device-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.375rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 1rem;
|
|
}
|
|
|
|
.device-icon.desktop {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
}
|
|
|
|
.device-icon.mobile {
|
|
background: #f3e5f5;
|
|
color: #7b1fa2;
|
|
}
|
|
|
|
.device-icon.tablet {
|
|
background: #e8f5e8;
|
|
color: #388e3c;
|
|
}
|
|
|
|
.session-actions {
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.session-item:hover .session-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stats-card {
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
margin-bottom: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stats-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
}
|
|
|
|
.location-map {
|
|
height: 200px;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #6c757d;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title mb-0">
|
|
<i class="fas fa-desktop me-2"></i>
|
|
Session Management
|
|
</h4>
|
|
<p class="card-subtitle text-muted mt-1">
|
|
Monitor and manage user sessions across the system
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<!-- Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="stats-card">
|
|
<div class="stats-number" id="total-sessions">{{ sessions.count }}</div>
|
|
<div class="text-muted">Total Sessions</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card">
|
|
<div class="stats-number text-success" id="active-sessions">
|
|
{{ sessions|length }}
|
|
</div>
|
|
<div class="text-muted">Active Sessions</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card">
|
|
<div class="stats-number text-warning" id="suspicious-sessions">0</div>
|
|
<div class="text-muted">Suspicious</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card">
|
|
<div class="stats-number text-info" id="unique-users">
|
|
{{ sessions|regroup:"user"|length }}
|
|
</div>
|
|
<div class="text-muted">Unique Users</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="session-filters">
|
|
<form method="get" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="user_id" class="form-label">User</label>
|
|
<select name="user_id" id="user_id" class="form-select">
|
|
<option value="">All Users</option>
|
|
{% for session in sessions %}
|
|
{% ifchanged session.user %}
|
|
<option value="{{ session.user.id }}"
|
|
{% if request.GET.user_id == session.user.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ session.user.get_full_name }}
|
|
</option>
|
|
{% endifchanged %}
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label for="status" class="form-label">Status</label>
|
|
<select name="status" id="status" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>Active</option>
|
|
<option value="expired" {% if request.GET.status == 'expired' %}selected{% endif %}>Expired</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label for="device_type" class="form-label">Device Type</label>
|
|
<select name="device_type" id="device_type" class="form-select">
|
|
<option value="">All Devices</option>
|
|
<option value="DESKTOP" {% if request.GET.device_type == 'DESKTOP' %}selected{% endif %}>Desktop</option>
|
|
<option value="MOBILE" {% if request.GET.device_type == 'MOBILE' %}selected{% endif %}>Mobile</option>
|
|
<option value="TABLET" {% if request.GET.device_type == 'TABLET' %}selected{% endif %}>Tablet</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">Search</label>
|
|
<input type="text" name="search" id="search" class="form-control"
|
|
placeholder="IP address, location, browser..."
|
|
value="{{ request.GET.search }}">
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<label class="form-label"> </label>
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-1"></i>Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Session List -->
|
|
<div id="session-list"
|
|
hx-get="{% url 'accounts:session_list' %}"
|
|
hx-trigger="load, every 30s"
|
|
hx-include="[name='user_id'], [name='status'], [name='device_type']">
|
|
|
|
{% if sessions %}
|
|
{% for session in sessions %}
|
|
<div class="session-item {% if session.is_active %}active{% elif session.expires_at < now %}expired{% endif %}"
|
|
data-session-id="{{ session.session_id }}">
|
|
<div class="row align-items-center">
|
|
<div class="col-auto">
|
|
<div class="device-icon {{ session.device_type|lower }}">
|
|
{% if session.device_type == 'DESKTOP' %}
|
|
<i class="fas fa-desktop"></i>
|
|
{% elif session.device_type == 'MOBILE' %}
|
|
<i class="fas fa-mobile-alt"></i>
|
|
{% elif session.device_type == 'TABLET' %}
|
|
<i class="fas fa-tablet-alt"></i>
|
|
{% else %}
|
|
<i class="fas fa-question"></i>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="fw-bold">
|
|
{{ session.user.get_full_name }}
|
|
{% if session.is_current_session %}
|
|
<span class="badge bg-primary ms-2">Current</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="session-meta">
|
|
<i class="fas fa-globe me-1"></i>{{ session.ip_address }}
|
|
<span class="mx-2">•</span>
|
|
<i class="fas fa-map-marker-alt me-1"></i>{{ session.location|default:"Unknown location" }}
|
|
<span class="mx-2">•</span>
|
|
<i class="fas fa-browser me-1"></i>{{ session.browser_name }} {{ session.browser_version }}
|
|
</div>
|
|
<div class="session-meta">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Started: {{ session.created_at|date:"M d, Y H:i" }}
|
|
<span class="mx-2">•</span>
|
|
Last activity: {{ session.last_activity_at|date:"M d, Y H:i" }}
|
|
{% if session.expires_at %}
|
|
<span class="mx-2">•</span>
|
|
Expires: {{ session.expires_at|date:"M d, Y H:i" }}
|
|
{% endif %}
|
|
</div>
|
|
{% if session.user_agent %}
|
|
<div class="session-meta">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
{{ session.user_agent|truncatechars:80 }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="session-actions">
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-outline-info"
|
|
title="View Details"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#session-detail-modal"
|
|
onclick="loadSessionDetails('{{ session.session_id }}')">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
|
|
{% if session.is_active and not session.is_current_session %}
|
|
<button class="btn btn-sm btn-outline-warning"
|
|
title="End Session"
|
|
hx-post="{% url 'accounts:end_session' session.session_id %}"
|
|
hx-confirm="End this session?"
|
|
hx-target="closest .session-item"
|
|
hx-swap="outerHTML">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button class="btn btn-sm btn-outline-secondary"
|
|
title="Session History">
|
|
<i class="fas fa-history"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-desktop fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No sessions found</h5>
|
|
<p class="text-muted">No sessions match your current filters.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<nav aria-label="Session pagination" class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if request.GET.user_id %}&user_id={{ request.GET.user_id }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.device_type %}&device_type={{ request.GET.device_type }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.user_id %}&user_id={{ request.GET.user_id }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.device_type %}&device_type={{ request.GET.device_type }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">
|
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
|
</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.user_id %}&user_id={{ request.GET.user_id }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.device_type %}&device_type={{ request.GET.device_type }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.user_id %}&user_id={{ request.GET.user_id }}{% endif %}{% if request.GET.status %}&status={{ request.GET.status }}{% endif %}{% if request.GET.device_type %}&device_type={{ request.GET.device_type }}{% endif %}{% if request.GET.search %}&search={{ request.GET.search }}{% endif %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Session Detail Modal -->
|
|
<div class="modal fade" id="session-detail-modal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Session Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Content loaded via JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-refresh session list every 30 seconds
|
|
setInterval(function() {
|
|
htmx.trigger('#session-list', 'refresh');
|
|
}, 30000);
|
|
|
|
// Update statistics
|
|
function updateStats() {
|
|
const sessions = document.querySelectorAll('.session-item');
|
|
const activeSessions = document.querySelectorAll('.session-item.active');
|
|
const suspiciousSessions = document.querySelectorAll('.session-item.suspicious');
|
|
|
|
document.getElementById('total-sessions').textContent = sessions.length;
|
|
document.getElementById('active-sessions').textContent = activeSessions.length;
|
|
document.getElementById('suspicious-sessions').textContent = suspiciousSessions.length;
|
|
}
|
|
|
|
// Load session details
|
|
window.loadSessionDetails = function(sessionId) {
|
|
fetch(`/accounts/session/${sessionId}/details/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const modalBody = document.querySelector('#session-detail-modal .modal-body');
|
|
modalBody.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Session Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td>Session ID</td><td>${data.session_id}</td></tr>
|
|
<tr><td>User</td><td>${data.user_name}</td></tr>
|
|
<tr><td>Status</td><td>${data.is_active ? 'Active' : 'Inactive'}</td></tr>
|
|
<tr><td>Created</td><td>${data.created_at}</td></tr>
|
|
<tr><td>Last Activity</td><td>${data.last_activity_at}</td></tr>
|
|
<tr><td>Expires</td><td>${data.expires_at || 'Never'}</td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Device Information</h6>
|
|
<table class="table table-sm">
|
|
<tr><td>Device Type</td><td>${data.device_type}</td></tr>
|
|
<tr><td>Browser</td><td>${data.browser_name} ${data.browser_version}</td></tr>
|
|
<tr><td>Operating System</td><td>${data.operating_system}</td></tr>
|
|
<tr><td>IP Address</td><td>${data.ip_address}</td></tr>
|
|
<tr><td>Location</td><td>${data.location || 'Unknown'}</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<h6>User Agent</h6>
|
|
<code>${data.user_agent}</code>
|
|
</div>
|
|
</div>
|
|
`;
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading session details:', error);
|
|
});
|
|
};
|
|
|
|
// Initial stats update
|
|
updateStats();
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|