Lizenzserver - Integration Admin Panel
Dieser Commit ist enthalten in:
@@ -553,4 +553,422 @@ def clear_attempts():
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('admin.blocked_ips'))
|
||||
return redirect(url_for('admin.blocked_ips'))
|
||||
|
||||
|
||||
# ===================== LICENSE SERVER MONITORING ROUTES =====================
|
||||
|
||||
@admin_bp.route("/lizenzserver/monitor")
|
||||
@login_required
|
||||
def license_monitor():
|
||||
"""License server live monitoring dashboard"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Get current statistics
|
||||
# Active validations in last 5 minutes
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT license_id) as active_licenses,
|
||||
COUNT(*) as total_validations,
|
||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||
COUNT(DISTINCT ip_address) as unique_ips
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||
""")
|
||||
live_stats = cur.fetchone()
|
||||
|
||||
# Get validation rate (per minute)
|
||||
cur.execute("""
|
||||
SELECT DATE_TRUNC('minute', timestamp) as minute,
|
||||
COUNT(*) as validations
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '10 minutes'
|
||||
GROUP BY minute
|
||||
ORDER BY minute DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
validation_rates = cur.fetchall()
|
||||
|
||||
# Get top active licenses
|
||||
cur.execute("""
|
||||
SELECT l.id, l.license_key, c.name as customer_name,
|
||||
COUNT(DISTINCT lh.hardware_id) as device_count,
|
||||
COUNT(*) as validation_count,
|
||||
MAX(lh.timestamp) as last_seen
|
||||
FROM licenses l
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
JOIN license_heartbeats lh ON l.id = lh.license_id
|
||||
WHERE lh.timestamp > NOW() - INTERVAL '15 minutes'
|
||||
GROUP BY l.id, l.license_key, c.name
|
||||
ORDER BY validation_count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
top_licenses = cur.fetchall()
|
||||
|
||||
# Get recent anomalies
|
||||
cur.execute("""
|
||||
SELECT ad.*, l.license_key, c.name as customer_name
|
||||
FROM anomaly_detections ad
|
||||
LEFT JOIN licenses l ON ad.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE ad.resolved = false
|
||||
ORDER BY ad.detected_at DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
recent_anomalies = cur.fetchall()
|
||||
|
||||
# Get geographic distribution
|
||||
cur.execute("""
|
||||
SELECT ip_address, COUNT(*) as count
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '1 hour'
|
||||
AND ip_address IS NOT NULL
|
||||
GROUP BY ip_address
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
""")
|
||||
geo_distribution = cur.fetchall()
|
||||
|
||||
return render_template('license_monitor.html',
|
||||
live_stats=live_stats,
|
||||
validation_rates=validation_rates,
|
||||
top_licenses=top_licenses,
|
||||
recent_anomalies=recent_anomalies,
|
||||
geo_distribution=geo_distribution
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Monitoring-Daten: {str(e)}', 'error')
|
||||
return render_template('license_monitor.html')
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/lizenzserver/analytics")
|
||||
@login_required
|
||||
def license_analytics():
|
||||
"""License usage analytics"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Time range from query params
|
||||
days = int(request.args.get('days', 30))
|
||||
|
||||
# Usage trends over time
|
||||
cur.execute("""
|
||||
SELECT DATE(timestamp) as date,
|
||||
COUNT(DISTINCT license_id) as unique_licenses,
|
||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||
COUNT(*) as total_validations
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '%s days'
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
""", (days,))
|
||||
usage_trends = cur.fetchall()
|
||||
|
||||
# License performance metrics
|
||||
cur.execute("""
|
||||
SELECT l.id, l.license_key, c.name as customer_name,
|
||||
COUNT(DISTINCT lh.hardware_id) as device_count,
|
||||
l.max_devices,
|
||||
COUNT(*) as total_validations,
|
||||
COUNT(DISTINCT DATE(lh.timestamp)) as active_days,
|
||||
MIN(lh.timestamp) as first_seen,
|
||||
MAX(lh.timestamp) as last_seen
|
||||
FROM licenses l
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
|
||||
WHERE lh.timestamp > NOW() - INTERVAL '%s days'
|
||||
GROUP BY l.id, l.license_key, c.name, l.max_devices
|
||||
ORDER BY total_validations DESC
|
||||
""", (days,))
|
||||
license_metrics = cur.fetchall()
|
||||
|
||||
# Device distribution
|
||||
cur.execute("""
|
||||
SELECT l.max_devices as limit,
|
||||
COUNT(*) as license_count,
|
||||
AVG(device_count) as avg_usage
|
||||
FROM licenses l
|
||||
LEFT JOIN (
|
||||
SELECT license_id, COUNT(DISTINCT hardware_id) as device_count
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '30 days'
|
||||
GROUP BY license_id
|
||||
) usage ON l.id = usage.license_id
|
||||
WHERE l.is_active = true
|
||||
GROUP BY l.max_devices
|
||||
ORDER BY l.max_devices
|
||||
""")
|
||||
device_distribution = cur.fetchall()
|
||||
|
||||
# Revenue analysis
|
||||
cur.execute("""
|
||||
SELECT l.license_type,
|
||||
COUNT(DISTINCT l.id) as license_count,
|
||||
COUNT(DISTINCT CASE WHEN lh.license_id IS NOT NULL THEN l.id END) as active_licenses,
|
||||
COUNT(DISTINCT lh.hardware_id) as total_devices
|
||||
FROM licenses l
|
||||
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
|
||||
AND lh.timestamp > NOW() - INTERVAL '%s days'
|
||||
GROUP BY l.license_type
|
||||
""", (days,))
|
||||
revenue_analysis = cur.fetchall()
|
||||
|
||||
return render_template('license_analytics.html',
|
||||
days=days,
|
||||
usage_trends=usage_trends,
|
||||
license_metrics=license_metrics,
|
||||
device_distribution=device_distribution,
|
||||
revenue_analysis=revenue_analysis
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Analytics-Daten: {str(e)}', 'error')
|
||||
return render_template('license_analytics.html', days=30)
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/lizenzserver/anomalies")
|
||||
@login_required
|
||||
def license_anomalies():
|
||||
"""Anomaly detection and management"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Filter parameters
|
||||
severity = request.args.get('severity', 'all')
|
||||
resolved = request.args.get('resolved', 'false')
|
||||
|
||||
# Build query
|
||||
query = """
|
||||
SELECT ad.*, l.license_key, c.name as customer_name, c.email
|
||||
FROM anomaly_detections ad
|
||||
LEFT JOIN licenses l ON ad.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if severity != 'all':
|
||||
query += " AND ad.severity = %s"
|
||||
params.append(severity)
|
||||
|
||||
if resolved == 'false':
|
||||
query += " AND ad.resolved = false"
|
||||
elif resolved == 'true':
|
||||
query += " AND ad.resolved = true"
|
||||
|
||||
query += " ORDER BY ad.detected_at DESC LIMIT 100"
|
||||
|
||||
cur.execute(query, params)
|
||||
anomalies = cur.fetchall()
|
||||
|
||||
# Get anomaly statistics
|
||||
cur.execute("""
|
||||
SELECT anomaly_type, severity, COUNT(*) as count
|
||||
FROM anomaly_detections
|
||||
WHERE resolved = false
|
||||
GROUP BY anomaly_type, severity
|
||||
ORDER BY count DESC
|
||||
""")
|
||||
anomaly_stats = cur.fetchall()
|
||||
|
||||
return render_template('license_anomalies.html',
|
||||
anomalies=anomalies,
|
||||
anomaly_stats=anomaly_stats,
|
||||
severity=severity,
|
||||
resolved=resolved
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Anomalie-Daten: {str(e)}', 'error')
|
||||
return render_template('license_anomalies.html')
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/lizenzserver/anomaly/<anomaly_id>/resolve", methods=["POST"])
|
||||
@login_required
|
||||
def resolve_anomaly(anomaly_id):
|
||||
"""Resolve an anomaly"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
action_taken = request.form.get('action_taken', '')
|
||||
|
||||
cur.execute("""
|
||||
UPDATE anomaly_detections
|
||||
SET resolved = true,
|
||||
resolved_at = NOW(),
|
||||
resolved_by = %s,
|
||||
action_taken = %s
|
||||
WHERE id = %s
|
||||
""", (session.get('username'), action_taken, str(anomaly_id)))
|
||||
|
||||
conn.commit()
|
||||
|
||||
flash('Anomalie wurde als behoben markiert', 'success')
|
||||
log_audit('RESOLVE_ANOMALY', 'license_server', entity_id=str(anomaly_id),
|
||||
additional_info=f"Action: {action_taken}")
|
||||
|
||||
except Exception as e:
|
||||
if 'conn' in locals():
|
||||
conn.rollback()
|
||||
flash(f'Fehler beim Beheben der Anomalie: {str(e)}', 'error')
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('admin.license_anomalies'))
|
||||
|
||||
|
||||
@admin_bp.route("/lizenzserver/config")
|
||||
@login_required
|
||||
def license_config():
|
||||
"""License server configuration"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Get feature flags
|
||||
cur.execute("""
|
||||
SELECT * FROM feature_flags
|
||||
ORDER BY feature_name
|
||||
""")
|
||||
feature_flags = cur.fetchall()
|
||||
|
||||
# Get API clients
|
||||
cur.execute("""
|
||||
SELECT id, client_name, api_key, is_active, created_at
|
||||
FROM api_clients
|
||||
ORDER BY created_at DESC
|
||||
""")
|
||||
api_clients = cur.fetchall()
|
||||
|
||||
# Get rate limits
|
||||
cur.execute("""
|
||||
SELECT * FROM api_rate_limits
|
||||
ORDER BY api_key
|
||||
""")
|
||||
rate_limits = cur.fetchall()
|
||||
|
||||
return render_template('license_config.html',
|
||||
feature_flags=feature_flags,
|
||||
api_clients=api_clients,
|
||||
rate_limits=rate_limits
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Konfiguration: {str(e)}', 'error')
|
||||
return render_template('license_config.html')
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/lizenzserver/config/feature-flag/<int:flag_id>", methods=["POST"])
|
||||
@login_required
|
||||
def update_feature_flag(flag_id):
|
||||
"""Update feature flag settings"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
is_enabled = request.form.get('is_enabled') == 'on'
|
||||
rollout_percentage = int(request.form.get('rollout_percentage', 0))
|
||||
|
||||
cur.execute("""
|
||||
UPDATE feature_flags
|
||||
SET is_enabled = %s,
|
||||
rollout_percentage = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (is_enabled, rollout_percentage, flag_id))
|
||||
|
||||
conn.commit()
|
||||
|
||||
flash('Feature Flag wurde aktualisiert', 'success')
|
||||
log_audit('UPDATE_FEATURE_FLAG', 'license_server', entity_id=flag_id)
|
||||
|
||||
except Exception as e:
|
||||
if 'conn' in locals():
|
||||
conn.rollback()
|
||||
flash(f'Fehler beim Aktualisieren: {str(e)}', 'error')
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('admin.license_config'))
|
||||
|
||||
|
||||
@admin_bp.route("/api/admin/lizenzserver/live-stats")
|
||||
@login_required
|
||||
def license_live_stats():
|
||||
"""API endpoint for live statistics (for AJAX updates)"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Get real-time stats
|
||||
cur.execute("""
|
||||
SELECT COUNT(DISTINCT license_id) as active_licenses,
|
||||
COUNT(*) as validations_per_minute,
|
||||
COUNT(DISTINCT hardware_id) as active_devices
|
||||
FROM license_heartbeats
|
||||
WHERE timestamp > NOW() - INTERVAL '1 minute'
|
||||
""")
|
||||
stats = cur.fetchone()
|
||||
|
||||
# Get latest validations
|
||||
cur.execute("""
|
||||
SELECT l.license_key, lh.hardware_id, lh.ip_address, lh.timestamp
|
||||
FROM license_heartbeats lh
|
||||
JOIN licenses l ON lh.license_id = l.id
|
||||
ORDER BY lh.timestamp DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
latest_validations = cur.fetchall()
|
||||
|
||||
return jsonify({
|
||||
'active_licenses': stats[0] or 0,
|
||||
'validations_per_minute': stats[1] or 0,
|
||||
'active_devices': stats[2] or 0,
|
||||
'latest_validations': [
|
||||
{
|
||||
'license_key': v[0][:8] + '...',
|
||||
'hardware_id': v[1][:8] + '...',
|
||||
'ip_address': v[2] or 'Unknown',
|
||||
'timestamp': v[3].strftime('%H:%M:%S')
|
||||
} for v in latest_validations
|
||||
]
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
@@ -338,6 +338,11 @@ def api_customer_licenses(customer_id):
|
||||
WHEN l.is_active = false THEN 'inaktiv'
|
||||
ELSE 'aktiv'
|
||||
END as status,
|
||||
-- License Server Status
|
||||
(SELECT COUNT(*) FROM license_heartbeats lh WHERE lh.license_id = l.id AND lh.timestamp > NOW() - INTERVAL '5 minutes') as recent_heartbeats,
|
||||
(SELECT MAX(timestamp) FROM license_heartbeats lh WHERE lh.license_id = l.id) as last_heartbeat,
|
||||
(SELECT COUNT(DISTINCT hardware_id) FROM license_heartbeats lh WHERE lh.license_id = l.id AND lh.timestamp > NOW() - INTERVAL '15 minutes') as active_server_devices,
|
||||
(SELECT COUNT(*) FROM anomaly_detections ad WHERE ad.license_id = l.id AND ad.resolved = false) as unresolved_anomalies,
|
||||
l.domain_count,
|
||||
l.ipv4_count,
|
||||
l.phone_count,
|
||||
@@ -408,14 +413,19 @@ def api_customer_licenses(customer_id):
|
||||
'active_sessions': row[9],
|
||||
'registered_devices': row[10],
|
||||
'status': row[11],
|
||||
'domain_count': row[12],
|
||||
'ipv4_count': row[13],
|
||||
'phone_count': row[14],
|
||||
'active_devices': row[15],
|
||||
'actual_domain_count': row[16],
|
||||
'actual_ipv4_count': row[17],
|
||||
'actual_phone_count': row[18],
|
||||
'resources': resources
|
||||
'domain_count': row[16],
|
||||
'ipv4_count': row[17],
|
||||
'phone_count': row[18],
|
||||
'active_devices': row[19],
|
||||
'actual_domain_count': row[20],
|
||||
'actual_ipv4_count': row[21],
|
||||
'actual_phone_count': row[22],
|
||||
'resources': resources,
|
||||
# License Server Data
|
||||
'recent_heartbeats': row[12],
|
||||
'last_heartbeat': row[13].strftime('%Y-%m-%d %H:%M:%S') if row[13] else None,
|
||||
'active_server_devices': row[14],
|
||||
'unresolved_anomalies': row[15]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren