Add latest changes
Dieser Commit ist enthalten in:
@@ -15,13 +15,18 @@ RUN apt-get update && apt-get install -y \
|
||||
locales \
|
||||
postgresql-client \
|
||||
tzdata \
|
||||
git \
|
||||
git-lfs \
|
||||
openssh-client \
|
||||
&& sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen \
|
||||
&& locale-gen \
|
||||
&& update-locale LANG=de_DE.UTF-8 \
|
||||
&& ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
|
||||
&& echo "Europe/Berlin" > /etc/timezone \
|
||||
&& git lfs install \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& git config --global --add safe.directory /opt/v2-Docker
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
Binäre Datei nicht angezeigt.
@@ -10,7 +10,7 @@ SECRET_KEY = os.urandom(24)
|
||||
SESSION_TYPE = 'filesystem'
|
||||
JSON_AS_ASCII = False
|
||||
JSONIFY_MIMETYPE = 'application/json; charset=utf-8'
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(minutes=5)
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(minutes=15)
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = os.getenv("SESSION_COOKIE_SECURE", "true").lower() == "true" # Default True for HTTPS
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
62
v2_adminpanel/db_license.py
Normale Datei
62
v2_adminpanel/db_license.py
Normale Datei
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Database connection helper for License Server database
|
||||
"""
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
# License Server DB configuration
|
||||
LICENSE_DB_CONFIG = {
|
||||
'host': 'db', # Same container name as in docker network
|
||||
'port': 5432,
|
||||
'database': 'meinedatenbank', # License Server database name
|
||||
'user': 'adminuser',
|
||||
'password': 'supergeheimespasswort'
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_license_db_connection():
|
||||
"""Get a connection to the license server database"""
|
||||
try:
|
||||
conn = psycopg2.connect(**LICENSE_DB_CONFIG)
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to license server database: {str(e)}")
|
||||
raise
|
||||
|
||||
@contextmanager
|
||||
def get_license_db_cursor(dict_cursor=False):
|
||||
"""Context manager for license server database cursor"""
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = get_license_db_connection()
|
||||
cursor_factory = RealDictCursor if dict_cursor else None
|
||||
cur = conn.cursor(cursor_factory=cursor_factory)
|
||||
yield cur
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
logger.error(f"License DB error: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
if cur:
|
||||
cur.close()
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def test_license_db_connection():
|
||||
"""Test the connection to license server database"""
|
||||
try:
|
||||
with get_license_db_cursor() as cur:
|
||||
cur.execute("SELECT 1")
|
||||
result = cur.fetchone()
|
||||
if result:
|
||||
logger.info("Successfully connected to license server database")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to test license server database connection: {str(e)}")
|
||||
return False
|
||||
@@ -570,6 +570,37 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Migration: Add max_concurrent_sessions column to licenses if it doesn't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'licenses' AND column_name = 'max_concurrent_sessions') THEN
|
||||
ALTER TABLE licenses ADD COLUMN max_concurrent_sessions INTEGER DEFAULT 1 CHECK (max_concurrent_sessions >= 1);
|
||||
-- Set initial value to same as max_devices for existing licenses
|
||||
UPDATE licenses SET max_concurrent_sessions = max_devices WHERE max_concurrent_sessions IS NULL;
|
||||
-- Add constraint to ensure concurrent sessions don't exceed device limit
|
||||
ALTER TABLE licenses ADD CONSTRAINT check_concurrent_sessions CHECK (max_concurrent_sessions <= max_devices);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Migration: Remove UNIQUE constraint on license_sessions.license_id to allow multiple concurrent sessions
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Check if the unique constraint exists and drop it
|
||||
IF EXISTS (SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'license_sessions_license_id_key'
|
||||
AND conrelid = 'license_sessions'::regclass) THEN
|
||||
ALTER TABLE license_sessions DROP CONSTRAINT license_sessions_license_id_key;
|
||||
END IF;
|
||||
|
||||
-- Add a compound index for better performance on concurrent session queries
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'license_sessions'
|
||||
AND indexname = 'idx_license_sessions_license_hardware') THEN
|
||||
CREATE INDEX idx_license_sessions_license_hardware ON license_sessions(license_id, hardware_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
|
||||
-- Migration: Add device_type column to device_registrations table
|
||||
DO $$
|
||||
|
||||
@@ -39,14 +39,16 @@ def get_licenses(show_fake=False):
|
||||
with get_db_cursor(conn) as cur:
|
||||
if show_fake:
|
||||
cur.execute("""
|
||||
SELECT l.*, c.name as customer_name
|
||||
SELECT l.*, c.name as customer_name,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
ORDER BY l.created_at DESC
|
||||
""")
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT l.*, c.name as customer_name
|
||||
SELECT l.*, c.name as customer_name,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.is_fake = false
|
||||
@@ -70,7 +72,8 @@ def get_license_by_id(license_id):
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT l.*, c.name as customer_name
|
||||
SELECT l.*, c.name as customer_name,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.id = %s
|
||||
@@ -86,6 +89,37 @@ def get_license_by_id(license_id):
|
||||
return None
|
||||
|
||||
|
||||
def get_license_session_stats(license_id):
|
||||
"""Get session statistics for a specific license"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.device_limit,
|
||||
l.concurrent_sessions_limit,
|
||||
(SELECT COUNT(*) FROM device_registrations dr WHERE dr.license_id = l.id AND dr.is_active = true) as registered_devices,
|
||||
(SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as active_sessions,
|
||||
l.concurrent_sessions_limit - (SELECT COUNT(*) FROM license_sessions ls WHERE ls.license_id = l.id) as available_sessions
|
||||
FROM licenses l
|
||||
WHERE l.id = %s
|
||||
""", (license_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {
|
||||
'device_limit': row[0],
|
||||
'concurrent_sessions_limit': row[1],
|
||||
'registered_devices': row[2],
|
||||
'active_sessions': row[3],
|
||||
'available_sessions': row[4]
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching session stats for license {license_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def get_customers(show_fake=False, search=None):
|
||||
"""Get all customers from database"""
|
||||
try:
|
||||
@@ -175,4 +209,67 @@ def get_active_sessions():
|
||||
return sessions
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching is_active sessions: {str(e)}")
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def get_devices_for_license(license_id):
|
||||
"""Get all registered devices for a specific license"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
id,
|
||||
hardware_fingerprint,
|
||||
device_name,
|
||||
device_type,
|
||||
operating_system,
|
||||
app_version,
|
||||
first_activated_at,
|
||||
last_seen_at,
|
||||
is_active,
|
||||
ip_address,
|
||||
(SELECT COUNT(*) FROM license_sessions ls
|
||||
WHERE ls.device_registration_id = dr.id) as active_sessions
|
||||
FROM device_registrations dr
|
||||
WHERE dr.license_id = %s
|
||||
ORDER BY dr.last_seen_at DESC
|
||||
""", (license_id,))
|
||||
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
devices = []
|
||||
for row in cur.fetchall():
|
||||
device_dict = dict(zip(columns, row))
|
||||
devices.append(device_dict)
|
||||
return devices
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching devices for license {license_id}: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def check_device_limit(license_id):
|
||||
"""Check if license has reached its device limit"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.device_limit,
|
||||
COUNT(dr.id) as active_devices
|
||||
FROM licenses l
|
||||
LEFT JOIN device_registrations dr ON l.id = dr.license_id AND dr.is_active = true
|
||||
WHERE l.id = %s
|
||||
GROUP BY l.device_limit
|
||||
""", (license_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {
|
||||
'device_limit': row[0],
|
||||
'active_devices': row[1],
|
||||
'limit_reached': row[1] >= row[0]
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking device limit for license {license_id}: {str(e)}")
|
||||
return None
|
||||
@@ -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'
|
||||
""")
|
||||
|
||||
@@ -15,7 +15,7 @@ def scheduled_backup():
|
||||
|
||||
|
||||
def cleanup_expired_sessions():
|
||||
"""Clean up expired license sessions"""
|
||||
"""Clean up expired license sessions - concurrent sessions aware"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
@@ -29,11 +29,12 @@ def cleanup_expired_sessions():
|
||||
result = cur.fetchone()
|
||||
timeout_seconds = result[0] if result else 60
|
||||
|
||||
# Find expired sessions
|
||||
# Find expired sessions that are still active
|
||||
cur.execute("""
|
||||
SELECT id, license_id, hardware_id, ip_address, client_version, started_at
|
||||
SELECT id, license_id, hardware_fingerprint, ip_address, client_version, started_at, hardware_id, machine_name
|
||||
FROM license_sessions
|
||||
WHERE last_heartbeat < CURRENT_TIMESTAMP - INTERVAL '%s seconds'
|
||||
WHERE ended_at IS NULL
|
||||
AND last_heartbeat < CURRENT_TIMESTAMP - INTERVAL '%s seconds'
|
||||
""", (timeout_seconds,))
|
||||
|
||||
expired_sessions = cur.fetchall()
|
||||
@@ -41,19 +42,32 @@ def cleanup_expired_sessions():
|
||||
if expired_sessions:
|
||||
logging.info(f"Found {len(expired_sessions)} expired sessions to clean up")
|
||||
|
||||
# Count sessions by license before cleanup for logging
|
||||
license_session_counts = {}
|
||||
for session in expired_sessions:
|
||||
license_id = session[1]
|
||||
if license_id not in license_session_counts:
|
||||
license_session_counts[license_id] = 0
|
||||
license_session_counts[license_id] += 1
|
||||
|
||||
for session in expired_sessions:
|
||||
# Log to history
|
||||
cur.execute("""
|
||||
INSERT INTO session_history
|
||||
(license_id, hardware_id, ip_address, client_version, started_at, ended_at, end_reason)
|
||||
VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP, 'timeout')
|
||||
""", (session[1], session[2], session[3], session[4], session[5]))
|
||||
(license_id, hardware_id, hardware_fingerprint, machine_name, ip_address, client_version, started_at, ended_at, end_reason)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, 'timeout')
|
||||
""", (session[1], session[6], session[2], session[7], session[3], session[4], session[5]))
|
||||
|
||||
# Delete session
|
||||
cur.execute("DELETE FROM license_sessions WHERE id = %s", (session[0],))
|
||||
# Mark session as ended instead of deleting
|
||||
cur.execute("UPDATE license_sessions SET ended_at = CURRENT_TIMESTAMP, end_reason = 'timeout' WHERE id = %s", (session[0],))
|
||||
|
||||
conn.commit()
|
||||
logging.info(f"Cleaned up {len(expired_sessions)} expired sessions")
|
||||
|
||||
# Log cleanup summary
|
||||
logging.info(f"Cleaned up {len(expired_sessions)} expired sessions from {len(license_session_counts)} licenses")
|
||||
for license_id, count in license_session_counts.items():
|
||||
if count > 1:
|
||||
logging.info(f" License ID {license_id}: {count} sessions cleaned up")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
@@ -219,7 +219,7 @@ function createBackup(type) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Erstelle...';
|
||||
|
||||
fetch('/backups/backup/create', {
|
||||
fetch('/admin/backup/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -249,7 +249,7 @@ function createBackup(type) {
|
||||
}
|
||||
|
||||
function downloadFromGitHub(backupId) {
|
||||
window.location.href = `/backups/backup/download/${backupId}?from_github=true`;
|
||||
window.location.href = `/admin/backup/download/${backupId}?from_github=true`;
|
||||
}
|
||||
|
||||
function showRestoreModal(backupId) {
|
||||
@@ -261,7 +261,7 @@ function showRestoreModal(backupId) {
|
||||
function confirmRestore() {
|
||||
const encryptionKey = document.getElementById('encryptionKey').value;
|
||||
|
||||
fetch(`/backups/backup/restore/${selectedBackupId}`, {
|
||||
fetch(`/admin/backup/restore/${selectedBackupId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<div id="session-timer" class="timer-normal me-3">
|
||||
⏱️ <span id="timer-display">5:00</span>
|
||||
⏱️ <span id="timer-display">15:00</span>
|
||||
</div>
|
||||
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
|
||||
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-light btn-sm me-2">👤 Profil</a>
|
||||
@@ -482,7 +482,7 @@
|
||||
|
||||
<script>
|
||||
// Session-Timer Konfiguration
|
||||
const SESSION_TIMEOUT = 5 * 60; // 5 Minuten in Sekunden
|
||||
const SESSION_TIMEOUT = 15 * 60; // 15 Minuten in Sekunden
|
||||
let timeRemaining = SESSION_TIMEOUT;
|
||||
let timerInterval;
|
||||
let warningShown = false;
|
||||
|
||||
@@ -174,6 +174,19 @@
|
||||
Jede generierte Lizenz kann auf maximal dieser Anzahl von Geräten gleichzeitig aktiviert werden.
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">
|
||||
Max. gleichzeitige Sessions pro Lizenz
|
||||
</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% for i in range(1, 11) %}
|
||||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Wie viele Geräte können gleichzeitig online sein. Muss kleiner oder gleich dem Gerätelimit sein.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,6 +259,30 @@ document.getElementById('validFrom').addEventListener('change', calculateValidUn
|
||||
document.getElementById('duration').addEventListener('input', calculateValidUntil);
|
||||
document.getElementById('durationType').addEventListener('change', calculateValidUntil);
|
||||
|
||||
// Funktion zur Anpassung der max_concurrent_sessions Optionen
|
||||
function updateConcurrentSessionsOptions() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSelect = document.getElementById('concurrentSessions');
|
||||
const currentValue = parseInt(concurrentSelect.value);
|
||||
|
||||
// Clear current options
|
||||
concurrentSelect.innerHTML = '';
|
||||
|
||||
// Add new options up to device limit
|
||||
for (let i = 1; i <= Math.min(deviceLimit, 10); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.text = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(currentValue, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
concurrentSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listener für Device Limit Änderungen
|
||||
document.getElementById('deviceLimit').addEventListener('change', updateConcurrentSessionsOptions);
|
||||
|
||||
// Setze heutiges Datum als Standard
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
@@ -510,5 +547,51 @@ function showCustomerTypeIndicator(type) {
|
||||
function hideCustomerTypeIndicator() {
|
||||
document.getElementById('customerTypeIndicator').classList.add('d-none');
|
||||
}
|
||||
|
||||
// Validation for concurrent sessions vs device limit
|
||||
document.getElementById('deviceLimit').addEventListener('change', validateSessionLimit);
|
||||
document.getElementById('concurrentSessions').addEventListener('change', validateSessionLimit);
|
||||
|
||||
function validateSessionLimit() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSessions = parseInt(document.getElementById('concurrentSessions').value);
|
||||
const sessionsSelect = document.getElementById('concurrentSessions');
|
||||
|
||||
// Update options to not exceed device limit
|
||||
sessionsSelect.innerHTML = '';
|
||||
for (let i = 1; i <= Math.min(10, deviceLimit); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.textContent = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(concurrentSessions, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
sessionsSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Show warning if adjusted
|
||||
if (concurrentSessions > deviceLimit) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-warning border-0 position-fixed bottom-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Gleichzeitige Sessions wurden auf ${deviceLimit} angepasst (Max. Gerätelimit).
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
setTimeout(() => toast.remove(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize validation on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateSessionLimit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -367,6 +367,7 @@ function updateLicenseView(customerId, licenses) {
|
||||
<th>Gültig bis</th>
|
||||
<th>Status</th>
|
||||
<th>Server Status</th>
|
||||
<th>Sessions</th>
|
||||
<th>Ressourcen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -448,6 +449,11 @@ function updateLicenseView(customerId, licenses) {
|
||||
<td>${license.valid_until || '-'}</td>
|
||||
<td><span class="badge ${statusClass}">${license.status}</span></td>
|
||||
<td>${serverStatusHtml}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
${license.active_sessions || 0}/${license.max_concurrent_sessions || 1}
|
||||
</span>
|
||||
</td>
|
||||
<td class="resources-cell">
|
||||
${resourcesHtml || '<span class="text-muted">-</span>'}
|
||||
</td>
|
||||
@@ -1086,7 +1092,7 @@ function showDeviceManagement(licenseId) {
|
||||
content += `
|
||||
<tr>
|
||||
<td>${device.device_name}</td>
|
||||
<td><small class="text-muted">${device.hardware_id.substring(0, 12)}...</small></td>
|
||||
<td><small class="text-muted">${device.hardware_fingerprint.substring(0, 12)}...</small></td>
|
||||
<td>${device.operating_system}</td>
|
||||
<td>${device.first_seen}</td>
|
||||
<td>${device.last_seen}</td>
|
||||
|
||||
@@ -156,6 +156,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Utilization -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-broadcast"></i> Session-Auslastung
|
||||
<span class="badge bg-info float-end">{{ stats.session_stats.total_active_sessions or 0 }} aktive Sessions</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-primary">{{ stats.session_stats.total_active_sessions or 0 }}</h3>
|
||||
<p class="text-muted mb-0">Aktive Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-success">{{ stats.session_stats.total_max_sessions or 0 }}</h3>
|
||||
<p class="text-muted mb-0">Maximale Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-warning">{{ stats.session_stats.utilization_percent or 0 }}%</h3>
|
||||
<p class="text-muted mb-0">Auslastung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if stats.session_stats.licenses_at_limit > 0 %}
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>{{ stats.session_stats.licenses_at_limit }}</strong> Lizenz(en) haben ihr Session-Limit erreicht
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Health Status -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
|
||||
@@ -65,6 +65,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">Max. gleichzeitige Sessions</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% set device_limit = license.get('device_limit', 3) %}
|
||||
{% set upper_limit = device_limit + 1 if device_limit < 11 else 11 %}
|
||||
{% for i in range(1, upper_limit) %}
|
||||
<option value="{{ i }}" {% if license.get('max_concurrent_sessions', 1) == i %}selected{% endif %}>
|
||||
{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">Wie viele Geräte können gleichzeitig online sein</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert {% if license.is_fake %}alert-warning{% else %}alert-success{% endif %} mt-3" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Status:</strong>
|
||||
|
||||
@@ -153,6 +153,19 @@
|
||||
Anzahl der Geräte, auf denen die Lizenz gleichzeitig aktiviert sein kann.
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">
|
||||
Max. gleichzeitige Sessions
|
||||
</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% for i in range(1, 11) %}
|
||||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Wie viele Geräte können gleichzeitig online sein. Muss kleiner oder gleich dem Gerätelimit sein.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -574,5 +587,51 @@ function showCustomerTypeIndicator(type) {
|
||||
function hideCustomerTypeIndicator() {
|
||||
document.getElementById('customerTypeIndicator').classList.add('d-none');
|
||||
}
|
||||
|
||||
// Validation for concurrent sessions vs device limit
|
||||
document.getElementById('deviceLimit').addEventListener('change', validateSessionLimit);
|
||||
document.getElementById('concurrentSessions').addEventListener('change', validateSessionLimit);
|
||||
|
||||
function validateSessionLimit() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSessions = parseInt(document.getElementById('concurrentSessions').value);
|
||||
const sessionsSelect = document.getElementById('concurrentSessions');
|
||||
|
||||
// Update options to not exceed device limit
|
||||
sessionsSelect.innerHTML = '';
|
||||
for (let i = 1; i <= Math.min(10, deviceLimit); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.textContent = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(concurrentSessions, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
sessionsSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Show warning if adjusted
|
||||
if (concurrentSessions > deviceLimit) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-warning border-0 position-fixed bottom-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Gleichzeitige Sessions wurden auf ${deviceLimit} angepasst (Max. Gerätelimit).
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
setTimeout(() => toast.remove(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize validation on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateSessionLimit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
{{ sortable_header('Gültig von', 'valid_from', sort, order) }}
|
||||
{{ sortable_header('Gültig bis', 'valid_until', sort, order) }}
|
||||
{{ sortable_header('Status', 'status', sort, order) }}
|
||||
<th>Sessions</th>
|
||||
{{ sortable_header('Aktiv', 'active', sort, order) }}
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -225,6 +226,11 @@
|
||||
<span class="status-aktiv">✅ Aktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
{{ license.active_sessions or 0 }}/{{ license.max_concurrent_sessions or 1 }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-check form-switch form-switch-custom">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
|
||||
@@ -384,7 +384,7 @@
|
||||
`<div class="d-flex justify-content-between border-bottom py-2">
|
||||
<span>
|
||||
<code>${v.license_key}</code> |
|
||||
<span class="text-muted">${v.hardware_id}</span>
|
||||
<span class="text-muted">${v.hardware_fingerprint}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="badge bg-secondary">${v.ip_address}</span>
|
||||
|
||||
127
v2_adminpanel/templates/monitoring/device_limits.html
Normale Datei
127
v2_adminpanel/templates/monitoring/device_limits.html
Normale Datei
@@ -0,0 +1,127 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Device Limit Monitoring{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<h1 class="h3 mb-4">Device Limit Monitoring</h1>
|
||||
|
||||
<!-- Overall Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total Devices</h5>
|
||||
<p class="card-text display-6">{{ stats.total_devices }}</p>
|
||||
<small class="text-muted">of {{ stats.total_device_limit }} allowed</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Overall Usage</h5>
|
||||
<p class="card-text display-6">{{ stats.usage_percent }}%</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar {% if stats.usage_percent > 90 %}bg-danger{% elif stats.usage_percent > 70 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
role="progressbar" style="width: {{ stats.usage_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Active (24h)</h5>
|
||||
<p class="card-text display-6">{{ stats.active_24h }}</p>
|
||||
<small class="text-muted">devices seen today</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Active (7d)</h5>
|
||||
<p class="card-text display-6">{{ stats.active_7d }}</p>
|
||||
<small class="text-muted">devices seen this week</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
{% if warnings %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Device Limit Warnings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>License Key</th>
|
||||
<th>Customer</th>
|
||||
<th>Devices</th>
|
||||
<th>Limit</th>
|
||||
<th>Usage</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for warning in warnings %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('license_bp.license_detail', license_id=warning.license_id) }}">
|
||||
{{ warning.license_key }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ warning.customer_name }}<br>
|
||||
<small class="text-muted">{{ warning.customer_email }}</small>
|
||||
</td>
|
||||
<td>{{ warning.device_count }}</td>
|
||||
<td>{{ warning.device_limit }}</td>
|
||||
<td>
|
||||
<div class="progress" style="width: 100px;">
|
||||
<div class="progress-bar {% if warning.status == 'exceeded' %}bg-danger{% else %}bg-warning{% endif %}"
|
||||
role="progressbar" style="width: {{ warning.usage_percent }}%">
|
||||
{{ warning.usage_percent }}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if warning.status == 'exceeded' %}
|
||||
<span class="badge bg-danger">Exceeded</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Warning</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('license_bp.license_detail', license_id=warning.license_id) }}#devices"
|
||||
class="btn btn-sm btn-primary">
|
||||
View Devices
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i> All licenses are within their device limits.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-refresh every 30 seconds
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -327,7 +327,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="geo-info">
|
||||
<i class="bi bi-geo-alt"></i> {{ session.ip_address }}
|
||||
<div><small>Hardware: {{ session.hardware_id[:12] }}...</small></div>
|
||||
<div><small>Hardware: {{ session.hardware_fingerprint[:12] }}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
@@ -591,7 +591,7 @@
|
||||
<div class="col-md-3">
|
||||
<div class="geo-info">
|
||||
<i class="bi bi-geo-alt"></i> ${session.ip_address}
|
||||
<div><small>Hardware: ${session.hardware_id.substring(0, 12)}...</small></div>
|
||||
<div><small>Hardware: ${session.hardware_fingerprint.substring(0, 12)}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
@@ -619,7 +619,7 @@
|
||||
`<div class="d-flex justify-content-between border-bottom py-2">
|
||||
<span>
|
||||
<code>${v.license_key}</code> |
|
||||
<span class="text-muted">${v.hardware_id}</span>
|
||||
<span class="text-muted">${v.hardware_fingerprint}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="badge bg-secondary">${v.ip_address}</span>
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
</span>
|
||||
{{ event.description }}
|
||||
{% else %}
|
||||
Validierung von {{ event.ip_address }} • Gerät: {{ event.hardware_id[:8] }}...
|
||||
Validierung von {{ event.ip_address }} • Gerät: {{ event.hardware_fingerprint[:8] }}...
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
56
v2_adminpanel/test_device_count.py
Normale Datei
56
v2_adminpanel/test_device_count.py
Normale Datei
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to verify device count is properly returned from API"""
|
||||
|
||||
import sys
|
||||
sys.path.append('/opt/v2-Docker/v2_adminpanel')
|
||||
|
||||
from db import get_connection
|
||||
|
||||
def test_device_count():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get the license we're interested in
|
||||
license_key = 'AF-F-202506-WY2J-ZZB9-7LZD'
|
||||
|
||||
# Get license ID
|
||||
cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,))
|
||||
result = cur.fetchone()
|
||||
if not result:
|
||||
print(f"License {license_key} not found")
|
||||
return
|
||||
|
||||
license_id = result[0]
|
||||
print(f"License ID: {license_id}")
|
||||
|
||||
# Count active devices from activations table
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM activations
|
||||
WHERE license_id = %s AND is_active = true
|
||||
""", (license_id,))
|
||||
active_device_count = cur.fetchone()[0]
|
||||
|
||||
print(f"Active devices count: {active_device_count}")
|
||||
|
||||
# Show all activations for this license
|
||||
cur.execute("""
|
||||
SELECT machine_id, device_name, is_active, first_seen, last_seen
|
||||
FROM activations
|
||||
WHERE license_id = %s
|
||||
ORDER BY first_seen DESC
|
||||
""", (license_id,))
|
||||
|
||||
print("\nAll activations for this license:")
|
||||
for row in cur.fetchall():
|
||||
status = "ACTIVE" if row[2] else "INACTIVE"
|
||||
print(f" - {row[1]} ({row[0][:12]}...) - {status} - First: {row[3]} - Last: {row[4]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_device_count()
|
||||
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
@@ -3,6 +3,8 @@ import time
|
||||
import gzip
|
||||
import logging
|
||||
import subprocess
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -10,7 +12,7 @@ from cryptography.fernet import Fernet
|
||||
from db import get_db_connection, get_db_cursor
|
||||
from config import BACKUP_DIR, DATABASE_CONFIG, EMAIL_ENABLED, BACKUP_ENCRYPTION_KEY
|
||||
from utils.audit import log_audit
|
||||
from utils.github_backup import GitHubBackupManager, create_server_backup as create_server_backup_impl
|
||||
from utils.github_backup import GitHubBackupManager, create_server_backup_impl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -125,6 +127,10 @@ def create_backup(backup_type="manual", created_by=None):
|
||||
send_backup_notification(True, filename, filesize, duration)
|
||||
|
||||
logger.info(f"Backup successfully created: {filename}")
|
||||
|
||||
# Apply retention policy - keep only last 5 local backups
|
||||
cleanup_old_backups("database", 5)
|
||||
|
||||
return True, filename
|
||||
|
||||
except Exception as e:
|
||||
@@ -224,6 +230,69 @@ def send_backup_notification(success, filename, filesize=None, duration=None, er
|
||||
logger.info(f"Email notification prepared: Backup {'successful' if success else 'failed'}")
|
||||
|
||||
|
||||
def cleanup_old_backups(backup_type="database", keep_count=5):
|
||||
"""Clean up old local backups, keeping only the most recent ones"""
|
||||
try:
|
||||
# Get list of local backups from database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT id, filename, filepath
|
||||
FROM backup_history
|
||||
WHERE backup_type = %s
|
||||
AND status = 'success'
|
||||
AND local_deleted = FALSE
|
||||
AND filepath IS NOT NULL
|
||||
ORDER BY created_at DESC
|
||||
""", (backup_type,))
|
||||
backups = cur.fetchall()
|
||||
|
||||
if len(backups) <= keep_count:
|
||||
logger.info(f"No cleanup needed. Found {len(backups)} {backup_type} backups, keeping {keep_count}")
|
||||
return
|
||||
|
||||
# Delete old backups
|
||||
backups_to_delete = backups[keep_count:]
|
||||
deleted_count = 0
|
||||
|
||||
for backup_id, filename, filepath in backups_to_delete:
|
||||
try:
|
||||
# Check if file exists
|
||||
if filepath and os.path.exists(filepath):
|
||||
os.unlink(filepath)
|
||||
logger.info(f"Deleted old backup: {filename}")
|
||||
|
||||
# Update database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
UPDATE backup_history
|
||||
SET local_deleted = TRUE
|
||||
WHERE id = %s
|
||||
""", (backup_id,))
|
||||
conn.commit()
|
||||
|
||||
deleted_count += 1
|
||||
else:
|
||||
# File doesn't exist, just update database
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
UPDATE backup_history
|
||||
SET local_deleted = TRUE
|
||||
WHERE id = %s
|
||||
""", (backup_id,))
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete backup {filename}: {e}")
|
||||
|
||||
logger.info(f"Backup cleanup completed. Deleted {deleted_count} old {backup_type} backups")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Backup cleanup failed: {e}")
|
||||
|
||||
|
||||
def create_backup_with_github(backup_type="manual", created_by=None, push_to_github=True, delete_local=True):
|
||||
"""Create backup and optionally push to GitHub"""
|
||||
# Create the backup
|
||||
@@ -242,7 +311,8 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
db_backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
target_path = db_backup_dir / filename
|
||||
filepath.rename(target_path)
|
||||
# Use shutil.move instead of rename to handle cross-device links
|
||||
shutil.move(str(filepath), str(target_path))
|
||||
|
||||
# Push to GitHub
|
||||
github = GitHubBackupManager()
|
||||
@@ -269,8 +339,8 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
conn.commit()
|
||||
else:
|
||||
logger.error(f"Failed to push to GitHub: {git_result}")
|
||||
# Move file back
|
||||
target_path.rename(filepath)
|
||||
# Move file back using shutil
|
||||
shutil.move(str(target_path), str(filepath))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"GitHub upload error: {str(e)}")
|
||||
@@ -279,6 +349,63 @@ def create_backup_with_github(backup_type="manual", created_by=None, push_to_git
|
||||
return True, filename
|
||||
|
||||
|
||||
def create_container_server_backup_info(created_by="system"):
|
||||
"""Create a server info backup in container environment"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"server_backup_info_{timestamp}.json"
|
||||
filepath = Path("/app/backups") / filename
|
||||
|
||||
# Collect server info available in container
|
||||
server_info = {
|
||||
"backup_type": "server_info",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": created_by,
|
||||
"container_environment": True,
|
||||
"message": "Full server backups nur über Host-System möglich. Dies ist eine Info-Datei.",
|
||||
"docker_compose": None,
|
||||
"env_vars": {},
|
||||
"existing_backups": []
|
||||
}
|
||||
|
||||
# Try to read docker-compose if mounted
|
||||
if os.path.exists("/app/docker-compose.yaml"):
|
||||
try:
|
||||
with open("/app/docker-compose.yaml", 'r') as f:
|
||||
server_info["docker_compose"] = f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to read env vars (without secrets)
|
||||
if os.path.exists("/app/.env"):
|
||||
try:
|
||||
with open("/app/.env", 'r') as f:
|
||||
for line in f:
|
||||
if '=' in line and not any(secret in line.upper() for secret in ['PASSWORD', 'SECRET', 'KEY']):
|
||||
key, value = line.strip().split('=', 1)
|
||||
server_info["env_vars"][key] = "***" if len(value) > 20 else value
|
||||
except:
|
||||
pass
|
||||
|
||||
# List existing server backups
|
||||
if os.path.exists("/app/server-backups"):
|
||||
try:
|
||||
server_info["existing_backups"] = sorted(os.listdir("/app/server-backups"))[-10:]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Write info file
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(server_info, f, indent=2)
|
||||
|
||||
logger.info(f"Container server backup info created: {filename}")
|
||||
return True, str(filepath)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Container server backup info failed: {e}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def create_server_backup(created_by=None, push_to_github=True, delete_local=True):
|
||||
"""Create full server backup"""
|
||||
start_time = time.time()
|
||||
@@ -296,7 +423,7 @@ def create_server_backup(created_by=None, push_to_github=True, delete_local=True
|
||||
conn.commit()
|
||||
|
||||
try:
|
||||
# Create server backup
|
||||
# Create server backup - always use full backup now
|
||||
success, result = create_server_backup_impl(created_by)
|
||||
|
||||
if not success:
|
||||
@@ -353,6 +480,9 @@ def create_server_backup(created_by=None, push_to_github=True, delete_local=True
|
||||
log_audit('BACKUP', 'server', backup_id,
|
||||
additional_info=f"Server backup created: {filename} ({filesize} bytes)")
|
||||
|
||||
# Apply retention policy - keep only last 5 local server backups
|
||||
cleanup_old_backups("server", 5)
|
||||
|
||||
return True, filename
|
||||
|
||||
except Exception as e:
|
||||
|
||||
97
v2_adminpanel/utils/device_monitoring.py
Normale Datei
97
v2_adminpanel/utils/device_monitoring.py
Normale Datei
@@ -0,0 +1,97 @@
|
||||
"""Device limit monitoring utilities"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from db import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def check_device_limits():
|
||||
"""Check all licenses for device limit violations"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Find licenses approaching or exceeding device limits
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
l.device_limit,
|
||||
COUNT(DISTINCT dr.id) as device_count,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email
|
||||
FROM licenses l
|
||||
LEFT JOIN device_registrations dr ON dr.license_id = l.id AND dr.is_active = true
|
||||
LEFT JOIN customers c ON c.id = l.customer_id
|
||||
WHERE l.is_active = true
|
||||
GROUP BY l.id, l.license_key, l.device_limit, c.name, c.email
|
||||
HAVING COUNT(DISTINCT dr.id) >= l.device_limit * 0.8 -- 80% threshold
|
||||
ORDER BY (COUNT(DISTINCT dr.id)::float / l.device_limit) DESC
|
||||
""")
|
||||
|
||||
warnings = []
|
||||
for row in cur.fetchall():
|
||||
license_id, license_key, device_limit, device_count, customer_name, customer_email = row
|
||||
usage_percent = (device_count / device_limit) * 100 if device_limit > 0 else 0
|
||||
|
||||
warning = {
|
||||
'license_id': license_id,
|
||||
'license_key': license_key,
|
||||
'customer_name': customer_name or 'Unknown',
|
||||
'customer_email': customer_email or 'No email',
|
||||
'device_limit': device_limit,
|
||||
'device_count': device_count,
|
||||
'usage_percent': round(usage_percent, 1),
|
||||
'status': 'exceeded' if device_count > device_limit else 'warning'
|
||||
}
|
||||
warnings.append(warning)
|
||||
|
||||
if device_count > device_limit:
|
||||
logger.warning(f"License {license_key} exceeded device limit: {device_count}/{device_limit}")
|
||||
|
||||
return warnings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking device limits: {e}")
|
||||
return []
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
def get_device_usage_stats():
|
||||
"""Get overall device usage statistics"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get overall stats
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(DISTINCT l.id) as total_licenses,
|
||||
COUNT(DISTINCT dr.id) as total_devices,
|
||||
SUM(l.device_limit) as total_device_limit,
|
||||
COUNT(DISTINCT CASE WHEN dr.last_seen_at > NOW() - INTERVAL '24 hours' THEN dr.id END) as active_24h,
|
||||
COUNT(DISTINCT CASE WHEN dr.last_seen_at > NOW() - INTERVAL '7 days' THEN dr.id END) as active_7d
|
||||
FROM licenses l
|
||||
LEFT JOIN device_registrations dr ON dr.license_id = l.id AND dr.is_active = true
|
||||
WHERE l.is_active = true
|
||||
""")
|
||||
|
||||
stats = cur.fetchone()
|
||||
|
||||
return {
|
||||
'total_licenses': stats[0] or 0,
|
||||
'total_devices': stats[1] or 0,
|
||||
'total_device_limit': stats[2] or 0,
|
||||
'usage_percent': round((stats[1] / stats[2] * 100) if stats[2] > 0 else 0, 1),
|
||||
'active_24h': stats[3] or 0,
|
||||
'active_7d': stats[4] or 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting device usage stats: {e}")
|
||||
return {}
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
@@ -170,6 +170,7 @@ def create_batch_export(licenses):
|
||||
'Email': license.get('customer_email', ''),
|
||||
'Lizenztyp': license.get('license_type', 'full').upper(),
|
||||
'Geräte-Limit': license.get('device_limit', 3),
|
||||
'Max. Sessions': license.get('max_concurrent_sessions', 1),
|
||||
'Gültig von': format_datetime_for_export(license.get('valid_from')),
|
||||
'Gültig bis': format_datetime_for_export(license.get('valid_until')),
|
||||
'Status': 'Aktiv' if license.get('is_active', True) else 'Inaktiv',
|
||||
|
||||
@@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class GitHubBackupManager:
|
||||
def __init__(self):
|
||||
# Always use full path now that container has access
|
||||
self.repo_path = Path("/opt/v2-Docker")
|
||||
self.backup_remote = "backup"
|
||||
self.git_lfs_path = "/home/root/.local/bin"
|
||||
@@ -141,8 +142,8 @@ class GitHubBackupManager:
|
||||
logger.error(f"Download from GitHub error: {str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
def create_server_backup(created_by="system"):
|
||||
"""Create a full server backup"""
|
||||
def create_server_backup_impl(created_by="system"):
|
||||
"""Create a full server backup - implementation"""
|
||||
try:
|
||||
# Run the backup script
|
||||
backup_script = Path("/opt/v2-Docker/create_full_backup.sh")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren