Add latest changes

Dieser Commit ist enthalten in:
2025-07-03 20:38:33 +00:00
Ursprung 63f3d92724
Commit 6f6cde65db
129 geänderte Dateien mit 3998 neuen und 1199 gelöschten Zeilen

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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
Datei anzeigen

@@ -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

Datei anzeigen

@@ -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 $$

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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():

Datei anzeigen

@@ -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}%'))

Datei anzeigen

@@ -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()
))

Datei anzeigen

@@ -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']),

Datei anzeigen

@@ -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,

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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)

Datei anzeigen

@@ -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'
""")

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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',

Datei anzeigen

@@ -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;

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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">

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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"

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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:

Datei anzeigen

@@ -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()

Datei anzeigen

@@ -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',

Datei anzeigen

@@ -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")