Session Management (Noch leer)

Dieser Commit ist enthalten in:
2025-06-07 14:49:40 +02:00
Ursprung fc2145bd19
Commit 6f491e3833
9 geänderte Dateien mit 449 neuen und 5 gelöschten Zeilen

Datei anzeigen

@@ -274,4 +274,33 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker
- SQL WHERE-Klauseln für Filter
- LIMIT/OFFSET für Pagination
- URL-Parameter bleiben bei Navigation erhalten
- Responsive Bootstrap-Komponenten
- Responsive Bootstrap-Komponenten
### 2025-01-06 - Session-Tracking implementiert
- Neue Tabelle für Session-Verwaltung
- Anzeige aktiver und beendeter Sessions
- Manuelles Beenden von Sessions möglich
- Dashboard zeigt Anzahl aktiver Sessions
**Neue Features:**
- **Sessions-Tabelle**: Speichert Session-ID, IP, User-Agent, Zeitstempel
- **Aktive Sessions**: Zeigt alle laufenden Sessions mit Inaktivitätszeit
- **Session-Historie**: Letzte 24 Stunden beendeter Sessions
- **Session beenden**: Admins können Sessions manuell beenden
- **Farbcodierung**: Grün (aktiv), Gelb (>5 Min inaktiv), Rot (lange inaktiv)
**Geänderte/Neue Dateien:**
- v2_adminpanel/init.sql (sessions Tabelle hinzugefügt)
- v2_adminpanel/app.py (sessions() und end_session() Routen)
- v2_adminpanel/templates/sessions.html (neu erstellt)
- v2_adminpanel/templates/dashboard.html (Session-Statistik)
- Alle Templates (Session-Navigation hinzugefügt)
**Technische Details:**
- Heartbeat-basiertes Tracking (last_heartbeat)
- Automatische Inaktivitätsberechnung
- Session-Dauer Berechnung
- Responsive Tabellen mit Bootstrap
**Hinweis:**
Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden.

Datei anzeigen

@@ -86,6 +86,10 @@ def dashboard():
""")
active_licenses = cur.fetchone()[0]
# Aktive Sessions
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
active_sessions_count = cur.fetchone()[0]
# Abgelaufene Lizenzen
cur.execute("""
SELECT COUNT(*) FROM licenses
@@ -151,7 +155,8 @@ def dashboard():
'full_licenses': license_types.get('full', 0),
'test_licenses': license_types.get('test', 0),
'recent_licenses': recent_licenses,
'expiring_licenses': expiring_licenses
'expiring_licenses': expiring_licenses,
'active_sessions': active_sessions_count
}
return render_template("dashboard.html", stats=stats, username=session.get('username'))
@@ -478,5 +483,66 @@ def delete_customer(customer_id):
return redirect("/customers")
@app.route("/sessions")
@login_required
def sessions():
conn = get_connection()
cur = conn.cursor()
# Aktive Sessions abrufen
cur.execute("""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.user_agent, s.started_at, s.last_heartbeat,
EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
FROM sessions s
JOIN licenses l ON s.license_id = l.id
JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = TRUE
ORDER BY s.last_heartbeat DESC
""")
active_sessions = cur.fetchall()
# Inaktive Sessions der letzten 24 Stunden
cur.execute("""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.started_at, s.ended_at,
EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
FROM sessions s
JOIN licenses l ON s.license_id = l.id
JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = FALSE
AND s.ended_at > NOW() - INTERVAL '24 hours'
ORDER BY s.ended_at DESC
LIMIT 50
""")
recent_sessions = cur.fetchall()
cur.close()
conn.close()
return render_template("sessions.html",
active_sessions=active_sessions,
recent_sessions=recent_sessions,
username=session.get('username'))
@app.route("/session/end/<int:session_id>", methods=["POST"])
@login_required
def end_session(session_id):
conn = get_connection()
cur = conn.cursor()
# Session beenden
cur.execute("""
UPDATE sessions
SET is_active = FALSE, ended_at = NOW()
WHERE id = %s AND is_active = TRUE
""", (session_id,))
conn.commit()
cur.close()
conn.close()
return redirect("/sessions")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=443, ssl_context='adhoc')

Datei anzeigen

@@ -17,3 +17,15 @@ CREATE TABLE IF NOT EXISTS licenses (
valid_until DATE NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS sessions (
id SERIAL PRIMARY KEY,
license_id INTEGER REFERENCES licenses(id),
session_id TEXT UNIQUE NOT NULL,
ip_address TEXT,
user_agent TEXT,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE
);

Datei anzeigen

@@ -23,6 +23,7 @@
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
</div>
</div>

Datei anzeigen

@@ -34,6 +34,7 @@
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
</div>
</div>
@@ -69,9 +70,9 @@
<div class="col-md-3">
<div class="card stat-card h-100">
<div class="card-body text-center">
<h5 class="card-title">⚠️ Ablaufend</h5>
<h2 class="text-warning">{{ stats.expiring_soon }}</h2>
<p class="text-muted mb-0">Nächste 30 Tage</p>
<h5 class="card-title">🟢 Sessions</h5>
<h2 class="text-success">{{ stats.active_sessions }}</h2>
<p class="text-muted mb-0">Aktive Nutzer</p>
</div>
</div>
</div>

Datei anzeigen

@@ -23,6 +23,7 @@
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
</div>
</div>

Datei anzeigen

@@ -28,6 +28,7 @@
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
</div>
</div>

Datei anzeigen

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Session-Tracking - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.session-active { background-color: #d4edda; }
.session-warning { background-color: #fff3cd; }
.session-inactive { background-color: #f8f9fa; }
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">🎛️ Lizenzverwaltung</span>
<div class="d-flex align-items-center">
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
</div>
</div>
</nav>
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Session-Tracking</h2>
<div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
</div>
</div>
<!-- Aktive Sessions -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">🟢 Aktive Sessions ({{ active_sessions|length }})</h5>
</div>
<div class="card-body">
{% if active_sessions %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Kunde</th>
<th>Lizenz</th>
<th>IP-Adresse</th>
<th>Gestartet</th>
<th>Letzter Heartbeat</th>
<th>Inaktiv seit</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for session in active_sessions %}
<tr class="{% if session[8] > 5 %}session-warning{% else %}session-active{% endif %}">
<td>{{ session[3] }}</td>
<td><small><code>{{ session[2][:12] }}...</code></small></td>
<td>{{ session[4] or '-' }}</td>
<td>{{ session[6].strftime('%d.%m %H:%M') }}</td>
<td>{{ session[7].strftime('%d.%m %H:%M') }}</td>
<td>
{% if session[8] < 1 %}
<span class="badge bg-success">Aktiv</span>
{% elif session[8] < 5 %}
<span class="badge bg-warning">{{ session[8]|round|int }} Min.</span>
{% else %}
<span class="badge bg-danger">{{ session[8]|round|int }} Min.</span>
{% endif %}
</td>
<td>
<form method="post" action="/session/end/{{ session[0] }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Session wirklich beenden?');">
⏹️ Beenden
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<small class="text-muted">
Sessions gelten als inaktiv nach 5 Minuten ohne Heartbeat
</small>
{% else %}
<p class="text-muted mb-0">Keine aktiven Sessions vorhanden.</p>
{% endif %}
</div>
</div>
<!-- Beendete Sessions -->
<div class="card">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">⏸️ Beendete Sessions (letzte 24 Stunden)</h5>
</div>
<div class="card-body">
{% if recent_sessions %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Kunde</th>
<th>Lizenz</th>
<th>IP-Adresse</th>
<th>Gestartet</th>
<th>Beendet</th>
<th>Dauer</th>
</tr>
</thead>
<tbody>
{% for session in recent_sessions %}
<tr class="session-inactive">
<td>{{ session[3] }}</td>
<td><small><code>{{ session[2][:12] }}...</code></small></td>
<td>{{ session[4] or '-' }}</td>
<td>{{ session[5].strftime('%d.%m %H:%M') }}</td>
<td>{{ session[6].strftime('%d.%m %H:%M') }}</td>
<td>
{% if session[7] < 60 %}
{{ session[7]|round|int }} Min.
{% else %}
{{ (session[7]/60)|round(1) }} Std.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted mb-0">Keine beendeten Sessions in den letzten 24 Stunden.</p>
{% endif %}
</div>
</div>
</div>
</body>
</html>

194
v2_testing/test_sessions.py Normale Datei
Datei anzeigen

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
import requests
import urllib3
import subprocess
from datetime import datetime
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
base_url = "https://localhost:443"
admin_user = {"username": "rac00n", "password": "1248163264"}
def login(session):
"""Login to admin panel"""
login_data = {
"username": admin_user["username"],
"password": admin_user["password"]
}
response = session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False)
return response.status_code == 302
def test_session_table():
"""Test if session table exists and structure"""
print("1. Checking Session Table Structure:")
print("-" * 40)
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", "\\d sessions"
], capture_output=True, text=True)
if "Table \"public.sessions\"" in result.stdout:
print("✓ Sessions table exists")
print("\nTable structure:")
print(result.stdout)
else:
print("✗ Sessions table not found")
return False
# Check if table is empty
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM sessions;"
], capture_output=True, text=True)
count = int(result.stdout.strip())
print(f"\nCurrent session count: {count}")
if count == 0:
print("✓ Session table is empty (as expected - License Server not implemented)")
return True
def test_session_page():
"""Test the sessions management page"""
session = requests.Session()
if not login(session):
return "✗ Failed to login"
print("\n2. Testing Sessions Page:")
print("-" * 40)
response = session.get(f"{base_url}/sessions", verify=False)
if response.status_code == 200:
print("✓ Sessions page accessible")
content = response.text
# Check for expected elements
checks = [
("Aktive Sessions", "Active sessions section"),
("Letzte Sessions", "Recent sessions section"),
("IP-Adresse", "IP address column"),
("User Agent", "User agent column"),
("Heartbeat", "Heartbeat info"),
("Session beenden", "End session button")
]
for check_text, description in checks:
if check_text in content:
print(f"✓ Found: {description}")
else:
print(f"✗ Missing: {description}")
# Check for empty state
if "Keine aktiven Sessions" in content:
print("✓ Shows empty state for active sessions")
else:
print(f"✗ Failed to access sessions page: Status {response.status_code}")
def test_dashboard_session_count():
"""Test if dashboard shows session count"""
session = requests.Session()
if not login(session):
return "✗ Failed to login"
print("\n3. Testing Dashboard Session Counter:")
print("-" * 40)
response = session.get(f"{base_url}/", verify=False)
if response.status_code == 200:
content = response.text
if "Aktive Sessions" in content or "sessions" in content.lower():
print("✓ Dashboard shows session information")
# Check if it shows 0
if "0" in content:
print("✓ Shows 0 active sessions (correct)")
else:
print("✗ No session information on dashboard")
def simulate_session_data():
"""Add test session data directly to database"""
print("\n4. Simulating Session Data:")
print("-" * 40)
# Get a license ID
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT id FROM licenses WHERE is_active = TRUE LIMIT 1;"
], capture_output=True, text=True)
license_id = result.stdout.strip()
if not license_id:
print("✗ No active license found for test")
return
# Insert test sessions
test_sessions = [
# Active session
f"""INSERT INTO sessions (license_id, session_id, ip_address, user_agent, is_active)
VALUES ({license_id}, 'TEST-SESSION-001', '192.168.1.100',
'Mozilla/5.0 Test Browser', TRUE);""",
# Inactive session from today
f"""INSERT INTO sessions (license_id, session_id, ip_address, user_agent,
started_at, ended_at, is_active)
VALUES ({license_id}, 'TEST-SESSION-002', '10.0.0.50',
'Chrome/120.0 Windows',
NOW() - INTERVAL '2 hours', NOW() - INTERVAL '1 hour', FALSE);"""
]
for sql in test_sessions:
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", sql
], capture_output=True, text=True)
if "INSERT" in result.stdout:
print("✓ Test session inserted")
else:
print("✗ Failed to insert test session")
# Verify
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", "SELECT COUNT(*) as total, COUNT(CASE WHEN is_active THEN 1 END) as active FROM sessions;"
], capture_output=True, text=True)
print("\nSession count after simulation:")
print(result.stdout)
# Main execution
print("Testing Session Management Features")
print("=" * 50)
# Rebuild admin panel
print("Rebuilding admin panel with session features...")
subprocess.run(["docker-compose", "build", "admin-panel"], capture_output=True)
subprocess.run(["docker-compose", "up", "-d"], capture_output=True)
subprocess.run(["sleep", "5"], capture_output=True)
# Run tests
test_session_table()
test_session_page()
test_dashboard_session_count()
simulate_session_data()
# Test again with data
print("\n5. Re-testing with simulated data:")
print("-" * 40)
test_session_page()
# Cleanup test data
print("\n6. Cleaning up test data:")
subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", "DELETE FROM sessions WHERE session_id LIKE 'TEST-%';"
], capture_output=True, text=True)
print("✓ Test sessions removed")