From 6f491e3833eae74641071105d8165303997dc2af Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Sat, 7 Jun 2025 14:49:40 +0200 Subject: [PATCH] Session Management (Noch leer) --- JOURNAL.md | 31 +++- v2_adminpanel/app.py | 68 ++++++++- v2_adminpanel/init.sql | 12 ++ v2_adminpanel/templates/customers.html | 1 + v2_adminpanel/templates/dashboard.html | 7 +- v2_adminpanel/templates/index.html | 1 + v2_adminpanel/templates/licenses.html | 1 + v2_adminpanel/templates/sessions.html | 139 ++++++++++++++++++ v2_testing/test_sessions.py | 194 +++++++++++++++++++++++++ 9 files changed, 449 insertions(+), 5 deletions(-) create mode 100644 v2_adminpanel/templates/sessions.html create mode 100644 v2_testing/test_sessions.py diff --git a/JOURNAL.md b/JOURNAL.md index 35ac487..d9d2d6e 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -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 \ No newline at end of file +- 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. \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index bf8f574..8b3c4aa 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -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/", 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') diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index abf57a1..d27099f 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -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 +); diff --git a/v2_adminpanel/templates/customers.html b/v2_adminpanel/templates/customers.html index 18c1293..815b613 100644 --- a/v2_adminpanel/templates/customers.html +++ b/v2_adminpanel/templates/customers.html @@ -23,6 +23,7 @@ 📊 Dashboard ➕ Neue Lizenz 📋 Lizenzen + 🟢 Sessions diff --git a/v2_adminpanel/templates/dashboard.html b/v2_adminpanel/templates/dashboard.html index d503c97..0564281 100644 --- a/v2_adminpanel/templates/dashboard.html +++ b/v2_adminpanel/templates/dashboard.html @@ -34,6 +34,7 @@ ➕ Neue Lizenz 📋 Lizenzen 👥 Kunden + 🟢 Sessions @@ -69,9 +70,9 @@
-
⚠️ Ablaufend
-

{{ stats.expiring_soon }}

-

Nächste 30 Tage

+
🟢 Sessions
+

{{ stats.active_sessions }}

+

Aktive Nutzer

diff --git a/v2_adminpanel/templates/index.html b/v2_adminpanel/templates/index.html index 3825b87..f6b2e79 100644 --- a/v2_adminpanel/templates/index.html +++ b/v2_adminpanel/templates/index.html @@ -23,6 +23,7 @@ 📊 Dashboard 📋 Lizenzen 👥 Kunden + 🟢 Sessions diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index 671f6e8..332a142 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -28,6 +28,7 @@ 📊 Dashboard ➕ Neue Lizenz 👥 Kunden + 🟢 Sessions diff --git a/v2_adminpanel/templates/sessions.html b/v2_adminpanel/templates/sessions.html new file mode 100644 index 0000000..b53cf71 --- /dev/null +++ b/v2_adminpanel/templates/sessions.html @@ -0,0 +1,139 @@ + + + + + Session-Tracking - Admin Panel + + + + + + +
+
+

Session-Tracking

+
+ 📊 Dashboard + 📋 Lizenzen + 👥 Kunden +
+
+ + +
+
+
🟢 Aktive Sessions ({{ active_sessions|length }})
+
+
+ {% if active_sessions %} +
+ + + + + + + + + + + + + + {% for session in active_sessions %} + + + + + + + + + + {% endfor %} + +
KundeLizenzIP-AdresseGestartetLetzter HeartbeatInaktiv seitAktion
{{ session[3] }}{{ session[2][:12] }}...{{ session[4] or '-' }}{{ session[6].strftime('%d.%m %H:%M') }}{{ session[7].strftime('%d.%m %H:%M') }} + {% if session[8] < 1 %} + Aktiv + {% elif session[8] < 5 %} + {{ session[8]|round|int }} Min. + {% else %} + {{ session[8]|round|int }} Min. + {% endif %} + +
+ +
+
+
+ + Sessions gelten als inaktiv nach 5 Minuten ohne Heartbeat + + {% else %} +

Keine aktiven Sessions vorhanden.

+ {% endif %} +
+
+ + +
+
+
⏸️ Beendete Sessions (letzte 24 Stunden)
+
+
+ {% if recent_sessions %} +
+ + + + + + + + + + + + + {% for session in recent_sessions %} + + + + + + + + + {% endfor %} + +
KundeLizenzIP-AdresseGestartetBeendetDauer
{{ session[3] }}{{ session[2][:12] }}...{{ session[4] or '-' }}{{ session[5].strftime('%d.%m %H:%M') }}{{ session[6].strftime('%d.%m %H:%M') }} + {% if session[7] < 60 %} + {{ session[7]|round|int }} Min. + {% else %} + {{ (session[7]/60)|round(1) }} Std. + {% endif %} +
+
+ {% else %} +

Keine beendeten Sessions in den letzten 24 Stunden.

+ {% endif %} +
+
+
+ + \ No newline at end of file diff --git a/v2_testing/test_sessions.py b/v2_testing/test_sessions.py new file mode 100644 index 0000000..4509644 --- /dev/null +++ b/v2_testing/test_sessions.py @@ -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") \ No newline at end of file