Add latest changes
Dieser Commit ist enthalten in:
@@ -4,6 +4,7 @@ from zoneinfo import ZoneInfo
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, send_file, jsonify, current_app
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
import config
|
||||
from config import DATABASE_CONFIG
|
||||
@@ -12,6 +13,7 @@ from utils.audit import log_audit
|
||||
from utils.backup import create_backup, restore_backup, create_backup_with_github, create_server_backup
|
||||
from utils.network import get_client_ip
|
||||
from db import get_connection, get_db_connection, get_db_cursor, execute_query
|
||||
from db_license import get_license_db_cursor
|
||||
from utils.export import create_excel_export, prepare_audit_export_data
|
||||
|
||||
# Create Blueprint
|
||||
@@ -116,9 +118,17 @@ def dashboard():
|
||||
cur.execute("SELECT COUNT(*) FROM licenses WHERE is_fake = true")
|
||||
test_licenses_count = cur.fetchone()[0] if cur.rowcount > 0 else 0
|
||||
|
||||
# Anzahl aktiver Sessions (Admin-Panel)
|
||||
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = true")
|
||||
active_sessions = cur.fetchone()[0] if cur.rowcount > 0 else 0
|
||||
# Anzahl aktiver Sessions aus License Server DB
|
||||
active_sessions = 0
|
||||
try:
|
||||
with get_license_db_cursor() as license_cur:
|
||||
license_cur.execute("SELECT COUNT(*) FROM license_sessions WHERE ended_at IS NULL")
|
||||
active_sessions = license_cur.fetchone()[0] if license_cur.rowcount > 0 else 0
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"Could not get active sessions from license server: {str(e)}")
|
||||
# Fallback auf Admin DB
|
||||
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = true")
|
||||
active_sessions = cur.fetchone()[0] if cur.rowcount > 0 else 0
|
||||
|
||||
# Aktive Nutzung (Kunden-Software) - Lizenzen mit Heartbeats in den letzten 15 Minuten
|
||||
active_usage = 0
|
||||
@@ -333,6 +343,52 @@ def dashboard():
|
||||
except:
|
||||
pass
|
||||
|
||||
# Session-Auslastung (Concurrent Sessions)
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
SUM(active_sessions) as total_active_sessions,
|
||||
SUM(max_concurrent_sessions) as total_max_sessions,
|
||||
COUNT(CASE WHEN active_sessions >= max_concurrent_sessions THEN 1 END) as at_limit_count
|
||||
FROM (
|
||||
SELECT
|
||||
l.id,
|
||||
l.max_concurrent_sessions,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions
|
||||
FROM licenses l
|
||||
WHERE l.is_fake = false AND l.is_active = true
|
||||
) session_data
|
||||
""")
|
||||
session_stats = cur.fetchone()
|
||||
if session_stats:
|
||||
total_active = session_stats[0] or 0
|
||||
total_max = session_stats[1] or 0
|
||||
at_limit = session_stats[2] or 0
|
||||
utilization = int((total_active / total_max * 100)) if total_max > 0 else 0
|
||||
|
||||
stats['session_stats'] = {
|
||||
'total_active_sessions': total_active,
|
||||
'total_max_sessions': total_max,
|
||||
'utilization_percent': utilization,
|
||||
'licenses_at_limit': at_limit
|
||||
}
|
||||
else:
|
||||
stats['session_stats'] = {
|
||||
'total_active_sessions': 0,
|
||||
'total_max_sessions': 0,
|
||||
'utilization_percent': 0,
|
||||
'licenses_at_limit': 0
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"Could not get session statistics: {str(e)}")
|
||||
stats['session_stats'] = {
|
||||
'total_active_sessions': 0,
|
||||
'total_max_sessions': 0,
|
||||
'utilization_percent': 0,
|
||||
'licenses_at_limit': 0
|
||||
}
|
||||
conn.rollback()
|
||||
|
||||
license_distribution = []
|
||||
hourly_sessions = []
|
||||
|
||||
@@ -621,7 +677,12 @@ def create_backup_route():
|
||||
def restore_backup_route(backup_id):
|
||||
"""Backup wiederherstellen"""
|
||||
from flask import jsonify
|
||||
encryption_key = request.form.get('encryption_key')
|
||||
|
||||
# Handle both JSON and form data
|
||||
if request.is_json:
|
||||
encryption_key = request.json.get('encryption_key')
|
||||
else:
|
||||
encryption_key = request.form.get('encryption_key')
|
||||
|
||||
success, message = restore_backup(backup_id, encryption_key)
|
||||
|
||||
@@ -967,7 +1028,7 @@ def license_analytics():
|
||||
AVG(device_count) as avg_usage
|
||||
FROM licenses l
|
||||
LEFT JOIN (
|
||||
SELECT license_id, COUNT(DISTINCT hardware_id) as device_count
|
||||
SELECT license_id, COUNT(DISTINCT hardware_fingerprint) as device_count
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '30 days'
|
||||
GROUP BY license_id
|
||||
@@ -1282,7 +1343,7 @@ def terminate_session(session_id):
|
||||
|
||||
# Get session info
|
||||
cur.execute("""
|
||||
SELECT license_id, hardware_id, ip_address, client_version, started_at
|
||||
SELECT license_id, hardware_fingerprint, ip_address, client_version, started_at
|
||||
FROM license_sessions
|
||||
WHERE id = %s
|
||||
""", (session_id,))
|
||||
@@ -1424,6 +1485,9 @@ def regenerate_api_key():
|
||||
random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
||||
new_api_key = f"AF-{year_part}-{random_part}"
|
||||
|
||||
# Log what we're attempting
|
||||
app.logger.info(f"Attempting to regenerate API key. New key: {new_api_key[:10]}...")
|
||||
|
||||
# Update the API key
|
||||
cur.execute("""
|
||||
UPDATE system_api_key
|
||||
@@ -1433,15 +1497,27 @@ def regenerate_api_key():
|
||||
WHERE id = 1
|
||||
""", (new_api_key, session.get('username')))
|
||||
|
||||
# Log rows affected
|
||||
app.logger.info(f"Rows affected by UPDATE: {cur.rowcount}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
flash('API Key wurde erfolgreich regeneriert', 'success')
|
||||
# Verify the update
|
||||
cur.execute("SELECT api_key FROM system_api_key WHERE id = 1")
|
||||
result = cur.fetchone()
|
||||
if result and result[0] == new_api_key:
|
||||
app.logger.info("API key successfully updated in database")
|
||||
flash('API Key wurde erfolgreich regeneriert', 'success')
|
||||
else:
|
||||
app.logger.error(f"API key update verification failed. Expected: {new_api_key[:10]}..., Found: {result[0][:10] if result else 'None'}...")
|
||||
flash('API Key wurde regeneriert, aber Verifizierung fehlgeschlagen', 'warning')
|
||||
|
||||
# Log action
|
||||
log_audit('API_KEY_REGENERATED', 'system_api_key', 1,
|
||||
additional_info="API Key regenerated")
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error regenerating API key: {str(e)}", exc_info=True)
|
||||
conn.rollback()
|
||||
flash(f'Fehler beim Regenerieren des API Keys: {str(e)}', 'error')
|
||||
|
||||
@@ -1452,6 +1528,63 @@ def regenerate_api_key():
|
||||
return redirect(url_for('admin.license_config'))
|
||||
|
||||
|
||||
@admin_bp.route("/api-key/test-regenerate", methods=["GET"])
|
||||
@login_required
|
||||
def test_regenerate_api_key():
|
||||
"""Test endpoint to check if regeneration works"""
|
||||
import string
|
||||
import random
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check current API key
|
||||
cur.execute("SELECT api_key, regenerated_at FROM system_api_key WHERE id = 1")
|
||||
current = cur.fetchone()
|
||||
|
||||
# Generate new API key
|
||||
year_part = datetime.now().strftime('%Y')
|
||||
random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
||||
new_api_key = f"AF-{year_part}-{random_part}"
|
||||
|
||||
# Update the API key
|
||||
cur.execute("""
|
||||
UPDATE system_api_key
|
||||
SET api_key = %s,
|
||||
regenerated_at = CURRENT_TIMESTAMP,
|
||||
regenerated_by = %s
|
||||
WHERE id = 1
|
||||
""", (new_api_key, session.get('username')))
|
||||
|
||||
rows_affected = cur.rowcount
|
||||
conn.commit()
|
||||
|
||||
# Verify the update
|
||||
cur.execute("SELECT api_key, regenerated_at FROM system_api_key WHERE id = 1")
|
||||
updated = cur.fetchone()
|
||||
|
||||
result = {
|
||||
'current_api_key': current[0] if current else None,
|
||||
'current_regenerated_at': str(current[1]) if current and current[1] else None,
|
||||
'new_api_key': new_api_key,
|
||||
'rows_affected': rows_affected,
|
||||
'updated_api_key': updated[0] if updated else None,
|
||||
'updated_regenerated_at': str(updated[1]) if updated and updated[1] else None,
|
||||
'success': updated and updated[0] == new_api_key
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
return jsonify({'error': str(e), 'traceback': traceback.format_exc()})
|
||||
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/test-api-key")
|
||||
@login_required
|
||||
def test_api_key():
|
||||
|
||||
@@ -193,29 +193,27 @@ def get_license_devices(license_id):
|
||||
cur.execute("""
|
||||
SELECT
|
||||
dr.id,
|
||||
dr.hardware_id,
|
||||
dr.hardware_fingerprint,
|
||||
dr.device_name,
|
||||
dr.device_type,
|
||||
dr.first_seen as registration_date,
|
||||
dr.last_seen,
|
||||
dr.first_activated_at as registration_date,
|
||||
dr.last_seen_at,
|
||||
dr.is_active,
|
||||
dr.operating_system,
|
||||
dr.ip_address,
|
||||
(SELECT COUNT(*) FROM sessions s
|
||||
WHERE s.license_key = l.license_key
|
||||
AND s.hardware_id = dr.hardware_id
|
||||
AND s.is_active = true) as active_sessions
|
||||
(SELECT COUNT(*) FROM license_sessions ls
|
||||
WHERE ls.device_registration_id = dr.id
|
||||
AND ls.ended_at IS NULL) as active_sessions
|
||||
FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_id = l.id
|
||||
WHERE l.license_key = %s
|
||||
ORDER BY dr.first_seen DESC
|
||||
""", (license_data['license_key'],))
|
||||
WHERE dr.license_id = %s
|
||||
ORDER BY dr.last_seen_at DESC
|
||||
""", (license_id,))
|
||||
|
||||
devices = []
|
||||
for row in cur.fetchall():
|
||||
devices.append({
|
||||
'id': row[0],
|
||||
'hardware_id': row[1],
|
||||
'hardware_fingerprint': row[1],
|
||||
'device_name': row[2],
|
||||
'device_type': row[3],
|
||||
'registration_date': row[4].isoformat() if row[4] else None,
|
||||
@@ -268,22 +266,20 @@ def register_device(license_id):
|
||||
|
||||
# Prüfe Gerätelimit
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_id = l.id
|
||||
WHERE l.license_key = %s AND dr.is_active = true
|
||||
""", (license_data['license_key'],))
|
||||
SELECT COUNT(*) FROM device_registrations
|
||||
WHERE license_id = %s AND is_active = true
|
||||
""", (license_id,))
|
||||
|
||||
active_device_count = cur.fetchone()[0]
|
||||
|
||||
if active_device_count >= license_data['device_limit']:
|
||||
if active_device_count >= license_data.get('device_limit', 3):
|
||||
return jsonify({'error': 'Gerätelimit erreicht'}), 400
|
||||
|
||||
# Prüfe ob Gerät bereits registriert
|
||||
cur.execute("""
|
||||
SELECT dr.id, dr.is_active FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_id = l.id
|
||||
WHERE l.license_key = %s AND dr.hardware_id = %s
|
||||
""", (license_data['license_key'], hardware_id))
|
||||
SELECT id, is_active FROM device_registrations
|
||||
WHERE license_id = %s AND hardware_fingerprint = %s
|
||||
""", (license_id, hardware_id))
|
||||
|
||||
existing = cur.fetchone()
|
||||
|
||||
@@ -294,16 +290,18 @@ def register_device(license_id):
|
||||
# Reaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = true, last_seen = CURRENT_TIMESTAMP
|
||||
SET is_active = true, last_seen_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""", (existing[0],))
|
||||
else:
|
||||
# Registriere neues Gerät
|
||||
cur.execute("""
|
||||
INSERT INTO device_registrations
|
||||
(license_id, hardware_id, device_name, device_type, is_active)
|
||||
VALUES (%s, %s, %s, %s, true)
|
||||
""", (license_id, hardware_id, device_name, device_type))
|
||||
(license_id, hardware_fingerprint, device_name, device_type, is_active,
|
||||
first_activated_at, last_seen_at, operating_system, app_version)
|
||||
VALUES (%s, %s, %s, %s, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, %s, %s)
|
||||
""", (license_id, hardware_id, device_name, device_type,
|
||||
data.get('operating_system', 'unknown'), data.get('app_version')))
|
||||
|
||||
conn.commit()
|
||||
|
||||
@@ -332,7 +330,7 @@ def deactivate_device(license_id, device_id):
|
||||
try:
|
||||
# Prüfe ob Gerät zur Lizenz gehört
|
||||
cur.execute("""
|
||||
SELECT dr.device_name, dr.hardware_id, l.license_key
|
||||
SELECT dr.device_name, dr.hardware_fingerprint, l.license_key
|
||||
FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_id = l.id
|
||||
WHERE dr.id = %s AND l.id = %s
|
||||
@@ -345,15 +343,15 @@ def deactivate_device(license_id, device_id):
|
||||
# Deaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = false
|
||||
SET is_active = false, deactivated_at = CURRENT_TIMESTAMP, deactivated_by = %s
|
||||
WHERE id = %s
|
||||
""", (device_id,))
|
||||
""", (session.get('username'), device_id))
|
||||
|
||||
# Beende aktive Sessions
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET is_active = false, ended_at = CURRENT_TIMESTAMP
|
||||
WHERE license_key = %s AND hardware_id = %s AND is_active = true
|
||||
WHERE license_key = %s AND hardware_fingerprint = %s AND is_active = true
|
||||
""", (device[2], device[1]))
|
||||
|
||||
conn.commit()
|
||||
@@ -440,7 +438,7 @@ def bulk_delete_licenses():
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM activations
|
||||
FROM device_registrations
|
||||
WHERE license_id = %s
|
||||
AND is_active = true
|
||||
""", (license_id,))
|
||||
@@ -451,7 +449,7 @@ def bulk_delete_licenses():
|
||||
skipped_licenses.append(license_id)
|
||||
continue
|
||||
except:
|
||||
# If activations table doesn't exist, continue
|
||||
# If device_registrations table doesn't exist, continue
|
||||
pass
|
||||
|
||||
# Delete associated data
|
||||
@@ -468,7 +466,7 @@ def bulk_delete_licenses():
|
||||
pass
|
||||
|
||||
try:
|
||||
cur.execute("DELETE FROM activations WHERE license_id = %s", (license_id,))
|
||||
cur.execute("DELETE FROM license_sessions WHERE license_id = %s", (license_id,))
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -946,9 +944,9 @@ def global_search():
|
||||
|
||||
# Suche in Sessions
|
||||
cur.execute("""
|
||||
SELECT id, license_key, username, hardware_id, is_active
|
||||
SELECT id, license_key, username, hardware_fingerprint as hardware_id, is_active
|
||||
FROM sessions
|
||||
WHERE username ILIKE %s OR hardware_id ILIKE %s
|
||||
WHERE username ILIKE %s OR hardware_fingerprint ILIKE %s
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%'))
|
||||
|
||||
@@ -81,12 +81,14 @@ def batch_create():
|
||||
INSERT INTO licenses (
|
||||
license_key, customer_id,
|
||||
license_type, valid_from, valid_until, device_limit,
|
||||
max_devices, max_concurrent_sessions,
|
||||
is_fake, created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
license_key, customer_id,
|
||||
license_type, valid_from, valid_until, device_limit,
|
||||
device_limit, 1, # max_devices = device_limit, max_concurrent_sessions = 1 (default)
|
||||
is_fake, datetime.now()
|
||||
))
|
||||
|
||||
|
||||
@@ -338,7 +338,9 @@ def api_customer_licenses(customer_id):
|
||||
END as status,
|
||||
COALESCE(l.domain_count, 0) as domain_count,
|
||||
COALESCE(l.ipv4_count, 0) as ipv4_count,
|
||||
COALESCE(l.phone_count, 0) as phone_count
|
||||
COALESCE(l.phone_count, 0) as phone_count,
|
||||
l.max_concurrent_sessions,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions
|
||||
FROM licenses l
|
||||
WHERE l.customer_id = %s
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
@@ -379,6 +381,13 @@ def api_customer_licenses(customer_id):
|
||||
elif res_row[1] == 'phone':
|
||||
resources['phones'].append(resource_data)
|
||||
|
||||
# Count active devices from activations table
|
||||
cur2.execute("""
|
||||
SELECT COUNT(*) FROM activations
|
||||
WHERE license_id = %s AND is_active = true
|
||||
""", (license_id,))
|
||||
active_device_count = cur2.fetchone()[0]
|
||||
|
||||
cur2.close()
|
||||
conn2.close()
|
||||
|
||||
@@ -396,9 +405,10 @@ def api_customer_licenses(customer_id):
|
||||
'domain_count': row[10],
|
||||
'ipv4_count': row[11],
|
||||
'phone_count': row[12],
|
||||
'active_sessions': 0, # Platzhalter
|
||||
'registered_devices': 0, # Platzhalter
|
||||
'active_devices': 0, # Platzhalter
|
||||
'max_concurrent_sessions': row[13],
|
||||
'active_sessions': row[14],
|
||||
'registered_devices': active_device_count,
|
||||
'active_devices': active_device_count,
|
||||
'actual_domain_count': len(resources['domains']),
|
||||
'actual_ipv4_count': len(resources['ipv4s']),
|
||||
'actual_phone_count': len(resources['phones']),
|
||||
|
||||
@@ -32,6 +32,7 @@ def export_licenses():
|
||||
l.valid_until,
|
||||
l.is_active,
|
||||
l.device_limit,
|
||||
l.max_concurrent_sessions,
|
||||
l.created_at,
|
||||
l.is_fake,
|
||||
CASE
|
||||
@@ -39,8 +40,8 @@ def export_licenses():
|
||||
WHEN l.is_active = false THEN 'Deaktiviert'
|
||||
ELSE 'Aktiv'
|
||||
END as status,
|
||||
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.is_active = true) as active_sessions,
|
||||
(SELECT COUNT(DISTINCT hardware_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions,
|
||||
(SELECT COUNT(DISTINCT hardware_fingerprint) FROM device_registrations dr WHERE dr.license_id = l.id AND dr.is_active = true) as registered_devices
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.is_fake = false
|
||||
@@ -52,7 +53,7 @@ def export_licenses():
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 'Gültig von',
|
||||
'Gültig bis', 'Aktiv', 'Gerätelimit', 'Erstellt am', 'Fake-Lizenz',
|
||||
'Gültig bis', 'Aktiv', 'Gerätelimit', 'Max. Sessions', 'Erstellt am', 'Fake-Lizenz',
|
||||
'Status', 'Aktive Sessions', 'Registrierte Geräte']
|
||||
|
||||
for row in cur.fetchall():
|
||||
@@ -62,8 +63,8 @@ def export_licenses():
|
||||
row_data[5] = format_datetime_for_export(row_data[5])
|
||||
if row_data[6]: # valid_until
|
||||
row_data[6] = format_datetime_for_export(row_data[6])
|
||||
if row_data[9]: # created_at
|
||||
row_data[9] = format_datetime_for_export(row_data[9])
|
||||
if row_data[10]: # created_at (index shifted due to max_concurrent_sessions)
|
||||
row_data[10] = format_datetime_for_export(row_data[10])
|
||||
data.append(row_data)
|
||||
|
||||
# Format prüfen
|
||||
@@ -239,7 +240,7 @@ def export_sessions():
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
s.username,
|
||||
s.hardware_id,
|
||||
s.hardware_fingerprint as hardware_id,
|
||||
s.started_at,
|
||||
s.ended_at,
|
||||
s.last_heartbeat,
|
||||
@@ -259,7 +260,7 @@ def export_sessions():
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
s.username,
|
||||
s.hardware_id,
|
||||
s.hardware_fingerprint as hardware_id,
|
||||
s.started_at,
|
||||
s.ended_at,
|
||||
s.last_heartbeat,
|
||||
@@ -416,7 +417,7 @@ def export_monitoring():
|
||||
lh.license_id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
lh.hardware_id,
|
||||
lh.hardware_fingerprint as hardware_id,
|
||||
lh.ip_address,
|
||||
'Heartbeat' as event_type,
|
||||
'Normal' as severity,
|
||||
@@ -447,7 +448,7 @@ def export_monitoring():
|
||||
ad.license_id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
ad.details->>'hardware_id' as hardware_id,
|
||||
ad.details->>'hardware_fingerprint' as hardware_id,
|
||||
ad.details->>'ip_address' as ip_address,
|
||||
ad.anomaly_type as event_type,
|
||||
ad.severity,
|
||||
|
||||
@@ -118,13 +118,14 @@ def edit_license(license_id):
|
||||
'valid_from': request.form['valid_from'],
|
||||
'valid_until': request.form['valid_until'],
|
||||
'is_active': 'is_active' in request.form,
|
||||
'device_limit': int(request.form.get('device_limit', 3))
|
||||
'max_devices': int(request.form.get('device_limit', 3)), # Form still uses device_limit
|
||||
'max_concurrent_sessions': int(request.form.get('max_concurrent_sessions', 1))
|
||||
}
|
||||
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET license_key = %s, license_type = %s, valid_from = %s,
|
||||
valid_until = %s, is_active = %s, device_limit = %s
|
||||
valid_until = %s, is_active = %s, max_devices = %s, max_concurrent_sessions = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
new_values['license_key'],
|
||||
@@ -132,7 +133,8 @@ def edit_license(license_id):
|
||||
new_values['valid_from'],
|
||||
new_values['valid_until'],
|
||||
new_values['is_active'],
|
||||
new_values['device_limit'],
|
||||
new_values['max_devices'],
|
||||
new_values['max_concurrent_sessions'],
|
||||
license_id
|
||||
))
|
||||
|
||||
@@ -146,7 +148,8 @@ def edit_license(license_id):
|
||||
'valid_from': str(current_license.get('valid_from', '')),
|
||||
'valid_until': str(current_license.get('valid_until', '')),
|
||||
'is_active': current_license.get('is_active'),
|
||||
'device_limit': current_license.get('device_limit', 3)
|
||||
'max_devices': current_license.get('max_devices', 3),
|
||||
'max_concurrent_sessions': current_license.get('max_concurrent_sessions', 1)
|
||||
},
|
||||
new_values=new_values)
|
||||
|
||||
@@ -313,6 +316,7 @@ def create_license():
|
||||
ipv4_count = int(request.form.get("ipv4_count", 1))
|
||||
phone_count = int(request.form.get("phone_count", 1))
|
||||
device_limit = int(request.form.get("device_limit", 3))
|
||||
max_concurrent_sessions = int(request.form.get("max_concurrent_sessions", 1))
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
@@ -365,11 +369,11 @@ def create_license():
|
||||
# Lizenz hinzufügen
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
||||
domain_count, ipv4_count, phone_count, device_limit, is_fake)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
|
||||
domain_count, ipv4_count, phone_count, device_limit, max_devices, max_concurrent_sessions, is_fake)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
||||
domain_count, ipv4_count, phone_count, device_limit, is_fake))
|
||||
domain_count, ipv4_count, phone_count, device_limit, device_limit, max_concurrent_sessions, is_fake))
|
||||
license_id = cur.fetchone()[0]
|
||||
|
||||
# Ressourcen zuweisen
|
||||
|
||||
@@ -91,7 +91,7 @@ def unified_monitoring():
|
||||
SELECT
|
||||
COUNT(DISTINCT license_id) as active_licenses,
|
||||
COUNT(*) as total_validations,
|
||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||
COUNT(DISTINCT hardware_fingerprint) as unique_devices,
|
||||
COUNT(DISTINCT ip_address) as unique_ips,
|
||||
0 as avg_response_time
|
||||
FROM license_heartbeats
|
||||
@@ -126,7 +126,7 @@ def unified_monitoring():
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
lh.ip_address,
|
||||
lh.hardware_id,
|
||||
lh.hardware_fingerprint,
|
||||
NULL as anomaly_type,
|
||||
NULL as description
|
||||
FROM license_heartbeats lh
|
||||
@@ -143,8 +143,8 @@ def unified_monitoring():
|
||||
ad.severity,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
ad.details->>'ip_address' as ip_address,
|
||||
ad.details->>'hardware_id' as hardware_id,
|
||||
(ad.details->>'ip_address')::inet as ip_address,
|
||||
ad.details->>'hardware_fingerprint' as hardware_fingerprint,
|
||||
ad.anomaly_type,
|
||||
ad.details->>'description' as description
|
||||
FROM anomaly_detections ad
|
||||
@@ -166,7 +166,7 @@ def unified_monitoring():
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
lh.ip_address,
|
||||
lh.hardware_id,
|
||||
lh.hardware_fingerprint,
|
||||
NULL as anomaly_type,
|
||||
NULL as description
|
||||
FROM license_heartbeats lh
|
||||
@@ -199,7 +199,7 @@ def unified_monitoring():
|
||||
l.id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
COUNT(DISTINCT lh.hardware_id) as device_count,
|
||||
COUNT(DISTINCT lh.hardware_fingerprint) as device_count,
|
||||
COUNT(lh.*) as validation_count,
|
||||
MAX(lh.timestamp) as last_seen,
|
||||
COUNT(DISTINCT ad.id) as anomaly_count
|
||||
@@ -220,7 +220,7 @@ def unified_monitoring():
|
||||
l.id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
COUNT(DISTINCT lh.hardware_id) as device_count,
|
||||
COUNT(DISTINCT lh.hardware_fingerprint) as device_count,
|
||||
COUNT(lh.*) as validation_count,
|
||||
MAX(lh.timestamp) as last_seen,
|
||||
0 as anomaly_count
|
||||
@@ -345,7 +345,7 @@ def analytics():
|
||||
SELECT
|
||||
COUNT(DISTINCT license_id) as active_licenses,
|
||||
COUNT(*) as total_validations,
|
||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||
COUNT(DISTINCT hardware_fingerprint) as unique_devices,
|
||||
COUNT(DISTINCT ip_address) as unique_ips
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||
@@ -403,7 +403,7 @@ def analytics_stream():
|
||||
SELECT
|
||||
COUNT(DISTINCT license_id) as active_licenses,
|
||||
COUNT(*) as total_validations,
|
||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||
COUNT(DISTINCT hardware_fingerprint) as unique_devices,
|
||||
COUNT(DISTINCT ip_address) as unique_ips
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||
@@ -425,4 +425,15 @@ def analytics_stream():
|
||||
time.sleep(5) # Update every 5 seconds
|
||||
|
||||
from flask import Response
|
||||
return Response(generate(), mimetype="text/event-stream")
|
||||
return Response(generate(), mimetype="text/event-stream")
|
||||
|
||||
@monitoring_bp.route('/device_limits')
|
||||
@login_required
|
||||
def device_limits():
|
||||
"""Device limit monitoring dashboard"""
|
||||
from utils.device_monitoring import check_device_limits, get_device_usage_stats
|
||||
|
||||
warnings = check_device_limits()
|
||||
stats = get_device_usage_stats()
|
||||
|
||||
return render_template('monitoring/device_limits.html', warnings=warnings, stats=stats)
|
||||
@@ -8,6 +8,7 @@ from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from utils.network import get_client_ip
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from db_license import get_license_db_cursor
|
||||
from models import get_active_sessions
|
||||
|
||||
# Create Blueprint
|
||||
@@ -17,37 +18,72 @@ session_bp = Blueprint('sessions', __name__)
|
||||
@session_bp.route("/sessions")
|
||||
@login_required
|
||||
def sessions():
|
||||
# Use regular DB for customer/license info
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get is_active sessions with calculated inactive time
|
||||
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()
|
||||
# First get license mapping from admin DB
|
||||
cur.execute("SELECT id, license_key FROM licenses")
|
||||
license_map = {row[0]: row[1] for row in cur.fetchall()}
|
||||
|
||||
# Get recent ended sessions
|
||||
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()
|
||||
# Get customer mapping
|
||||
cur.execute("SELECT l.id, c.name FROM licenses l JOIN customers c ON l.customer_id = c.id")
|
||||
customer_map = {row[0]: row[1] for row in cur.fetchall()}
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Now get sessions from license server DB
|
||||
with get_license_db_cursor() as license_cur:
|
||||
# Get active sessions
|
||||
license_cur.execute("""
|
||||
SELECT id, license_id, session_token, ip_address, client_version,
|
||||
started_at, last_heartbeat, hardware_id,
|
||||
EXTRACT(EPOCH FROM (NOW() - last_heartbeat))/60 as minutes_inactive
|
||||
FROM license_sessions
|
||||
WHERE ended_at IS NULL
|
||||
ORDER BY last_heartbeat DESC
|
||||
""")
|
||||
|
||||
active_sessions = []
|
||||
for row in license_cur.fetchall():
|
||||
active_sessions.append((
|
||||
row[0], # id
|
||||
row[2], # session_token
|
||||
license_map.get(row[1], 'Unknown'), # license_key
|
||||
customer_map.get(row[1], 'Unknown'), # customer name
|
||||
row[3], # ip_address
|
||||
row[4], # client_version
|
||||
row[5], # started_at
|
||||
row[6], # last_heartbeat
|
||||
row[8] # minutes_inactive
|
||||
))
|
||||
|
||||
# Get recent ended sessions
|
||||
license_cur.execute("""
|
||||
SELECT id, license_id, session_token, ip_address,
|
||||
started_at, ended_at,
|
||||
EXTRACT(EPOCH FROM (ended_at - started_at))/60 as duration_minutes
|
||||
FROM license_sessions
|
||||
WHERE ended_at IS NOT NULL
|
||||
AND ended_at > NOW() - INTERVAL '24 hours'
|
||||
ORDER BY ended_at DESC
|
||||
LIMIT 50
|
||||
""")
|
||||
|
||||
recent_sessions = []
|
||||
for row in license_cur.fetchall():
|
||||
recent_sessions.append((
|
||||
row[0], # id
|
||||
row[2], # session_token
|
||||
license_map.get(row[1], 'Unknown'), # license_key
|
||||
customer_map.get(row[1], 'Unknown'), # customer name
|
||||
row[3], # ip_address
|
||||
row[4], # started_at
|
||||
row[5], # ended_at
|
||||
row[6] # duration_minutes
|
||||
))
|
||||
|
||||
return render_template("sessions.html",
|
||||
active_sessions=active_sessions,
|
||||
@@ -57,9 +93,6 @@ def sessions():
|
||||
logging.error(f"Error loading sessions: {str(e)}")
|
||||
flash('Fehler beim Laden der Sessions!', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@session_bp.route("/sessions/history")
|
||||
@@ -78,19 +111,20 @@ def session_history():
|
||||
# Base query
|
||||
query = """
|
||||
SELECT
|
||||
s.id,
|
||||
s.license_key,
|
||||
s.username,
|
||||
s.hardware_id,
|
||||
s.started_at,
|
||||
s.ended_at,
|
||||
s.last_heartbeat,
|
||||
s.is_active,
|
||||
l.customer_name,
|
||||
ls.id,
|
||||
l.license_key,
|
||||
ls.machine_name as username,
|
||||
ls.hardware_id,
|
||||
ls.started_at,
|
||||
ls.ended_at,
|
||||
ls.last_heartbeat,
|
||||
CASE WHEN ls.ended_at IS NULL THEN true ELSE false END as is_active,
|
||||
c.name as customer_name,
|
||||
l.license_type,
|
||||
l.is_test
|
||||
FROM sessions s
|
||||
LEFT JOIN licenses l ON s.license_key = l.license_key
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
@@ -98,18 +132,18 @@ def session_history():
|
||||
|
||||
# Apply filters
|
||||
if license_key:
|
||||
query += " AND s.license_key = %s"
|
||||
query += " AND l.license_key = %s"
|
||||
params.append(license_key)
|
||||
|
||||
if username:
|
||||
query += " AND s.username ILIKE %s"
|
||||
query += " AND ls.machine_name ILIKE %s"
|
||||
params.append(f'%{username}%')
|
||||
|
||||
# Time filter
|
||||
query += " AND s.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days'"
|
||||
query += " AND ls.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days'"
|
||||
params.append(days)
|
||||
|
||||
query += " ORDER BY s.started_at DESC LIMIT 1000"
|
||||
query += " ORDER BY ls.started_at DESC LIMIT 1000"
|
||||
|
||||
cur.execute(query, params)
|
||||
|
||||
@@ -144,11 +178,12 @@ def session_history():
|
||||
|
||||
# Get unique license keys for filter dropdown
|
||||
cur.execute("""
|
||||
SELECT DISTINCT s.license_key, l.customer_name
|
||||
FROM sessions s
|
||||
LEFT JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.started_at >= CURRENT_TIMESTAMP - INTERVAL '30 days'
|
||||
ORDER BY l.customer_name, s.license_key
|
||||
SELECT DISTINCT l.license_key, c.name as customer_name
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE ls.started_at >= CURRENT_TIMESTAMP - INTERVAL '30 days'
|
||||
ORDER BY c.name, l.license_key
|
||||
""")
|
||||
|
||||
available_licenses = []
|
||||
@@ -180,44 +215,48 @@ def session_history():
|
||||
@login_required
|
||||
def terminate_session(session_id):
|
||||
"""Beendet eine aktive Session"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get session info
|
||||
cur.execute("""
|
||||
SELECT license_key, username, hardware_id
|
||||
FROM sessions
|
||||
WHERE id = %s AND is_active = true
|
||||
""", (session_id,))
|
||||
session_info = None
|
||||
|
||||
session_info = cur.fetchone()
|
||||
if not session_info:
|
||||
flash('Session nicht gefunden oder bereits beendet!', 'error')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
# Get session info from license server DB
|
||||
with get_license_db_cursor() as license_cur:
|
||||
license_cur.execute("""
|
||||
SELECT license_id, hardware_id, machine_name
|
||||
FROM license_sessions
|
||||
WHERE id = %s AND ended_at IS NULL
|
||||
""", (session_id,))
|
||||
|
||||
result = license_cur.fetchone()
|
||||
if not result:
|
||||
flash('Session nicht gefunden oder bereits beendet!', 'error')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
|
||||
license_id = result[0]
|
||||
|
||||
# Terminate session in license server DB
|
||||
license_cur.execute("""
|
||||
UPDATE license_sessions
|
||||
SET ended_at = CURRENT_TIMESTAMP, end_reason = 'admin_terminated'
|
||||
WHERE id = %s
|
||||
""", (session_id,))
|
||||
|
||||
# Terminate session
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET is_active = false, ended_at = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""", (session_id,))
|
||||
|
||||
conn.commit()
|
||||
# Get license key from admin DB for audit log
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT license_key FROM licenses WHERE id = %s", (license_id,))
|
||||
license_key = cur.fetchone()[0] if cur.fetchone() else 'Unknown'
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Audit log
|
||||
log_audit('SESSION_TERMINATE', 'session', session_id,
|
||||
additional_info=f"Session beendet für {session_info[1]} auf Lizenz {session_info[0]}")
|
||||
additional_info=f"Session beendet für Lizenz {license_key}")
|
||||
|
||||
flash('Session erfolgreich beendet!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Beenden der Session: {str(e)}")
|
||||
flash('Fehler beim Beenden der Session!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
|
||||
@@ -230,10 +269,11 @@ def terminate_all_sessions(license_key):
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Count is_active sessions
|
||||
# Count active sessions
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM sessions
|
||||
WHERE license_key = %s AND is_active = true
|
||||
SELECT COUNT(*) FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
WHERE l.license_key = %s AND ls.ended_at IS NULL
|
||||
""", (license_key,))
|
||||
|
||||
active_count = cur.fetchone()[0]
|
||||
@@ -244,9 +284,11 @@ def terminate_all_sessions(license_key):
|
||||
|
||||
# Terminate all sessions
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET is_active = false, ended_at = CURRENT_TIMESTAMP
|
||||
WHERE license_key = %s AND is_active = true
|
||||
UPDATE license_sessions
|
||||
SET ended_at = CURRENT_TIMESTAMP, end_reason = 'admin_terminated_all'
|
||||
WHERE license_id IN (
|
||||
SELECT id FROM licenses WHERE license_key = %s
|
||||
) AND ended_at IS NULL
|
||||
""", (license_key,))
|
||||
|
||||
conn.commit()
|
||||
@@ -280,8 +322,8 @@ def cleanup_sessions():
|
||||
|
||||
# Delete old inactive sessions
|
||||
cur.execute("""
|
||||
DELETE FROM sessions
|
||||
WHERE is_active = false
|
||||
DELETE FROM license_sessions
|
||||
WHERE ended_at IS NOT NULL
|
||||
AND ended_at < CURRENT_TIMESTAMP - INTERVAL '%s days'
|
||||
RETURNING id
|
||||
""", (days,))
|
||||
@@ -320,12 +362,13 @@ def session_statistics():
|
||||
# Aktuelle Statistiken
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(DISTINCT s.license_key) as active_licenses,
|
||||
COUNT(DISTINCT s.username) as unique_users,
|
||||
COUNT(DISTINCT s.hardware_id) as unique_devices,
|
||||
COUNT(DISTINCT l.license_key) as active_licenses,
|
||||
COUNT(DISTINCT ls.machine_name) as unique_users,
|
||||
COUNT(DISTINCT ls.hardware_id) as unique_devices,
|
||||
COUNT(*) as total_active_sessions
|
||||
FROM sessions s
|
||||
WHERE s.is_active = true
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
WHERE ls.ended_at IS NULL
|
||||
""")
|
||||
|
||||
current_stats = cur.fetchone()
|
||||
@@ -335,9 +378,9 @@ def session_statistics():
|
||||
SELECT
|
||||
l.license_type,
|
||||
COUNT(*) as session_count
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.is_active = true
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
WHERE ls.ended_at IS NULL
|
||||
GROUP BY l.license_type
|
||||
ORDER BY session_count DESC
|
||||
""")
|
||||
@@ -352,14 +395,15 @@ def session_statistics():
|
||||
# Top 10 Lizenzen nach aktiven Sessions
|
||||
cur.execute("""
|
||||
SELECT
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
COUNT(*) as session_count,
|
||||
l.device_limit
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.is_active = true
|
||||
GROUP BY s.license_key, l.customer_name, l.device_limit
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
WHERE ls.ended_at IS NULL
|
||||
GROUP BY l.license_key, c.name, l.device_limit
|
||||
ORDER BY session_count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
@@ -376,13 +420,14 @@ def session_statistics():
|
||||
# Session-Verlauf (letzte 7 Tage)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
DATE(started_at) as date,
|
||||
DATE(ls.started_at) as date,
|
||||
COUNT(*) as login_count,
|
||||
COUNT(DISTINCT license_key) as unique_licenses,
|
||||
COUNT(DISTINCT username) as unique_users
|
||||
FROM sessions
|
||||
WHERE started_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||
GROUP BY DATE(started_at)
|
||||
COUNT(DISTINCT l.license_key) as unique_licenses,
|
||||
COUNT(DISTINCT ls.machine_name) as unique_users
|
||||
FROM license_sessions ls
|
||||
JOIN licenses l ON ls.license_id = l.id
|
||||
WHERE ls.started_at >= CURRENT_DATE - INTERVAL '7 days'
|
||||
GROUP BY DATE(ls.started_at)
|
||||
ORDER BY date
|
||||
""")
|
||||
|
||||
@@ -399,9 +444,8 @@ def session_statistics():
|
||||
cur.execute("""
|
||||
SELECT
|
||||
AVG(EXTRACT(EPOCH FROM (ended_at - started_at))/3600) as avg_duration_hours
|
||||
FROM sessions
|
||||
WHERE is_active = false
|
||||
AND ended_at IS NOT NULL
|
||||
FROM license_sessions
|
||||
WHERE ended_at IS NOT NULL
|
||||
AND ended_at - started_at < INTERVAL '24 hours'
|
||||
AND started_at >= CURRENT_DATE - INTERVAL '30 days'
|
||||
""")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren