Rollback
Dieser Commit ist enthalten in:
Binäre Datei nicht angezeigt.
4461
v2_adminpanel/app.py.backup_before_blueprint_migration
Normale Datei
4461
v2_adminpanel/app.py.backup_before_blueprint_migration
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
4475
v2_adminpanel/app.py.backup_before_cleanup_20250616_223830
Normale Datei
4475
v2_adminpanel/app.py.backup_before_cleanup_20250616_223830
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
4475
v2_adminpanel/app.py.backup_before_cleanup_20250616_223919
Normale Datei
4475
v2_adminpanel/app.py.backup_before_cleanup_20250616_223919
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
156
v2_adminpanel/cleanup_commented_routes.py
Normale Datei
156
v2_adminpanel/cleanup_commented_routes.py
Normale Datei
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cleanup-Skript zum sicheren Entfernen auskommentierter Routes aus app.py
|
||||
Entfernt alle auskommentierten @app.route Blöcke nach der Blueprint-Migration
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def cleanup_commented_routes(file_path):
|
||||
"""Entfernt auskommentierte Route-Blöcke aus app.py"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
print(f"📄 Datei geladen: {len(lines)} Zeilen")
|
||||
|
||||
cleaned_lines = []
|
||||
in_commented_block = False
|
||||
block_start_line = -1
|
||||
removed_blocks = []
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Prüfe ob eine auskommentierte Route beginnt
|
||||
if re.match(r'^# @app\.route\(', line.strip()):
|
||||
in_commented_block = True
|
||||
block_start_line = i + 1 # 1-basiert für Anzeige
|
||||
block_lines = [line]
|
||||
i += 1
|
||||
|
||||
# Sammle den ganzen auskommentierten Block
|
||||
while i < len(lines):
|
||||
current_line = lines[i]
|
||||
|
||||
# Block endet wenn:
|
||||
# 1. Eine neue Funktion/Route beginnt (nicht auskommentiert)
|
||||
# 2. Eine Leerzeile nach mehreren kommentierten Zeilen
|
||||
# 3. Eine neue auskommentierte Route beginnt
|
||||
|
||||
if re.match(r'^@', current_line.strip()) or \
|
||||
re.match(r'^def\s+\w+', current_line.strip()) or \
|
||||
re.match(r'^class\s+\w+', current_line.strip()):
|
||||
# Nicht-kommentierter Code gefunden, Block endet
|
||||
break
|
||||
|
||||
if re.match(r'^# @app\.route\(', current_line.strip()) and len(block_lines) > 1:
|
||||
# Neue auskommentierte Route, vorheriger Block endet
|
||||
break
|
||||
|
||||
# Prüfe ob die Zeile zum kommentierten Block gehört
|
||||
if current_line.strip() == '' and i + 1 < len(lines):
|
||||
# Schaue voraus
|
||||
next_line = lines[i + 1]
|
||||
if not (next_line.strip().startswith('#') or next_line.strip() == ''):
|
||||
# Leerzeile gefolgt von nicht-kommentiertem Code
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
break
|
||||
|
||||
if current_line.strip().startswith('#') or current_line.strip() == '':
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
else:
|
||||
# Nicht-kommentierte Zeile gefunden, Block endet
|
||||
break
|
||||
|
||||
# Entferne trailing Leerzeilen vom Block
|
||||
while block_lines and block_lines[-1].strip() == '':
|
||||
block_lines.pop()
|
||||
i -= 1
|
||||
|
||||
removed_blocks.append({
|
||||
'start_line': block_start_line,
|
||||
'end_line': block_start_line + len(block_lines) - 1,
|
||||
'lines': len(block_lines),
|
||||
'route': extract_route_info(block_lines)
|
||||
})
|
||||
|
||||
in_commented_block = False
|
||||
|
||||
# Füge eine Leerzeile ein wenn nötig (um Abstand zu wahren)
|
||||
if cleaned_lines and cleaned_lines[-1].strip() != '' and \
|
||||
i < len(lines) and lines[i].strip() != '':
|
||||
cleaned_lines.append('\n')
|
||||
|
||||
else:
|
||||
cleaned_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return cleaned_lines, removed_blocks
|
||||
|
||||
def extract_route_info(block_lines):
|
||||
"""Extrahiert Route-Information aus dem kommentierten Block"""
|
||||
for line in block_lines:
|
||||
match = re.search(r'# @app\.route\("([^"]+)"', line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "Unknown"
|
||||
|
||||
def main():
|
||||
file_path = 'app.py'
|
||||
backup_path = f'app.py.backup_before_cleanup_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||
|
||||
print("🧹 Starte Cleanup der auskommentierten Routes...\n")
|
||||
|
||||
# Backup erstellen
|
||||
print(f"💾 Erstelle Backup: {backup_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
# Cleanup durchführen
|
||||
cleaned_lines, removed_blocks = cleanup_commented_routes(file_path)
|
||||
|
||||
# Statistiken anzeigen
|
||||
print(f"\n📊 Cleanup-Statistiken:")
|
||||
print(f" Entfernte Blöcke: {len(removed_blocks)}")
|
||||
print(f" Entfernte Zeilen: {sum(block['lines'] for block in removed_blocks)}")
|
||||
print(f" Neue Dateigröße: {len(cleaned_lines)} Zeilen")
|
||||
|
||||
print(f"\n🗑️ Entfernte Routes:")
|
||||
for block in removed_blocks:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
|
||||
# Bestätigung abfragen
|
||||
print(f"\n⚠️ Diese Operation wird {sum(block['lines'] for block in removed_blocks)} Zeilen entfernen!")
|
||||
response = input("Fortfahren? (ja/nein): ")
|
||||
|
||||
if response.lower() in ['ja', 'j', 'yes', 'y']:
|
||||
# Bereinigte Datei schreiben
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(cleaned_lines)
|
||||
print(f"\n✅ Cleanup abgeschlossen! Backup gespeichert als: {backup_path}")
|
||||
|
||||
# Größenvergleich
|
||||
import os
|
||||
old_size = os.path.getsize(backup_path)
|
||||
new_size = os.path.getsize(file_path)
|
||||
reduction = old_size - new_size
|
||||
reduction_percent = (reduction / old_size) * 100
|
||||
|
||||
print(f"\n📉 Dateigrößen-Reduktion:")
|
||||
print(f" Vorher: {old_size:,} Bytes")
|
||||
print(f" Nachher: {new_size:,} Bytes")
|
||||
print(f" Reduziert um: {reduction:,} Bytes ({reduction_percent:.1f}%)")
|
||||
|
||||
else:
|
||||
print("\n❌ Cleanup abgebrochen. Keine Änderungen vorgenommen.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
153
v2_adminpanel/cleanup_commented_routes_auto.py
Normale Datei
153
v2_adminpanel/cleanup_commented_routes_auto.py
Normale Datei
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Automatisches Cleanup-Skript zum Entfernen auskommentierter Routes aus app.py
|
||||
Führt das Cleanup ohne Benutzerinteraktion durch
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
def cleanup_commented_routes(file_path):
|
||||
"""Entfernt auskommentierte Route-Blöcke aus app.py"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
print(f"📄 Datei geladen: {len(lines)} Zeilen")
|
||||
|
||||
cleaned_lines = []
|
||||
removed_blocks = []
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Prüfe ob eine auskommentierte Route beginnt
|
||||
if re.match(r'^# @app\.route\(', line.strip()):
|
||||
block_start_line = i + 1 # 1-basiert für Anzeige
|
||||
block_lines = [line]
|
||||
i += 1
|
||||
|
||||
# Sammle den ganzen auskommentierten Block
|
||||
while i < len(lines):
|
||||
current_line = lines[i]
|
||||
|
||||
# Block endet wenn:
|
||||
# 1. Eine neue Funktion/Route beginnt (nicht auskommentiert)
|
||||
# 2. Eine neue auskommentierte Route beginnt
|
||||
# 3. Nicht-kommentierter Code gefunden wird
|
||||
|
||||
if re.match(r'^@', current_line.strip()) or \
|
||||
re.match(r'^def\s+\w+', current_line.strip()) or \
|
||||
re.match(r'^class\s+\w+', current_line.strip()):
|
||||
# Nicht-kommentierter Code gefunden, Block endet
|
||||
break
|
||||
|
||||
if re.match(r'^# @app\.route\(', current_line.strip()) and len(block_lines) > 1:
|
||||
# Neue auskommentierte Route, vorheriger Block endet
|
||||
break
|
||||
|
||||
# Prüfe ob die Zeile zum kommentierten Block gehört
|
||||
if current_line.strip() == '' and i + 1 < len(lines):
|
||||
# Schaue voraus
|
||||
next_line = lines[i + 1]
|
||||
if not (next_line.strip().startswith('#') or next_line.strip() == ''):
|
||||
# Leerzeile gefolgt von nicht-kommentiertem Code
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
break
|
||||
|
||||
if current_line.strip().startswith('#') or current_line.strip() == '':
|
||||
block_lines.append(current_line)
|
||||
i += 1
|
||||
else:
|
||||
# Nicht-kommentierte Zeile gefunden, Block endet
|
||||
break
|
||||
|
||||
# Entferne trailing Leerzeilen vom Block
|
||||
while block_lines and block_lines[-1].strip() == '':
|
||||
block_lines.pop()
|
||||
i -= 1
|
||||
|
||||
removed_blocks.append({
|
||||
'start_line': block_start_line,
|
||||
'end_line': block_start_line + len(block_lines) - 1,
|
||||
'lines': len(block_lines),
|
||||
'route': extract_route_info(block_lines)
|
||||
})
|
||||
|
||||
# Füge eine Leerzeile ein wenn nötig (um Abstand zu wahren)
|
||||
if cleaned_lines and cleaned_lines[-1].strip() != '' and \
|
||||
i < len(lines) and lines[i].strip() != '':
|
||||
cleaned_lines.append('\n')
|
||||
|
||||
else:
|
||||
cleaned_lines.append(line)
|
||||
i += 1
|
||||
|
||||
return cleaned_lines, removed_blocks
|
||||
|
||||
def extract_route_info(block_lines):
|
||||
"""Extrahiert Route-Information aus dem kommentierten Block"""
|
||||
for line in block_lines:
|
||||
match = re.search(r'# @app\.route\("([^"]+)"', line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "Unknown"
|
||||
|
||||
def main():
|
||||
file_path = 'app.py'
|
||||
backup_path = f'app.py.backup_before_cleanup_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||
|
||||
print("🧹 Starte automatisches Cleanup der auskommentierten Routes...\n")
|
||||
|
||||
# Backup erstellen
|
||||
print(f"💾 Erstelle Backup: {backup_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
# Cleanup durchführen
|
||||
cleaned_lines, removed_blocks = cleanup_commented_routes(file_path)
|
||||
|
||||
# Statistiken anzeigen
|
||||
print(f"\n📊 Cleanup-Statistiken:")
|
||||
print(f" Entfernte Blöcke: {len(removed_blocks)}")
|
||||
print(f" Entfernte Zeilen: {sum(block['lines'] for block in removed_blocks)}")
|
||||
print(f" Neue Dateigröße: {len(cleaned_lines)} Zeilen")
|
||||
|
||||
if len(removed_blocks) > 10:
|
||||
print(f"\n🗑️ Erste 10 entfernte Routes:")
|
||||
for block in removed_blocks[:10]:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
print(f" ... und {len(removed_blocks) - 10} weitere Routes")
|
||||
else:
|
||||
print(f"\n🗑️ Entfernte Routes:")
|
||||
for block in removed_blocks:
|
||||
print(f" Zeilen {block['start_line']}-{block['end_line']}: {block['route']} ({block['lines']} Zeilen)")
|
||||
|
||||
# Bereinigte Datei schreiben
|
||||
print(f"\n✍️ Schreibe bereinigte Datei...")
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(cleaned_lines)
|
||||
|
||||
# Größenvergleich
|
||||
old_size = os.path.getsize(backup_path)
|
||||
new_size = os.path.getsize(file_path)
|
||||
reduction = old_size - new_size
|
||||
reduction_percent = (reduction / old_size) * 100
|
||||
|
||||
print(f"\n📉 Dateigrößen-Reduktion:")
|
||||
print(f" Vorher: {old_size:,} Bytes")
|
||||
print(f" Nachher: {new_size:,} Bytes")
|
||||
print(f" Reduziert um: {reduction:,} Bytes ({reduction_percent:.1f}%)")
|
||||
|
||||
print(f"\n✅ Cleanup erfolgreich abgeschlossen!")
|
||||
print(f" Backup: {backup_path}")
|
||||
print(f" Rollback möglich mit: cp {backup_path} app.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to comment out routes that have been moved to blueprints
|
||||
"""
|
||||
|
||||
# Routes that have been moved to auth_routes.py
|
||||
auth_routes = [
|
||||
("@app.route(\"/login\"", "def login():", 138, 251), # login route
|
||||
("@app.route(\"/logout\")", "def logout():", 252, 263), # logout route
|
||||
("@app.route(\"/verify-2fa\"", "def verify_2fa():", 264, 342), # verify-2fa route
|
||||
("@app.route(\"/profile\")", "def profile():", 343, 352), # profile route
|
||||
("@app.route(\"/profile/change-password\"", "def change_password():", 353, 390), # change-password route
|
||||
("@app.route(\"/profile/setup-2fa\")", "def setup_2fa():", 391, 410), # setup-2fa route
|
||||
("@app.route(\"/profile/enable-2fa\"", "def enable_2fa():", 411, 448), # enable-2fa route
|
||||
("@app.route(\"/profile/disable-2fa\"", "def disable_2fa():", 449, 475), # disable-2fa route
|
||||
("@app.route(\"/heartbeat\"", "def heartbeat():", 476, 489), # heartbeat route
|
||||
]
|
||||
|
||||
# Routes that have been moved to admin_routes.py
|
||||
admin_routes = [
|
||||
("@app.route(\"/\")", "def dashboard():", 647, 870), # dashboard route
|
||||
("@app.route(\"/audit\")", "def audit_log():", 2772, 2866), # audit route
|
||||
("@app.route(\"/backups\")", "def backups():", 2866, 2901), # backups route
|
||||
("@app.route(\"/backup/create\"", "def create_backup_route():", 2901, 2919), # backup/create route
|
||||
("@app.route(\"/backup/restore/<int:backup_id>\"", "def restore_backup_route(backup_id):", 2919, 2938), # backup/restore route
|
||||
("@app.route(\"/backup/download/<int:backup_id>\")", "def download_backup(backup_id):", 2938, 2970), # backup/download route
|
||||
("@app.route(\"/backup/delete/<int:backup_id>\"", "def delete_backup(backup_id):", 2970, 3026), # backup/delete route
|
||||
("@app.route(\"/security/blocked-ips\")", "def blocked_ips():", 3026, 3067), # security/blocked-ips route
|
||||
("@app.route(\"/security/unblock-ip\"", "def unblock_ip():", 3067, 3093), # security/unblock-ip route
|
||||
("@app.route(\"/security/clear-attempts\"", "def clear_attempts():", 3093, 3119), # security/clear-attempts route
|
||||
]
|
||||
|
||||
print("This script would comment out the following routes:")
|
||||
print("\nAuth routes:")
|
||||
for route in auth_routes:
|
||||
print(f" - {route[0]} (lines {route[2]}-{route[3]})")
|
||||
|
||||
print("\nAdmin routes:")
|
||||
for route in admin_routes:
|
||||
print(f" - {route[0]} (lines {route[2]}-{route[3]})")
|
||||
|
||||
print("\nNote: Manual verification and adjustment of line numbers is recommended before running the actual commenting.")
|
||||
@@ -61,4 +61,10 @@ ADMIN_USERS = {
|
||||
SCHEDULER_CONFIG = {
|
||||
'backup_hour': 3,
|
||||
'backup_minute': 0
|
||||
}
|
||||
|
||||
# Logging Configuration
|
||||
LOGGING_CONFIG = {
|
||||
'level': 'INFO',
|
||||
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
}
|
||||
@@ -28,13 +28,22 @@ CREATE TABLE IF NOT EXISTS licenses (
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
license_id INTEGER REFERENCES licenses(id),
|
||||
license_key VARCHAR(60), -- Denormalized for performance
|
||||
session_id TEXT UNIQUE NOT NULL,
|
||||
username VARCHAR(50),
|
||||
computer_name VARCHAR(100),
|
||||
hardware_id VARCHAR(100),
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
app_version VARCHAR(20),
|
||||
login_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for started_at
|
||||
last_activity TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Alias for last_heartbeat
|
||||
logout_time TIMESTAMP WITH TIME ZONE, -- Alias for ended_at
|
||||
started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TIMESTAMP WITH TIME ZONE,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
active BOOLEAN DEFAULT TRUE -- Alias for is_active
|
||||
);
|
||||
|
||||
-- Audit-Log-Tabelle für Änderungsprotokolle
|
||||
@@ -280,3 +289,65 @@ BEGIN
|
||||
CREATE INDEX idx_resource_pools_is_test ON resource_pools(is_test);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Migration: Add missing columns to sessions table
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Add license_key column
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'license_key') THEN
|
||||
ALTER TABLE sessions ADD COLUMN license_key VARCHAR(60);
|
||||
END IF;
|
||||
|
||||
-- Add username column
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'username') THEN
|
||||
ALTER TABLE sessions ADD COLUMN username VARCHAR(50);
|
||||
END IF;
|
||||
|
||||
-- Add computer_name column
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'computer_name') THEN
|
||||
ALTER TABLE sessions ADD COLUMN computer_name VARCHAR(100);
|
||||
END IF;
|
||||
|
||||
-- Add hardware_id column
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'hardware_id') THEN
|
||||
ALTER TABLE sessions ADD COLUMN hardware_id VARCHAR(100);
|
||||
END IF;
|
||||
|
||||
-- Add app_version column
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'app_version') THEN
|
||||
ALTER TABLE sessions ADD COLUMN app_version VARCHAR(20);
|
||||
END IF;
|
||||
|
||||
-- Add login_time as alias for started_at
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'login_time') THEN
|
||||
ALTER TABLE sessions ADD COLUMN login_time TIMESTAMP WITH TIME ZONE;
|
||||
UPDATE sessions SET login_time = started_at;
|
||||
END IF;
|
||||
|
||||
-- Add last_activity as alias for last_heartbeat
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'last_activity') THEN
|
||||
ALTER TABLE sessions ADD COLUMN last_activity TIMESTAMP WITH TIME ZONE;
|
||||
UPDATE sessions SET last_activity = last_heartbeat;
|
||||
END IF;
|
||||
|
||||
-- Add logout_time as alias for ended_at
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'logout_time') THEN
|
||||
ALTER TABLE sessions ADD COLUMN logout_time TIMESTAMP WITH TIME ZONE;
|
||||
UPDATE sessions SET logout_time = ended_at;
|
||||
END IF;
|
||||
|
||||
-- Add active as alias for is_active
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sessions' AND column_name = 'active') THEN
|
||||
ALTER TABLE sessions ADD COLUMN active BOOLEAN DEFAULT TRUE;
|
||||
UPDATE sessions SET active = is_active;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Temporary models file - will be expanded in Phase 3
|
||||
from db import execute_query
|
||||
from db import execute_query, get_db_connection, get_db_cursor
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_user_by_username(username):
|
||||
@@ -26,4 +29,134 @@ def get_user_by_username(username):
|
||||
'last_password_change': result[7],
|
||||
'failed_2fa_attempts': result[8]
|
||||
}
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def get_licenses(show_test=False):
|
||||
"""Get all licenses from database"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
if show_test:
|
||||
cur.execute("""
|
||||
SELECT l.*, c.name as customer_name
|
||||
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
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.is_test = false
|
||||
ORDER BY l.created_at DESC
|
||||
""")
|
||||
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
licenses = []
|
||||
for row in cur.fetchall():
|
||||
license_dict = dict(zip(columns, row))
|
||||
licenses.append(license_dict)
|
||||
return licenses
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching licenses: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def get_license_by_id(license_id):
|
||||
"""Get a specific license by ID"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT l.*, c.name as customer_name
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.id = %s
|
||||
""", (license_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
return dict(zip(columns, row))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching license {license_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def get_customers():
|
||||
"""Get all customers from database"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT c.*,
|
||||
COUNT(DISTINCT l.id) as license_count,
|
||||
COUNT(DISTINCT CASE WHEN l.is_active THEN l.id END) as active_licenses
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name
|
||||
""")
|
||||
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
customers = []
|
||||
for row in cur.fetchall():
|
||||
customer_dict = dict(zip(columns, row))
|
||||
customers.append(customer_dict)
|
||||
return customers
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching customers: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def get_customer_by_id(customer_id):
|
||||
"""Get a specific customer by ID"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT c.*,
|
||||
COUNT(DISTINCT l.id) as license_count,
|
||||
COUNT(DISTINCT CASE WHEN l.is_active THEN l.id END) as active_licenses
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE c.id = %s
|
||||
GROUP BY c.id
|
||||
""", (customer_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
return dict(zip(columns, row))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching customer {customer_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def get_active_sessions():
|
||||
"""Get all active sessions"""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with get_db_cursor(conn) as cur:
|
||||
cur.execute("""
|
||||
SELECT s.*, l.license_key, c.name as customer_name
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE s.is_active = true
|
||||
ORDER BY s.start_time DESC
|
||||
""")
|
||||
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
sessions = []
|
||||
for row in cur.fetchall():
|
||||
session_dict = dict(zip(columns, row))
|
||||
sessions.append(session_dict)
|
||||
return sessions
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching active sessions: {str(e)}")
|
||||
return []
|
||||
@@ -25,7 +25,7 @@ def dashboard():
|
||||
try:
|
||||
# Hole Statistiken
|
||||
# Anzahl aktiver Lizenzen
|
||||
cur.execute("SELECT COUNT(*) FROM licenses WHERE active = true")
|
||||
cur.execute("SELECT COUNT(*) FROM licenses WHERE is_active = true")
|
||||
active_licenses = cur.fetchone()[0]
|
||||
|
||||
# Anzahl Kunden
|
||||
@@ -33,7 +33,7 @@ def dashboard():
|
||||
total_customers = cur.fetchone()[0]
|
||||
|
||||
# Anzahl aktiver Sessions
|
||||
cur.execute("SELECT COUNT(*) FROM sessions WHERE active = true")
|
||||
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = true")
|
||||
active_sessions = cur.fetchone()[0]
|
||||
|
||||
# Top 10 Lizenzen nach Nutzung (letzte 30 Tage)
|
||||
@@ -46,7 +46,7 @@ def dashboard():
|
||||
MAX(s.last_activity) as last_activity
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
LEFT JOIN sessions s ON l.license_key = s.license_key
|
||||
LEFT JOIN sessions s ON l.id = s.license_id
|
||||
AND s.login_time >= CURRENT_TIMESTAMP - INTERVAL '30 days'
|
||||
GROUP BY l.license_key, c.name
|
||||
ORDER BY session_count DESC
|
||||
|
||||
906
v2_adminpanel/routes/api_routes.py
Normale Datei
906
v2_adminpanel/routes/api_routes.py
Normale Datei
@@ -0,0 +1,906 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from utils.network import get_client_ip
|
||||
from utils.license import generate_license_key
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_license_by_id
|
||||
|
||||
# Create Blueprint
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_license(license_id):
|
||||
"""Toggle license active status"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get current status
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
new_status = not license_data['active']
|
||||
|
||||
# Update status
|
||||
cur.execute("UPDATE licenses SET active = %s WHERE id = %s", (new_status, license_id))
|
||||
conn.commit()
|
||||
|
||||
# Log change
|
||||
log_audit('TOGGLE', 'license', license_id,
|
||||
old_values={'active': license_data['active']},
|
||||
new_values={'active': new_status})
|
||||
|
||||
return jsonify({'success': True, 'active': new_status})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Umschalten der Lizenz'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-activate", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_activate_licenses():
|
||||
"""Aktiviere mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = true
|
||||
WHERE id = ANY(%s) AND active = false
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
updated_ids = [row[0] for row in cur.fetchall()]
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_ACTIVATE', 'license', license_id,
|
||||
new_values={'active': True})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'updated_count': len(updated_ids)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Aktivieren: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Aktivieren der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-deactivate", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_deactivate_licenses():
|
||||
"""Deaktiviere mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Update all selected licenses
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET active = false
|
||||
WHERE id = ANY(%s) AND active = true
|
||||
RETURNING id
|
||||
""", (license_ids,))
|
||||
|
||||
updated_ids = [row[0] for row in cur.fetchall()]
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
for license_id in updated_ids:
|
||||
log_audit('BULK_DEACTIVATE', 'license', license_id,
|
||||
new_values={'active': False})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'updated_count': len(updated_ids)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Deaktivieren: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Deaktivieren der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/devices")
|
||||
@login_required
|
||||
def get_license_devices(license_id):
|
||||
"""Hole alle Geräte einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Hole registrierte Geräte
|
||||
cur.execute("""
|
||||
SELECT
|
||||
dr.id,
|
||||
dr.device_id,
|
||||
dr.device_name,
|
||||
dr.device_type,
|
||||
dr.registration_date,
|
||||
dr.last_seen,
|
||||
dr.is_active,
|
||||
(SELECT COUNT(*) FROM sessions s
|
||||
WHERE s.license_key = dr.license_key
|
||||
AND s.device_id = dr.device_id
|
||||
AND s.active = true) as active_sessions
|
||||
FROM device_registrations dr
|
||||
WHERE dr.license_key = %s
|
||||
ORDER BY dr.registration_date DESC
|
||||
""", (license_data['license_key'],))
|
||||
|
||||
devices = []
|
||||
for row in cur.fetchall():
|
||||
devices.append({
|
||||
'id': row[0],
|
||||
'device_id': row[1],
|
||||
'device_name': row[2],
|
||||
'device_type': row[3],
|
||||
'registration_date': row[4].isoformat() if row[4] else None,
|
||||
'last_seen': row[5].isoformat() if row[5] else None,
|
||||
'is_active': row[6],
|
||||
'active_sessions': row[7]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'license_key': license_data['license_key'],
|
||||
'device_limit': license_data['device_limit'],
|
||||
'devices': devices,
|
||||
'device_count': len(devices)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Abrufen der Geräte'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/register-device", methods=["POST"])
|
||||
@login_required
|
||||
def register_device(license_id):
|
||||
"""Registriere ein neues Gerät für eine Lizenz"""
|
||||
data = request.get_json()
|
||||
|
||||
device_id = data.get('device_id')
|
||||
device_name = data.get('device_name')
|
||||
device_type = data.get('device_type', 'unknown')
|
||||
|
||||
if not device_id or not device_name:
|
||||
return jsonify({'error': 'Geräte-ID und Name erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Prüfe Gerätelimit
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM device_registrations
|
||||
WHERE license_key = %s AND is_active = true
|
||||
""", (license_data['license_key'],))
|
||||
|
||||
active_device_count = cur.fetchone()[0]
|
||||
|
||||
if active_device_count >= license_data['device_limit']:
|
||||
return jsonify({'error': 'Gerätelimit erreicht'}), 400
|
||||
|
||||
# Prüfe ob Gerät bereits registriert
|
||||
cur.execute("""
|
||||
SELECT id, is_active FROM device_registrations
|
||||
WHERE license_key = %s AND device_id = %s
|
||||
""", (license_data['license_key'], device_id))
|
||||
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
if existing[1]: # is_active
|
||||
return jsonify({'error': 'Gerät bereits registriert'}), 400
|
||||
else:
|
||||
# Reaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = true, last_seen = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""", (existing[0],))
|
||||
else:
|
||||
# Registriere neues Gerät
|
||||
cur.execute("""
|
||||
INSERT INTO device_registrations
|
||||
(license_key, device_id, device_name, device_type, is_active)
|
||||
VALUES (%s, %s, %s, %s, true)
|
||||
""", (license_data['license_key'], device_id, device_name, device_type))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('DEVICE_REGISTER', 'license', license_id,
|
||||
additional_info=f"Gerät {device_name} ({device_id}) registriert")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Registrieren des Geräts: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Registrieren des Geräts'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
|
||||
@login_required
|
||||
def deactivate_device(license_id, device_id):
|
||||
"""Deaktiviere ein Gerät einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Prüfe ob Gerät zur Lizenz gehört
|
||||
cur.execute("""
|
||||
SELECT dr.device_name, dr.device_id, l.license_key
|
||||
FROM device_registrations dr
|
||||
JOIN licenses l ON dr.license_key = l.license_key
|
||||
WHERE dr.id = %s AND l.id = %s
|
||||
""", (device_id, license_id))
|
||||
|
||||
device = cur.fetchone()
|
||||
if not device:
|
||||
return jsonify({'error': 'Gerät nicht gefunden'}), 404
|
||||
|
||||
# Deaktiviere Gerät
|
||||
cur.execute("""
|
||||
UPDATE device_registrations
|
||||
SET is_active = false
|
||||
WHERE id = %s
|
||||
""", (device_id,))
|
||||
|
||||
# Beende aktive Sessions
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET active = false, logout_time = CURRENT_TIMESTAMP
|
||||
WHERE license_key = %s AND device_id = %s AND active = true
|
||||
""", (device[2], device[1]))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('DEVICE_DEACTIVATE', 'license', license_id,
|
||||
additional_info=f"Gerät {device[0]} ({device[1]}) deaktiviert")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Deaktivieren des Geräts'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/licenses/bulk-delete", methods=["POST"])
|
||||
@login_required
|
||||
def bulk_delete_licenses():
|
||||
"""Lösche mehrere Lizenzen gleichzeitig"""
|
||||
data = request.get_json()
|
||||
license_ids = data.get('license_ids', [])
|
||||
|
||||
if not license_ids:
|
||||
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
deleted_count = 0
|
||||
|
||||
for license_id in license_ids:
|
||||
# Hole Lizenz-Info für Audit
|
||||
cur.execute("SELECT license_key FROM licenses WHERE id = %s", (license_id,))
|
||||
result = cur.fetchone()
|
||||
|
||||
if result:
|
||||
license_key = result[0]
|
||||
|
||||
# Lösche Sessions
|
||||
cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_key,))
|
||||
|
||||
# Lösche Geräte-Registrierungen
|
||||
cur.execute("DELETE FROM device_registrations WHERE license_key = %s", (license_key,))
|
||||
|
||||
# Lösche Lizenz
|
||||
cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
|
||||
|
||||
# Audit-Log
|
||||
log_audit('BULK_DELETE', 'license', license_id,
|
||||
old_values={'license_key': license_key})
|
||||
|
||||
deleted_count += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'deleted_count': deleted_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bulk-Löschen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Löschen der Lizenzen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/quick-edit", methods=['POST'])
|
||||
@login_required
|
||||
def quick_edit_license(license_id):
|
||||
"""Schnellbearbeitung einer Lizenz"""
|
||||
data = request.get_json()
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole aktuelle Lizenz für Vergleich
|
||||
current_license = get_license_by_id(license_id)
|
||||
if not current_license:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Update nur die übergebenen Felder
|
||||
updates = []
|
||||
params = []
|
||||
old_values = {}
|
||||
new_values = {}
|
||||
|
||||
if 'device_limit' in data:
|
||||
updates.append("device_limit = %s")
|
||||
params.append(int(data['device_limit']))
|
||||
old_values['device_limit'] = current_license['device_limit']
|
||||
new_values['device_limit'] = int(data['device_limit'])
|
||||
|
||||
if 'valid_until' in data:
|
||||
updates.append("valid_until = %s")
|
||||
params.append(data['valid_until'])
|
||||
old_values['valid_until'] = str(current_license['valid_until'])
|
||||
new_values['valid_until'] = data['valid_until']
|
||||
|
||||
if 'active' in data:
|
||||
updates.append("active = %s")
|
||||
params.append(bool(data['active']))
|
||||
old_values['active'] = current_license['active']
|
||||
new_values['active'] = bool(data['active'])
|
||||
|
||||
if not updates:
|
||||
return jsonify({'error': 'Keine Änderungen angegeben'}), 400
|
||||
|
||||
# Führe Update aus
|
||||
params.append(license_id)
|
||||
cur.execute(f"""
|
||||
UPDATE licenses
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = %s
|
||||
""", params)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('QUICK_EDIT', 'license', license_id,
|
||||
old_values=old_values,
|
||||
new_values=new_values)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler bei Schnellbearbeitung: {str(e)}")
|
||||
return jsonify({'error': 'Fehler bei der Bearbeitung'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/license/<int:license_id>/resources")
|
||||
@login_required
|
||||
def get_license_resources(license_id):
|
||||
"""Hole alle Ressourcen einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Lizenz-Info
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
# Hole zugewiesene Ressourcen
|
||||
cur.execute("""
|
||||
SELECT
|
||||
rp.id,
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
rp.is_test,
|
||||
rp.status_changed_at,
|
||||
lr.assigned_at,
|
||||
lr.assigned_by
|
||||
FROM resource_pools rp
|
||||
JOIN license_resources lr ON rp.id = lr.resource_id
|
||||
WHERE lr.license_id = %s
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""", (license_id,))
|
||||
|
||||
resources = []
|
||||
for row in cur.fetchall():
|
||||
resources.append({
|
||||
'id': row[0],
|
||||
'type': row[1],
|
||||
'value': row[2],
|
||||
'is_test': row[3],
|
||||
'status_changed_at': row[4].isoformat() if row[4] else None,
|
||||
'assigned_at': row[5].isoformat() if row[5] else None,
|
||||
'assigned_by': row[6]
|
||||
})
|
||||
|
||||
# Gruppiere nach Typ
|
||||
grouped = {}
|
||||
for resource in resources:
|
||||
res_type = resource['type']
|
||||
if res_type not in grouped:
|
||||
grouped[res_type] = []
|
||||
grouped[res_type].append(resource)
|
||||
|
||||
return jsonify({
|
||||
'license_key': license_data['license_key'],
|
||||
'resources': resources,
|
||||
'grouped': grouped,
|
||||
'total_count': len(resources)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Abrufen der Ressourcen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Abrufen der Ressourcen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/resources/allocate", methods=['POST'])
|
||||
@login_required
|
||||
def allocate_resources():
|
||||
"""Weise Ressourcen einer Lizenz zu"""
|
||||
data = request.get_json()
|
||||
|
||||
license_id = data.get('license_id')
|
||||
resource_ids = data.get('resource_ids', [])
|
||||
|
||||
if not license_id or not resource_ids:
|
||||
return jsonify({'error': 'Lizenz-ID und Ressourcen erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Prüfe Lizenz
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
||||
|
||||
allocated_count = 0
|
||||
errors = []
|
||||
|
||||
for resource_id in resource_ids:
|
||||
try:
|
||||
# Prüfe ob Ressource verfügbar ist
|
||||
cur.execute("""
|
||||
SELECT resource_value, status, is_test
|
||||
FROM resource_pools
|
||||
WHERE id = %s
|
||||
""", (resource_id,))
|
||||
|
||||
resource = cur.fetchone()
|
||||
if not resource:
|
||||
errors.append(f"Ressource {resource_id} nicht gefunden")
|
||||
continue
|
||||
|
||||
if resource[1] != 'available':
|
||||
errors.append(f"Ressource {resource[0]} ist nicht verfügbar")
|
||||
continue
|
||||
|
||||
# Prüfe Test/Produktion Kompatibilität
|
||||
if resource[2] != license_data['is_test']:
|
||||
errors.append(f"Ressource {resource[0]} ist {'Test' if resource[2] else 'Produktion'}, Lizenz ist {'Test' if license_data['is_test'] else 'Produktion'}")
|
||||
continue
|
||||
|
||||
# Weise Ressource zu
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'allocated',
|
||||
allocated_to_license = %s,
|
||||
status_changed_at = CURRENT_TIMESTAMP,
|
||||
status_changed_by = %s
|
||||
WHERE id = %s
|
||||
""", (license_id, session['username'], resource_id))
|
||||
|
||||
# Erstelle Verknüpfung
|
||||
cur.execute("""
|
||||
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (license_id, resource_id, session['username']))
|
||||
|
||||
# History-Eintrag
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'allocated', %s, %s)
|
||||
""", (resource_id, license_id, session['username'], get_client_ip()))
|
||||
|
||||
allocated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Fehler bei Ressource {resource_id}: {str(e)}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
if allocated_count > 0:
|
||||
log_audit('RESOURCE_ALLOCATE', 'license', license_id,
|
||||
additional_info=f"{allocated_count} Ressourcen zugewiesen")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'allocated_count': allocated_count,
|
||||
'errors': errors
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Zuweisen der Ressourcen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Zuweisen der Ressourcen'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/resources/check-availability", methods=['GET'])
|
||||
@login_required
|
||||
def check_resource_availability():
|
||||
"""Prüfe Verfügbarkeit von Ressourcen"""
|
||||
resource_type = request.args.get('type')
|
||||
count = int(request.args.get('count', 1))
|
||||
is_test = request.args.get('is_test', 'false') == 'true'
|
||||
|
||||
if not resource_type:
|
||||
return jsonify({'error': 'Ressourcen-Typ erforderlich'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Zähle verfügbare Ressourcen
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM resource_pools
|
||||
WHERE resource_type = %s
|
||||
AND status = 'available'
|
||||
AND is_test = %s
|
||||
""", (resource_type, is_test))
|
||||
|
||||
available_count = cur.fetchone()[0]
|
||||
|
||||
return jsonify({
|
||||
'resource_type': resource_type,
|
||||
'requested': count,
|
||||
'available': available_count,
|
||||
'sufficient': available_count >= count,
|
||||
'is_test': is_test
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Prüfen der Verfügbarkeit: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/global-search", methods=['GET'])
|
||||
@login_required
|
||||
def global_search():
|
||||
"""Globale Suche über alle Entitäten"""
|
||||
query = request.args.get('q', '').strip()
|
||||
|
||||
if not query or len(query) < 3:
|
||||
return jsonify({'error': 'Suchbegriff muss mindestens 3 Zeichen haben'}), 400
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {
|
||||
'licenses': [],
|
||||
'customers': [],
|
||||
'resources': [],
|
||||
'sessions': []
|
||||
}
|
||||
|
||||
try:
|
||||
# Suche in Lizenzen
|
||||
cur.execute("""
|
||||
SELECT id, license_key, customer_name, active
|
||||
FROM licenses
|
||||
WHERE license_key ILIKE %s
|
||||
OR customer_name ILIKE %s
|
||||
OR customer_email ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['licenses'].append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'customer_name': row[2],
|
||||
'active': row[3]
|
||||
})
|
||||
|
||||
# Suche in Kunden
|
||||
cur.execute("""
|
||||
SELECT id, name, email
|
||||
FROM customers
|
||||
WHERE name ILIKE %s OR email ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['customers'].append({
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'email': row[2]
|
||||
})
|
||||
|
||||
# Suche in Ressourcen
|
||||
cur.execute("""
|
||||
SELECT id, resource_type, resource_value, status
|
||||
FROM resource_pools
|
||||
WHERE resource_value ILIKE %s
|
||||
LIMIT 10
|
||||
""", (f'%{query}%',))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['resources'].append({
|
||||
'id': row[0],
|
||||
'type': row[1],
|
||||
'value': row[2],
|
||||
'status': row[3]
|
||||
})
|
||||
|
||||
# Suche in Sessions
|
||||
cur.execute("""
|
||||
SELECT id, license_key, username, device_id, active
|
||||
FROM sessions
|
||||
WHERE username ILIKE %s OR device_id ILIKE %s
|
||||
ORDER BY login_time DESC
|
||||
LIMIT 10
|
||||
""", (f'%{query}%', f'%{query}%'))
|
||||
|
||||
for row in cur.fetchall():
|
||||
results['sessions'].append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'username': row[2],
|
||||
'device_id': row[3],
|
||||
'active': row[4]
|
||||
})
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei der globalen Suche: {str(e)}")
|
||||
return jsonify({'error': 'Fehler bei der Suche'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@api_bp.route("/generate-license-key", methods=['POST'])
|
||||
@login_required
|
||||
def api_generate_key():
|
||||
"""API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
|
||||
try:
|
||||
# Lizenztyp aus Request holen (default: full)
|
||||
data = request.get_json() or {}
|
||||
license_type = data.get('type', 'full')
|
||||
|
||||
# Key generieren
|
||||
key = generate_license_key(license_type)
|
||||
|
||||
# Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Wiederhole bis eindeutiger Key gefunden
|
||||
attempts = 0
|
||||
while attempts < 10: # Max 10 Versuche
|
||||
cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
|
||||
if not cur.fetchone():
|
||||
break # Key ist eindeutig
|
||||
key = generate_license_key(license_type)
|
||||
attempts += 1
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Log für Audit
|
||||
log_audit('GENERATE_KEY', 'license',
|
||||
additional_info={'type': license_type, 'key': key})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'key': key,
|
||||
'type': license_type
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei Key-Generierung: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler bei der Key-Generierung'
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route("/customers", methods=['GET'])
|
||||
@login_required
|
||||
def api_customers():
|
||||
"""API Endpoint für die Kundensuche mit Select2"""
|
||||
try:
|
||||
# Suchparameter
|
||||
search = request.args.get('q', '').strip()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
customer_id = request.args.get('id', type=int)
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Einzelnen Kunden per ID abrufen
|
||||
if customer_id:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE c.id = %s
|
||||
GROUP BY c.id, c.name, c.email
|
||||
""", (customer_id,))
|
||||
|
||||
customer = cur.fetchone()
|
||||
results = []
|
||||
if customer:
|
||||
results.append({
|
||||
'id': customer[0],
|
||||
'text': f"{customer[1]} ({customer[2]})",
|
||||
'name': customer[1],
|
||||
'email': customer[2],
|
||||
'license_count': customer[3]
|
||||
})
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'results': results,
|
||||
'pagination': {'more': False}
|
||||
})
|
||||
|
||||
# SQL Query mit optionaler Suche
|
||||
elif search:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE LOWER(c.name) LIKE LOWER(%s)
|
||||
OR LOWER(c.email) LIKE LOWER(%s)
|
||||
GROUP BY c.id, c.name, c.email
|
||||
ORDER BY c.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT c.id, c.name, c.email,
|
||||
COUNT(l.id) as license_count
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
GROUP BY c.id, c.name, c.email
|
||||
ORDER BY c.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", (per_page, (page - 1) * per_page))
|
||||
|
||||
customers = cur.fetchall()
|
||||
|
||||
# Format für Select2
|
||||
results = []
|
||||
for customer in customers:
|
||||
results.append({
|
||||
'id': customer[0],
|
||||
'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
|
||||
'name': customer[1],
|
||||
'email': customer[2],
|
||||
'license_count': customer[3]
|
||||
})
|
||||
|
||||
# Gesamtanzahl für Pagination
|
||||
if search:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM customers
|
||||
WHERE LOWER(name) LIKE LOWER(%s)
|
||||
OR LOWER(email) LIKE LOWER(%s)
|
||||
""", (f'%{search}%', f'%{search}%'))
|
||||
else:
|
||||
cur.execute("SELECT COUNT(*) FROM customers")
|
||||
|
||||
total_count = cur.fetchone()[0]
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Select2 Response Format
|
||||
return jsonify({
|
||||
'results': results,
|
||||
'pagination': {
|
||||
'more': (page * per_page) < total_count
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler bei Kundensuche: {str(e)}")
|
||||
return jsonify({
|
||||
'results': [],
|
||||
'pagination': {'more': False},
|
||||
'error': str(e)
|
||||
}), 500
|
||||
377
v2_adminpanel/routes/batch_routes.py
Normale Datei
377
v2_adminpanel/routes/batch_routes.py
Normale Datei
@@ -0,0 +1,377 @@
|
||||
import os
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, send_file
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from utils.network import get_client_ip
|
||||
from utils.export import create_batch_export
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_customers
|
||||
|
||||
# Create Blueprint
|
||||
batch_bp = Blueprint('batch', __name__)
|
||||
|
||||
|
||||
def generate_license_key():
|
||||
"""Generiert einen zufälligen Lizenzschlüssel"""
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return '-'.join([''.join(secrets.choice(chars) for _ in range(4)) for _ in range(4)])
|
||||
|
||||
|
||||
@batch_bp.route("/batch", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def batch_create():
|
||||
"""Batch-Erstellung von Lizenzen"""
|
||||
customers = get_customers()
|
||||
|
||||
if request.method == "POST":
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Form data
|
||||
customer_id = int(request.form['customer_id'])
|
||||
license_type = request.form['license_type']
|
||||
count = int(request.form['count'])
|
||||
valid_from = request.form['valid_from']
|
||||
valid_until = request.form['valid_until']
|
||||
device_limit = int(request.form['device_limit'])
|
||||
is_test = 'is_test' in request.form
|
||||
|
||||
# Validierung
|
||||
if count < 1 or count > 100:
|
||||
flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
|
||||
return redirect(url_for('batch.batch_create'))
|
||||
|
||||
# Hole Kundendaten
|
||||
cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
|
||||
customer = cur.fetchone()
|
||||
if not customer:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('batch.batch_create'))
|
||||
|
||||
created_licenses = []
|
||||
|
||||
# Erstelle Lizenzen
|
||||
for i in range(count):
|
||||
license_key = generate_license_key()
|
||||
|
||||
# Prüfe ob Schlüssel bereits existiert
|
||||
while True:
|
||||
cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,))
|
||||
if not cur.fetchone():
|
||||
break
|
||||
license_key = generate_license_key()
|
||||
|
||||
# Erstelle Lizenz
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (
|
||||
license_key, customer_id, customer_name, customer_email,
|
||||
license_type, valid_from, valid_until, device_limit,
|
||||
is_test, created_at, created_by
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
license_key, customer_id, customer[0], customer[1],
|
||||
license_type, valid_from, valid_until, device_limit,
|
||||
is_test, datetime.now(), session['username']
|
||||
))
|
||||
|
||||
license_id = cur.fetchone()[0]
|
||||
created_licenses.append({
|
||||
'id': license_id,
|
||||
'license_key': license_key
|
||||
})
|
||||
|
||||
# Audit-Log
|
||||
log_audit('CREATE', 'license', license_id,
|
||||
new_values={
|
||||
'license_key': license_key,
|
||||
'customer_name': customer[0],
|
||||
'batch_creation': True
|
||||
})
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Speichere erstellte Lizenzen in Session für Export
|
||||
session['batch_created_licenses'] = created_licenses
|
||||
|
||||
flash(f'{count} Lizenzen erfolgreich erstellt!', 'success')
|
||||
|
||||
# Weiterleitung zum Export
|
||||
return redirect(url_for('batch.batch_export'))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler bei Batch-Erstellung: {str(e)}")
|
||||
flash('Fehler bei der Batch-Erstellung!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template("batch_create.html", customers=customers)
|
||||
|
||||
|
||||
@batch_bp.route("/batch/export")
|
||||
@login_required
|
||||
def batch_export():
|
||||
"""Exportiert die zuletzt erstellten Batch-Lizenzen"""
|
||||
created_licenses = session.get('batch_created_licenses', [])
|
||||
|
||||
if not created_licenses:
|
||||
flash('Keine Lizenzen zum Exportieren gefunden!', 'error')
|
||||
return redirect(url_for('batch.batch_create'))
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole vollständige Lizenzdaten
|
||||
license_ids = [l['id'] for l in created_licenses]
|
||||
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.license_key, l.customer_name, l.customer_email,
|
||||
l.license_type, l.valid_from, l.valid_until,
|
||||
l.device_limit, l.is_test, l.created_at
|
||||
FROM licenses l
|
||||
WHERE l.id = ANY(%s)
|
||||
ORDER BY l.id
|
||||
""", (license_ids,))
|
||||
|
||||
licenses = []
|
||||
for row in cur.fetchall():
|
||||
licenses.append({
|
||||
'license_key': row[0],
|
||||
'customer_name': row[1],
|
||||
'customer_email': row[2],
|
||||
'license_type': row[3],
|
||||
'valid_from': row[4],
|
||||
'valid_until': row[5],
|
||||
'device_limit': row[6],
|
||||
'is_test': row[7],
|
||||
'created_at': row[8]
|
||||
})
|
||||
|
||||
# Erstelle Excel-Export
|
||||
excel_file = create_batch_export(licenses)
|
||||
|
||||
# Lösche aus Session
|
||||
session.pop('batch_created_licenses', None)
|
||||
|
||||
# Sende Datei
|
||||
filename = f"batch_licenses_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
flash('Fehler beim Exportieren der Lizenzen!', 'error')
|
||||
return redirect(url_for('batch.batch_create'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@batch_bp.route("/batch/update", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def batch_update():
|
||||
"""Batch-Update von Lizenzen"""
|
||||
if request.method == "POST":
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Form data
|
||||
license_keys = request.form.get('license_keys', '').strip().split('\n')
|
||||
license_keys = [key.strip() for key in license_keys if key.strip()]
|
||||
|
||||
if not license_keys:
|
||||
flash('Keine Lizenzschlüssel angegeben!', 'error')
|
||||
return redirect(url_for('batch.batch_update'))
|
||||
|
||||
# Update-Parameter
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if 'update_valid_until' in request.form and request.form['valid_until']:
|
||||
updates.append("valid_until = %s")
|
||||
params.append(request.form['valid_until'])
|
||||
|
||||
if 'update_device_limit' in request.form and request.form['device_limit']:
|
||||
updates.append("device_limit = %s")
|
||||
params.append(int(request.form['device_limit']))
|
||||
|
||||
if 'update_active' in request.form:
|
||||
updates.append("active = %s")
|
||||
params.append('active' in request.form)
|
||||
|
||||
if not updates:
|
||||
flash('Keine Änderungen angegeben!', 'error')
|
||||
return redirect(url_for('batch.batch_update'))
|
||||
|
||||
# Führe Updates aus
|
||||
updated_count = 0
|
||||
not_found = []
|
||||
|
||||
for license_key in license_keys:
|
||||
# Prüfe ob Lizenz existiert
|
||||
cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,))
|
||||
result = cur.fetchone()
|
||||
|
||||
if not result:
|
||||
not_found.append(license_key)
|
||||
continue
|
||||
|
||||
license_id = result[0]
|
||||
|
||||
# Update ausführen
|
||||
update_params = params + [license_id]
|
||||
cur.execute(f"""
|
||||
UPDATE licenses
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = %s
|
||||
""", update_params)
|
||||
|
||||
# Audit-Log
|
||||
log_audit('BATCH_UPDATE', 'license', license_id,
|
||||
additional_info=f"Batch-Update: {', '.join(updates)}")
|
||||
|
||||
updated_count += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Feedback
|
||||
flash(f'{updated_count} Lizenzen erfolgreich aktualisiert!', 'success')
|
||||
|
||||
if not_found:
|
||||
flash(f'{len(not_found)} Lizenzen nicht gefunden: {", ".join(not_found[:5])}{"..." if len(not_found) > 5 else ""}', 'warning')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler bei Batch-Update: {str(e)}")
|
||||
flash('Fehler beim Batch-Update!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template("batch_update.html")
|
||||
|
||||
|
||||
@batch_bp.route("/batch/import", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def batch_import():
|
||||
"""Import von Lizenzen aus CSV/Excel"""
|
||||
if request.method == "POST":
|
||||
if 'file' not in request.files:
|
||||
flash('Keine Datei ausgewählt!', 'error')
|
||||
return redirect(url_for('batch.batch_import'))
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
flash('Keine Datei ausgewählt!', 'error')
|
||||
return redirect(url_for('batch.batch_import'))
|
||||
|
||||
# Verarbeite Datei
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
# Lese Datei
|
||||
if file.filename.endswith('.csv'):
|
||||
df = pd.read_csv(file)
|
||||
elif file.filename.endswith(('.xlsx', '.xls')):
|
||||
df = pd.read_excel(file)
|
||||
else:
|
||||
flash('Ungültiges Dateiformat! Nur CSV und Excel erlaubt.', 'error')
|
||||
return redirect(url_for('batch.batch_import'))
|
||||
|
||||
# Validiere Spalten
|
||||
required_columns = ['customer_email', 'license_type', 'valid_from', 'valid_until', 'device_limit']
|
||||
missing_columns = [col for col in required_columns if col not in df.columns]
|
||||
|
||||
if missing_columns:
|
||||
flash(f'Fehlende Spalten: {", ".join(missing_columns)}', 'error')
|
||||
return redirect(url_for('batch.batch_import'))
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
imported_count = 0
|
||||
errors = []
|
||||
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# Finde oder erstelle Kunde
|
||||
email = row['customer_email']
|
||||
cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
|
||||
customer = cur.fetchone()
|
||||
|
||||
if not customer:
|
||||
# Erstelle neuen Kunden
|
||||
name = row.get('customer_name', email.split('@')[0])
|
||||
cur.execute("""
|
||||
INSERT INTO customers (name, email, created_at)
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING id
|
||||
""", (name, email, datetime.now()))
|
||||
customer_id = cur.fetchone()[0]
|
||||
customer_name = name
|
||||
else:
|
||||
customer_id = customer[0]
|
||||
customer_name = customer[1]
|
||||
|
||||
# Generiere Lizenzschlüssel
|
||||
license_key = row.get('license_key', generate_license_key())
|
||||
|
||||
# Erstelle Lizenz
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (
|
||||
license_key, customer_id, customer_name, customer_email,
|
||||
license_type, valid_from, valid_until, device_limit,
|
||||
is_test, created_at, created_by
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
license_key, customer_id, customer_name, email,
|
||||
row['license_type'], row['valid_from'], row['valid_until'],
|
||||
int(row['device_limit']), row.get('is_test', False),
|
||||
datetime.now(), session['username']
|
||||
))
|
||||
|
||||
license_id = cur.fetchone()[0]
|
||||
imported_count += 1
|
||||
|
||||
# Audit-Log
|
||||
log_audit('IMPORT', 'license', license_id,
|
||||
additional_info=f"Importiert aus {file.filename}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Zeile {index + 2}: {str(e)}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Feedback
|
||||
flash(f'{imported_count} Lizenzen erfolgreich importiert!', 'success')
|
||||
|
||||
if errors:
|
||||
flash(f'{len(errors)} Fehler aufgetreten. Erste Fehler: {"; ".join(errors[:3])}', 'warning')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Import: {str(e)}")
|
||||
flash(f'Fehler beim Import: {str(e)}', 'error')
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template("batch_import.html")
|
||||
338
v2_adminpanel/routes/customer_routes.py
Normale Datei
338
v2_adminpanel/routes/customer_routes.py
Normale Datei
@@ -0,0 +1,338 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_customers, get_customer_by_id
|
||||
|
||||
# Create Blueprint
|
||||
customer_bp = Blueprint('customers', __name__)
|
||||
|
||||
|
||||
@customer_bp.route("/customers")
|
||||
@login_required
|
||||
def customers():
|
||||
customers_list = get_customers()
|
||||
return render_template("customers.html", customers=customers_list)
|
||||
|
||||
|
||||
@customer_bp.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_customer(customer_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
# Get current customer data for comparison
|
||||
current_customer = get_customer_by_id(customer_id)
|
||||
if not current_customer:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
# Update customer data
|
||||
new_values = {
|
||||
'name': request.form['name'],
|
||||
'email': request.form['email'],
|
||||
'phone': request.form.get('phone', ''),
|
||||
'address': request.form.get('address', ''),
|
||||
'notes': request.form.get('notes', '')
|
||||
}
|
||||
|
||||
cur.execute("""
|
||||
UPDATE customers
|
||||
SET name = %s, email = %s, phone = %s, address = %s, notes = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
new_values['name'],
|
||||
new_values['email'],
|
||||
new_values['phone'],
|
||||
new_values['address'],
|
||||
new_values['notes'],
|
||||
customer_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
log_audit('UPDATE', 'customer', customer_id,
|
||||
old_values={
|
||||
'name': current_customer['name'],
|
||||
'email': current_customer['email'],
|
||||
'phone': current_customer.get('phone', ''),
|
||||
'address': current_customer.get('address', ''),
|
||||
'notes': current_customer.get('notes', '')
|
||||
},
|
||||
new_values=new_values)
|
||||
|
||||
flash('Kunde erfolgreich aktualisiert!', 'success')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Aktualisieren des Kunden: {str(e)}")
|
||||
flash('Fehler beim Aktualisieren des Kunden!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# GET request
|
||||
customer_data = get_customer_by_id(customer_id)
|
||||
if not customer_data:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
return render_template("edit_customer.html", customer=customer_data)
|
||||
|
||||
|
||||
@customer_bp.route("/customer/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_customer():
|
||||
if request.method == "POST":
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Insert new customer
|
||||
name = request.form['name']
|
||||
email = request.form['email']
|
||||
phone = request.form.get('phone', '')
|
||||
address = request.form.get('address', '')
|
||||
notes = request.form.get('notes', '')
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO customers (name, email, phone, address, notes, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (name, email, phone, address, notes, datetime.now()))
|
||||
|
||||
customer_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
|
||||
# Log creation
|
||||
log_audit('CREATE', 'customer', customer_id,
|
||||
new_values={
|
||||
'name': name,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'address': address,
|
||||
'notes': notes
|
||||
})
|
||||
|
||||
flash(f'Kunde {name} erfolgreich erstellt!', 'success')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Erstellen des Kunden: {str(e)}")
|
||||
flash('Fehler beim Erstellen des Kunden!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template("create_customer.html")
|
||||
|
||||
|
||||
@customer_bp.route("/customer/delete/<int:customer_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_customer(customer_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get customer data before deletion
|
||||
customer_data = get_customer_by_id(customer_id)
|
||||
if not customer_data:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
# Check if customer has licenses
|
||||
cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
|
||||
license_count = cur.fetchone()[0]
|
||||
|
||||
if license_count > 0:
|
||||
flash(f'Kunde kann nicht gelöscht werden - hat noch {license_count} Lizenz(en)!', 'error')
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
# Delete the customer
|
||||
cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log deletion
|
||||
log_audit('DELETE', 'customer', customer_id,
|
||||
old_values={
|
||||
'name': customer_data['name'],
|
||||
'email': customer_data['email']
|
||||
})
|
||||
|
||||
flash(f'Kunde {customer_data["name"]} erfolgreich gelöscht!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Löschen des Kunden: {str(e)}")
|
||||
flash('Fehler beim Löschen des Kunden!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('customers.customers'))
|
||||
|
||||
|
||||
@customer_bp.route("/customers-licenses")
|
||||
@login_required
|
||||
def customers_licenses():
|
||||
"""Zeigt die Übersicht von Kunden und deren Lizenzen"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole alle Kunden mit ihren Lizenzen
|
||||
cur.execute("""
|
||||
SELECT
|
||||
c.id as customer_id,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email,
|
||||
c.created_at as customer_created,
|
||||
COUNT(l.id) as license_count,
|
||||
COUNT(CASE WHEN l.active = true THEN 1 END) as active_licenses,
|
||||
COUNT(CASE WHEN l.is_test = true THEN 1 END) as test_licenses,
|
||||
MAX(l.created_at) as last_license_created
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
GROUP BY c.id, c.name, c.email, c.created_at
|
||||
ORDER BY c.name
|
||||
""")
|
||||
|
||||
customers = []
|
||||
for row in cur.fetchall():
|
||||
customers.append({
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'email': row[2],
|
||||
'created_at': row[3],
|
||||
'license_count': row[4],
|
||||
'active_licenses': row[5],
|
||||
'test_licenses': row[6],
|
||||
'last_license_created': row[7]
|
||||
})
|
||||
|
||||
return render_template("customers_licenses.html", customers=customers)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Kunden-Lizenz-Übersicht: {str(e)}")
|
||||
flash('Fehler beim Laden der Daten!', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@customer_bp.route("/api/customer/<int:customer_id>/licenses")
|
||||
@login_required
|
||||
def api_customer_licenses(customer_id):
|
||||
"""API-Endpunkt für die Lizenzen eines Kunden"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Kundeninformationen
|
||||
customer = get_customer_by_id(customer_id)
|
||||
if not customer:
|
||||
return jsonify({'error': 'Kunde nicht gefunden'}), 404
|
||||
|
||||
# Hole alle Lizenzen des Kunden
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
l.license_type,
|
||||
l.active,
|
||||
l.is_test,
|
||||
l.valid_from,
|
||||
l.valid_until,
|
||||
l.device_limit,
|
||||
l.created_at,
|
||||
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.active = true) as active_sessions,
|
||||
(SELECT COUNT(DISTINCT device_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices,
|
||||
CASE
|
||||
WHEN l.valid_until < CURRENT_DATE THEN 'expired'
|
||||
WHEN l.active = false THEN 'inactive'
|
||||
ELSE 'active'
|
||||
END as status
|
||||
FROM licenses l
|
||||
WHERE l.customer_id = %s
|
||||
ORDER BY l.created_at DESC
|
||||
""", (customer_id,))
|
||||
|
||||
licenses = []
|
||||
for row in cur.fetchall():
|
||||
licenses.append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'license_type': row[2],
|
||||
'active': row[3],
|
||||
'is_test': row[4],
|
||||
'valid_from': row[5].strftime('%Y-%m-%d') if row[5] else None,
|
||||
'valid_until': row[6].strftime('%Y-%m-%d') if row[6] else None,
|
||||
'device_limit': row[7],
|
||||
'created_at': row[8].strftime('%Y-%m-%d %H:%M:%S') if row[8] else None,
|
||||
'active_sessions': row[9],
|
||||
'registered_devices': row[10],
|
||||
'status': row[11]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'customer': {
|
||||
'id': customer['id'],
|
||||
'name': customer['name'],
|
||||
'email': customer['email']
|
||||
},
|
||||
'licenses': licenses
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Kundenlizenzen: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Laden der Daten'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@customer_bp.route("/api/customer/<int:customer_id>/quick-stats")
|
||||
@login_required
|
||||
def api_customer_quick_stats(customer_id):
|
||||
"""Schnelle Statistiken für einen Kunden"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(l.id) as total_licenses,
|
||||
COUNT(CASE WHEN l.active = true THEN 1 END) as active_licenses,
|
||||
COUNT(CASE WHEN l.is_test = true THEN 1 END) as test_licenses,
|
||||
SUM(l.device_limit) as total_device_limit
|
||||
FROM licenses l
|
||||
WHERE l.customer_id = %s
|
||||
""", (customer_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
return jsonify({
|
||||
'total_licenses': row[0] or 0,
|
||||
'active_licenses': row[1] or 0,
|
||||
'test_licenses': row[2] or 0,
|
||||
'total_device_limit': row[3] or 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Kundenstatistiken: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Laden der Daten'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
364
v2_adminpanel/routes/export_routes.py
Normale Datei
364
v2_adminpanel/routes/export_routes.py
Normale Datei
@@ -0,0 +1,364 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, request, send_file
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.export import create_excel_export, prepare_audit_export_data
|
||||
from db import get_connection
|
||||
|
||||
# Create Blueprint
|
||||
export_bp = Blueprint('export', __name__, url_prefix='/export')
|
||||
|
||||
|
||||
@export_bp.route("/licenses")
|
||||
@login_required
|
||||
def export_licenses():
|
||||
"""Exportiert Lizenzen als Excel-Datei"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Filter aus Request
|
||||
show_test = request.args.get('show_test', 'false') == 'true'
|
||||
|
||||
# SQL Query mit optionalem Test-Filter
|
||||
if show_test:
|
||||
query = """
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email,
|
||||
l.license_type,
|
||||
l.valid_from,
|
||||
l.valid_until,
|
||||
l.active,
|
||||
l.device_limit,
|
||||
l.created_at,
|
||||
l.is_test,
|
||||
CASE
|
||||
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
|
||||
WHEN l.active = false THEN 'Deaktiviert'
|
||||
ELSE 'Aktiv'
|
||||
END as status,
|
||||
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.active = true) as active_sessions,
|
||||
(SELECT COUNT(DISTINCT device_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
ORDER BY l.created_at DESC
|
||||
"""
|
||||
else:
|
||||
query = """
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
c.email as customer_email,
|
||||
l.license_type,
|
||||
l.valid_from,
|
||||
l.valid_until,
|
||||
l.active,
|
||||
l.device_limit,
|
||||
l.created_at,
|
||||
l.is_test,
|
||||
CASE
|
||||
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
|
||||
WHEN l.active = false THEN 'Deaktiviert'
|
||||
ELSE 'Aktiv'
|
||||
END as status,
|
||||
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.active = true) as active_sessions,
|
||||
(SELECT COUNT(DISTINCT device_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
|
||||
FROM licenses l
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.is_test = false
|
||||
ORDER BY l.created_at DESC
|
||||
"""
|
||||
|
||||
cur.execute(query)
|
||||
|
||||
# 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', 'Test-Lizenz',
|
||||
'Status', 'Aktive Sessions', 'Registrierte Geräte']
|
||||
|
||||
for row in cur.fetchall():
|
||||
data.append(list(row))
|
||||
|
||||
# Excel-Datei erstellen
|
||||
excel_file = create_excel_export(data, columns, 'Lizenzen')
|
||||
|
||||
# Datei senden
|
||||
filename = f"lizenzen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
return "Fehler beim Exportieren der Lizenzen", 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@export_bp.route("/audit")
|
||||
@login_required
|
||||
def export_audit():
|
||||
"""Exportiert Audit-Logs als Excel-Datei"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Filter aus Request
|
||||
days = int(request.args.get('days', 30))
|
||||
action_filter = request.args.get('action', '')
|
||||
entity_type_filter = request.args.get('entity_type', '')
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = prepare_audit_export_data(days, action_filter, entity_type_filter)
|
||||
|
||||
# Excel-Datei erstellen
|
||||
columns = ['Zeitstempel', 'Benutzer', 'Aktion', 'Entität', 'Entität ID',
|
||||
'IP-Adresse', 'Alte Werte', 'Neue Werte', 'Zusatzinfo']
|
||||
|
||||
excel_file = create_excel_export(data, columns, 'Audit-Log')
|
||||
|
||||
# Datei senden
|
||||
filename = f"audit_log_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
return "Fehler beim Exportieren der Audit-Logs", 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@export_bp.route("/customers")
|
||||
@login_required
|
||||
def export_customers():
|
||||
"""Exportiert Kunden als Excel-Datei"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# SQL Query
|
||||
cur.execute("""
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.email,
|
||||
c.phone,
|
||||
c.address,
|
||||
c.created_at,
|
||||
c.is_test,
|
||||
COUNT(l.id) as license_count,
|
||||
COUNT(CASE WHEN l.active = true THEN 1 END) as active_licenses,
|
||||
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
GROUP BY c.id, c.name, c.email, c.phone, c.address, c.created_at, c.is_test
|
||||
ORDER BY c.name
|
||||
""")
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
columns = ['ID', 'Name', 'E-Mail', 'Telefon', 'Adresse', 'Erstellt am',
|
||||
'Test-Kunde', 'Anzahl Lizenzen', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
|
||||
|
||||
for row in cur.fetchall():
|
||||
data.append(list(row))
|
||||
|
||||
# Excel-Datei erstellen
|
||||
excel_file = create_excel_export(data, columns, 'Kunden')
|
||||
|
||||
# Datei senden
|
||||
filename = f"kunden_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
return "Fehler beim Exportieren der Kunden", 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@export_bp.route("/sessions")
|
||||
@login_required
|
||||
def export_sessions():
|
||||
"""Exportiert Sessions als Excel-Datei"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Filter aus Request
|
||||
days = int(request.args.get('days', 7))
|
||||
active_only = request.args.get('active_only', 'false') == 'true'
|
||||
|
||||
# SQL Query
|
||||
if active_only:
|
||||
query = """
|
||||
SELECT
|
||||
s.id,
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
s.username,
|
||||
s.device_id,
|
||||
s.login_time,
|
||||
s.logout_time,
|
||||
s.last_activity,
|
||||
s.active,
|
||||
l.license_type,
|
||||
l.is_test
|
||||
FROM sessions s
|
||||
LEFT JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.active = true
|
||||
ORDER BY s.login_time DESC
|
||||
"""
|
||||
cur.execute(query)
|
||||
else:
|
||||
query = """
|
||||
SELECT
|
||||
s.id,
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
s.username,
|
||||
s.device_id,
|
||||
s.login_time,
|
||||
s.logout_time,
|
||||
s.last_activity,
|
||||
s.active,
|
||||
l.license_type,
|
||||
l.is_test
|
||||
FROM sessions s
|
||||
LEFT JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.login_time >= CURRENT_TIMESTAMP - INTERVAL '%s days'
|
||||
ORDER BY s.login_time DESC
|
||||
"""
|
||||
cur.execute(query, (days,))
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'Benutzer', 'Geräte-ID',
|
||||
'Login-Zeit', 'Logout-Zeit', 'Letzte Aktivität', 'Aktiv',
|
||||
'Lizenztyp', 'Test-Lizenz']
|
||||
|
||||
for row in cur.fetchall():
|
||||
data.append(list(row))
|
||||
|
||||
# Excel-Datei erstellen
|
||||
excel_file = create_excel_export(data, columns, 'Sessions')
|
||||
|
||||
# Datei senden
|
||||
filename = f"sessions_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
return "Fehler beim Exportieren der Sessions", 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@export_bp.route("/resources")
|
||||
@login_required
|
||||
def export_resources():
|
||||
"""Exportiert Ressourcen als Excel-Datei"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Filter aus Request
|
||||
resource_type = request.args.get('type', 'all')
|
||||
status_filter = request.args.get('status', 'all')
|
||||
show_test = request.args.get('show_test', 'false') == 'true'
|
||||
|
||||
# SQL Query aufbauen
|
||||
query = """
|
||||
SELECT
|
||||
rp.id,
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
rp.status,
|
||||
rp.is_test,
|
||||
l.license_key,
|
||||
c.name as customer_name,
|
||||
rp.created_at,
|
||||
rp.created_by,
|
||||
rp.status_changed_at,
|
||||
rp.status_changed_by,
|
||||
rp.quarantine_reason
|
||||
FROM resource_pools rp
|
||||
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
|
||||
if resource_type != 'all':
|
||||
query += " AND rp.resource_type = %s"
|
||||
params.append(resource_type)
|
||||
|
||||
if status_filter != 'all':
|
||||
query += " AND rp.status = %s"
|
||||
params.append(status_filter)
|
||||
|
||||
if not show_test:
|
||||
query += " AND rp.is_test = false"
|
||||
|
||||
query += " ORDER BY rp.resource_type, rp.resource_value"
|
||||
|
||||
cur.execute(query, params)
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
columns = ['ID', 'Typ', 'Wert', 'Status', 'Test-Ressource', 'Lizenzschlüssel',
|
||||
'Kunde', 'Erstellt am', 'Erstellt von', 'Status geändert am',
|
||||
'Status geändert von', 'Quarantäne-Grund']
|
||||
|
||||
for row in cur.fetchall():
|
||||
data.append(list(row))
|
||||
|
||||
# Excel-Datei erstellen
|
||||
excel_file = create_excel_export(data, columns, 'Ressourcen')
|
||||
|
||||
# Datei senden
|
||||
filename = f"ressourcen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
excel_file,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Export: {str(e)}")
|
||||
return "Fehler beim Exportieren der Ressourcen", 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
374
v2_adminpanel/routes/license_routes.py
Normale Datei
374
v2_adminpanel/routes/license_routes.py
Normale Datei
@@ -0,0 +1,374 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from utils.network import get_client_ip
|
||||
from utils.license import validate_license_key
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_licenses, get_license_by_id
|
||||
|
||||
# Create Blueprint
|
||||
license_bp = Blueprint('licenses', __name__)
|
||||
|
||||
|
||||
@license_bp.route("/licenses")
|
||||
@login_required
|
||||
def licenses():
|
||||
show_test = request.args.get('show_test', 'false') == 'true'
|
||||
licenses_list = get_licenses(show_test=show_test)
|
||||
return render_template("licenses.html", licenses=licenses_list, show_test=show_test)
|
||||
|
||||
|
||||
@license_bp.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_license(license_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
# Get current license data for comparison
|
||||
current_license = get_license_by_id(license_id)
|
||||
if not current_license:
|
||||
flash('Lizenz nicht gefunden!', 'error')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
|
||||
# Update license data
|
||||
new_values = {
|
||||
'customer_name': request.form['customer_name'],
|
||||
'customer_email': request.form['customer_email'],
|
||||
'valid_from': request.form['valid_from'],
|
||||
'valid_until': request.form['valid_until'],
|
||||
'device_limit': int(request.form['device_limit']),
|
||||
'active': 'active' in request.form
|
||||
}
|
||||
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET customer_name = %s, customer_email = %s, valid_from = %s,
|
||||
valid_until = %s, device_limit = %s, active = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
new_values['customer_name'],
|
||||
new_values['customer_email'],
|
||||
new_values['valid_from'],
|
||||
new_values['valid_until'],
|
||||
new_values['device_limit'],
|
||||
new_values['active'],
|
||||
license_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
log_audit('UPDATE', 'license', license_id,
|
||||
old_values={
|
||||
'customer_name': current_license['customer_name'],
|
||||
'customer_email': current_license['customer_email'],
|
||||
'valid_from': str(current_license['valid_from']),
|
||||
'valid_until': str(current_license['valid_until']),
|
||||
'device_limit': current_license['device_limit'],
|
||||
'active': current_license['active']
|
||||
},
|
||||
new_values=new_values)
|
||||
|
||||
flash('Lizenz erfolgreich aktualisiert!', 'success')
|
||||
|
||||
# Preserve show_test parameter if present
|
||||
show_test = request.args.get('show_test', 'false')
|
||||
return redirect(url_for('licenses.licenses', show_test=show_test))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Aktualisieren der Lizenz: {str(e)}")
|
||||
flash('Fehler beim Aktualisieren der Lizenz!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# GET request
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
flash('Lizenz nicht gefunden!', 'error')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
|
||||
return render_template("edit_license.html", license=license_data)
|
||||
|
||||
|
||||
@license_bp.route("/license/delete/<int:license_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_license(license_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get license data before deletion
|
||||
license_data = get_license_by_id(license_id)
|
||||
if not license_data:
|
||||
flash('Lizenz nicht gefunden!', 'error')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
|
||||
# Delete from sessions first
|
||||
cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_data['license_key'],))
|
||||
|
||||
# Delete the license
|
||||
cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log deletion
|
||||
log_audit('DELETE', 'license', license_id,
|
||||
old_values={
|
||||
'license_key': license_data['license_key'],
|
||||
'customer_name': license_data['customer_name'],
|
||||
'customer_email': license_data['customer_email']
|
||||
})
|
||||
|
||||
flash(f'Lizenz {license_data["license_key"]} erfolgreich gelöscht!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Löschen der Lizenz: {str(e)}")
|
||||
flash('Fehler beim Löschen der Lizenz!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Preserve show_test parameter if present
|
||||
show_test = request.args.get('show_test', 'false')
|
||||
return redirect(url_for('licenses.licenses', show_test=show_test))
|
||||
|
||||
|
||||
@license_bp.route("/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_license():
|
||||
if request.method == "POST":
|
||||
customer_id = request.form.get("customer_id")
|
||||
license_key = request.form["license_key"].upper() # Immer Großbuchstaben
|
||||
license_type = request.form["license_type"]
|
||||
valid_from = request.form["valid_from"]
|
||||
is_test = request.form.get("is_test") == "on" # Checkbox value
|
||||
|
||||
# Berechne valid_until basierend auf Laufzeit
|
||||
duration = int(request.form.get("duration", 1))
|
||||
duration_type = request.form.get("duration_type", "years")
|
||||
|
||||
start_date = datetime.strptime(valid_from, "%Y-%m-%d")
|
||||
|
||||
if duration_type == "days":
|
||||
end_date = start_date + timedelta(days=duration)
|
||||
elif duration_type == "months":
|
||||
end_date = start_date + relativedelta(months=duration)
|
||||
else: # years
|
||||
end_date = start_date + relativedelta(years=duration)
|
||||
|
||||
# Ein Tag abziehen, da der Starttag mitgezählt wird
|
||||
end_date = end_date - timedelta(days=1)
|
||||
valid_until = end_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Validiere License Key Format
|
||||
if not validate_license_key(license_key):
|
||||
flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error')
|
||||
return redirect(url_for('licenses.create_license'))
|
||||
|
||||
# Resource counts
|
||||
domain_count = int(request.form.get("domain_count", 1))
|
||||
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))
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Prüfe ob neuer Kunde oder bestehender
|
||||
if customer_id == "new":
|
||||
# Neuer Kunde
|
||||
name = request.form.get("customer_name")
|
||||
email = request.form.get("email")
|
||||
|
||||
if not name:
|
||||
flash('Kundenname ist erforderlich!', 'error')
|
||||
return redirect(url_for('licenses.create_license'))
|
||||
|
||||
# Prüfe ob E-Mail bereits existiert
|
||||
if email:
|
||||
cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
|
||||
existing = cur.fetchone()
|
||||
if existing:
|
||||
flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
|
||||
return redirect(url_for('licenses.create_license'))
|
||||
|
||||
# Kunde einfügen (erbt Test-Status von Lizenz)
|
||||
cur.execute("""
|
||||
INSERT INTO customers (name, email, is_test, created_at)
|
||||
VALUES (%s, %s, %s, NOW())
|
||||
RETURNING id
|
||||
""", (name, email, is_test))
|
||||
customer_id = cur.fetchone()[0]
|
||||
customer_info = {'name': name, 'email': email, 'is_test': is_test}
|
||||
|
||||
# Audit-Log für neuen Kunden
|
||||
log_audit('CREATE', 'customer', customer_id,
|
||||
new_values={'name': name, 'email': email, 'is_test': is_test})
|
||||
else:
|
||||
# Bestehender Kunde - hole Infos für Audit-Log
|
||||
cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
|
||||
customer_data = cur.fetchone()
|
||||
if not customer_data:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('licenses.create_license'))
|
||||
customer_info = {'name': customer_data[0], 'email': customer_data[1]}
|
||||
|
||||
# Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
|
||||
if customer_data[2]: # is_test des Kunden
|
||||
is_test = True
|
||||
|
||||
# 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_test)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %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_test))
|
||||
license_id = cur.fetchone()[0]
|
||||
|
||||
# Ressourcen zuweisen
|
||||
try:
|
||||
# Prüfe Verfügbarkeit
|
||||
cur.execute("""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
|
||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
|
||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
|
||||
""", (is_test, is_test, is_test))
|
||||
available = cur.fetchone()
|
||||
|
||||
if available[0] < domain_count:
|
||||
raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})")
|
||||
if available[1] < ipv4_count:
|
||||
raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})")
|
||||
if available[2] < phone_count:
|
||||
raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})")
|
||||
|
||||
# Domains zuweisen
|
||||
if domain_count > 0:
|
||||
cur.execute("""
|
||||
SELECT id FROM resource_pools
|
||||
WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_test, domain_count))
|
||||
for (resource_id,) in cur.fetchall():
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'allocated', allocated_to_license = %s,
|
||||
status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
|
||||
WHERE id = %s
|
||||
""", (license_id, session['username'], resource_id))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (license_id, resource_id, session['username']))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'allocated', %s, %s)
|
||||
""", (resource_id, license_id, session['username'], get_client_ip()))
|
||||
|
||||
# IPv4s zuweisen
|
||||
if ipv4_count > 0:
|
||||
cur.execute("""
|
||||
SELECT id FROM resource_pools
|
||||
WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_test, ipv4_count))
|
||||
for (resource_id,) in cur.fetchall():
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'allocated', allocated_to_license = %s,
|
||||
status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
|
||||
WHERE id = %s
|
||||
""", (license_id, session['username'], resource_id))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (license_id, resource_id, session['username']))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'allocated', %s, %s)
|
||||
""", (resource_id, license_id, session['username'], get_client_ip()))
|
||||
|
||||
# Telefonnummern zuweisen
|
||||
if phone_count > 0:
|
||||
cur.execute("""
|
||||
SELECT id FROM resource_pools
|
||||
WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_test, phone_count))
|
||||
for (resource_id,) in cur.fetchall():
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'allocated', allocated_to_license = %s,
|
||||
status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
|
||||
WHERE id = %s
|
||||
""", (license_id, session['username'], resource_id))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (license_id, resource_id, session['username']))
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'allocated', %s, %s)
|
||||
""", (resource_id, license_id, session['username'], get_client_ip()))
|
||||
|
||||
except ValueError as e:
|
||||
conn.rollback()
|
||||
flash(str(e), 'error')
|
||||
return redirect(url_for('licenses.create_license'))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('CREATE', 'license', license_id,
|
||||
new_values={
|
||||
'license_key': license_key,
|
||||
'customer_name': customer_info['name'],
|
||||
'customer_email': customer_info['email'],
|
||||
'license_type': license_type,
|
||||
'valid_from': valid_from,
|
||||
'valid_until': valid_until,
|
||||
'device_limit': device_limit,
|
||||
'is_test': is_test
|
||||
})
|
||||
|
||||
flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}")
|
||||
flash('Fehler beim Erstellen der Lizenz!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Preserve show_test parameter if present
|
||||
redirect_url = url_for('licenses.create_license')
|
||||
if request.args.get('show_test') == 'true':
|
||||
redirect_url += "?show_test=true"
|
||||
return redirect(redirect_url)
|
||||
|
||||
# Unterstützung für vorausgewählten Kunden
|
||||
preselected_customer_id = request.args.get('customer_id', type=int)
|
||||
return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
|
||||
617
v2_adminpanel/routes/resource_routes.py
Normale Datei
617
v2_adminpanel/routes/resource_routes.py
Normale Datei
@@ -0,0 +1,617 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
|
||||
|
||||
import config
|
||||
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
|
||||
|
||||
# Create Blueprint
|
||||
resource_bp = Blueprint('resources', __name__)
|
||||
|
||||
|
||||
@resource_bp.route('/resources')
|
||||
@login_required
|
||||
def resources():
|
||||
"""Zeigt die Ressourcenpool-Übersicht"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Filter aus Query-Parametern
|
||||
resource_type = request.args.get('type', 'all')
|
||||
status_filter = request.args.get('status', 'all')
|
||||
search_query = request.args.get('search', '')
|
||||
show_test = request.args.get('show_test', 'false') == 'true'
|
||||
|
||||
# Basis-Query
|
||||
query = """
|
||||
SELECT
|
||||
rp.id,
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
rp.status,
|
||||
rp.is_test,
|
||||
rp.allocated_to_license,
|
||||
rp.created_at,
|
||||
rp.status_changed_at,
|
||||
rp.status_changed_by,
|
||||
l.customer_name,
|
||||
l.license_type
|
||||
FROM resource_pools rp
|
||||
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
|
||||
# Filter anwenden
|
||||
if resource_type != 'all':
|
||||
query += " AND rp.resource_type = %s"
|
||||
params.append(resource_type)
|
||||
|
||||
if status_filter != 'all':
|
||||
query += " AND rp.status = %s"
|
||||
params.append(status_filter)
|
||||
|
||||
if search_query:
|
||||
query += " AND (rp.resource_value ILIKE %s OR l.customer_name ILIKE %s)"
|
||||
params.extend([f'%{search_query}%', f'%{search_query}%'])
|
||||
|
||||
if not show_test:
|
||||
query += " AND rp.is_test = false"
|
||||
|
||||
query += " ORDER BY rp.resource_type, rp.resource_value"
|
||||
|
||||
cur.execute(query, params)
|
||||
|
||||
resources_list = []
|
||||
for row in cur.fetchall():
|
||||
resources_list.append({
|
||||
'id': row[0],
|
||||
'resource_type': row[1],
|
||||
'resource_value': row[2],
|
||||
'status': row[3],
|
||||
'is_test': row[4],
|
||||
'allocated_to_license': row[5],
|
||||
'created_at': row[6],
|
||||
'status_changed_at': row[7],
|
||||
'status_changed_by': row[8],
|
||||
'customer_name': row[9],
|
||||
'license_type': row[10]
|
||||
})
|
||||
|
||||
# Statistiken
|
||||
cur.execute("""
|
||||
SELECT
|
||||
resource_type,
|
||||
status,
|
||||
is_test,
|
||||
COUNT(*) as count
|
||||
FROM resource_pools
|
||||
GROUP BY resource_type, status, is_test
|
||||
""")
|
||||
|
||||
stats = {}
|
||||
for row in cur.fetchall():
|
||||
res_type = row[0]
|
||||
status = row[1]
|
||||
is_test = row[2]
|
||||
count = row[3]
|
||||
|
||||
if res_type not in stats:
|
||||
stats[res_type] = {'available': 0, 'allocated': 0, 'quarantined': 0, 'test': 0, 'prod': 0}
|
||||
|
||||
stats[res_type][status] = stats[res_type].get(status, 0) + count
|
||||
if is_test:
|
||||
stats[res_type]['test'] += count
|
||||
else:
|
||||
stats[res_type]['prod'] += count
|
||||
|
||||
return render_template('resources.html',
|
||||
resources=resources_list,
|
||||
stats=stats,
|
||||
resource_type=resource_type,
|
||||
status_filter=status_filter,
|
||||
search_query=search_query,
|
||||
show_test=show_test)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Ressourcen: {str(e)}")
|
||||
flash('Fehler beim Laden der Ressourcen!', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@resource_bp.route('/resources/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_resource():
|
||||
"""Neue Ressource hinzufügen"""
|
||||
if request.method == 'POST':
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
resource_type = request.form['resource_type']
|
||||
resource_value = request.form['resource_value'].strip()
|
||||
is_test = 'is_test' in request.form
|
||||
|
||||
# Prüfe ob Ressource bereits existiert
|
||||
cur.execute("""
|
||||
SELECT id FROM resource_pools
|
||||
WHERE resource_type = %s AND resource_value = %s
|
||||
""", (resource_type, resource_value))
|
||||
|
||||
if cur.fetchone():
|
||||
flash(f'Ressource {resource_value} existiert bereits!', 'error')
|
||||
return redirect(url_for('resources.add_resource'))
|
||||
|
||||
# Füge neue Ressource hinzu
|
||||
cur.execute("""
|
||||
INSERT INTO resource_pools (resource_type, resource_value, status, is_test, created_by)
|
||||
VALUES (%s, %s, 'available', %s, %s)
|
||||
RETURNING id
|
||||
""", (resource_type, resource_value, is_test, session['username']))
|
||||
|
||||
resource_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('CREATE', 'resource', resource_id,
|
||||
new_values={
|
||||
'resource_type': resource_type,
|
||||
'resource_value': resource_value,
|
||||
'is_test': is_test
|
||||
})
|
||||
|
||||
flash(f'Ressource {resource_value} erfolgreich hinzugefügt!', 'success')
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Hinzufügen der Ressource: {str(e)}")
|
||||
flash('Fehler beim Hinzufügen der Ressource!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template('add_resource.html')
|
||||
|
||||
|
||||
@resource_bp.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
|
||||
@login_required
|
||||
def quarantine_resource(resource_id):
|
||||
"""Ressource in Quarantäne versetzen"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
reason = request.form.get('reason', '')
|
||||
|
||||
# Hole aktuelle Ressourcen-Info
|
||||
cur.execute("""
|
||||
SELECT resource_value, status, allocated_to_license
|
||||
FROM resource_pools WHERE id = %s
|
||||
""", (resource_id,))
|
||||
resource = cur.fetchone()
|
||||
|
||||
if not resource:
|
||||
flash('Ressource nicht gefunden!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
# Setze Status auf quarantined
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'quarantined',
|
||||
allocated_to_license = NULL,
|
||||
status_changed_at = CURRENT_TIMESTAMP,
|
||||
status_changed_by = %s,
|
||||
quarantine_reason = %s
|
||||
WHERE id = %s
|
||||
""", (session['username'], reason, resource_id))
|
||||
|
||||
# Wenn die Ressource zugewiesen war, entferne die Zuweisung
|
||||
if resource[2]: # allocated_to_license
|
||||
cur.execute("""
|
||||
DELETE FROM license_resources
|
||||
WHERE license_id = %s AND resource_id = %s
|
||||
""", (resource[2], resource_id))
|
||||
|
||||
# History-Eintrag
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, notes, ip_address)
|
||||
VALUES (%s, %s, 'quarantined', %s, %s, %s)
|
||||
""", (resource_id, resource[2], session['username'], reason, get_client_ip()))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit-Log
|
||||
log_audit('QUARANTINE', 'resource', resource_id,
|
||||
old_values={'status': resource[1]},
|
||||
new_values={'status': 'quarantined', 'reason': reason})
|
||||
|
||||
flash(f'Ressource {resource[0]} in Quarantäne versetzt!', 'warning')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Quarantänisieren der Ressource: {str(e)}")
|
||||
flash('Fehler beim Quarantänisieren der Ressource!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
|
||||
@resource_bp.route('/resources/release', methods=['POST'])
|
||||
@login_required
|
||||
def release_resources():
|
||||
"""Ressourcen aus Quarantäne freigeben oder von Lizenz entfernen"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
resource_ids = request.form.getlist('resource_ids[]')
|
||||
action = request.form.get('action', 'release')
|
||||
|
||||
if not resource_ids:
|
||||
flash('Keine Ressourcen ausgewählt!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
for resource_id in resource_ids:
|
||||
# Hole aktuelle Ressourcen-Info
|
||||
cur.execute("""
|
||||
SELECT resource_value, status, allocated_to_license
|
||||
FROM resource_pools WHERE id = %s
|
||||
""", (resource_id,))
|
||||
resource = cur.fetchone()
|
||||
|
||||
if resource:
|
||||
# Setze Status auf available
|
||||
cur.execute("""
|
||||
UPDATE resource_pools
|
||||
SET status = 'available',
|
||||
allocated_to_license = NULL,
|
||||
status_changed_at = CURRENT_TIMESTAMP,
|
||||
status_changed_by = %s,
|
||||
quarantine_reason = NULL
|
||||
WHERE id = %s
|
||||
""", (session['username'], resource_id))
|
||||
|
||||
# Entferne Lizenz-Zuweisung wenn vorhanden
|
||||
if resource[2]: # allocated_to_license
|
||||
cur.execute("""
|
||||
DELETE FROM license_resources
|
||||
WHERE license_id = %s AND resource_id = %s
|
||||
""", (resource[2], resource_id))
|
||||
|
||||
# History-Eintrag
|
||||
cur.execute("""
|
||||
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
||||
VALUES (%s, %s, 'released', %s, %s)
|
||||
""", (resource_id, resource[2], session['username'], get_client_ip()))
|
||||
|
||||
# Audit-Log
|
||||
log_audit('RELEASE', 'resource', resource_id,
|
||||
old_values={'status': resource[1]},
|
||||
new_values={'status': 'available'})
|
||||
|
||||
conn.commit()
|
||||
flash(f'{len(resource_ids)} Ressource(n) freigegeben!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Freigeben der Ressourcen: {str(e)}")
|
||||
flash('Fehler beim Freigeben der Ressourcen!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
|
||||
@resource_bp.route('/resources/history/<int:resource_id>')
|
||||
@login_required
|
||||
def resource_history(resource_id):
|
||||
"""Zeigt die Historie einer Ressource"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Ressourcen-Info
|
||||
cur.execute("""
|
||||
SELECT resource_type, resource_value, status, is_test
|
||||
FROM resource_pools WHERE id = %s
|
||||
""", (resource_id,))
|
||||
resource = cur.fetchone()
|
||||
|
||||
if not resource:
|
||||
flash('Ressource nicht gefunden!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
|
||||
# Hole Historie
|
||||
cur.execute("""
|
||||
SELECT
|
||||
rh.action,
|
||||
rh.action_timestamp,
|
||||
rh.action_by,
|
||||
rh.notes,
|
||||
rh.ip_address,
|
||||
l.license_key,
|
||||
c.name as customer_name
|
||||
FROM resource_history rh
|
||||
LEFT JOIN licenses l ON rh.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE rh.resource_id = %s
|
||||
ORDER BY rh.action_timestamp DESC
|
||||
""", (resource_id,))
|
||||
|
||||
history = []
|
||||
for row in cur.fetchall():
|
||||
history.append({
|
||||
'action': row[0],
|
||||
'timestamp': row[1],
|
||||
'by': row[2],
|
||||
'notes': row[3],
|
||||
'ip_address': row[4],
|
||||
'license_key': row[5],
|
||||
'customer_name': row[6]
|
||||
})
|
||||
|
||||
return render_template('resource_history.html',
|
||||
resource={
|
||||
'id': resource_id,
|
||||
'type': resource[0],
|
||||
'value': resource[1],
|
||||
'status': resource[2],
|
||||
'is_test': resource[3]
|
||||
},
|
||||
history=history)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Ressourcen-Historie: {str(e)}")
|
||||
flash('Fehler beim Laden der Historie!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@resource_bp.route('/resources/metrics')
|
||||
@login_required
|
||||
def resource_metrics():
|
||||
"""Zeigt Metriken und Statistiken zu Ressourcen"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Allgemeine Statistiken
|
||||
cur.execute("""
|
||||
SELECT
|
||||
resource_type,
|
||||
status,
|
||||
is_test,
|
||||
COUNT(*) as count
|
||||
FROM resource_pools
|
||||
GROUP BY resource_type, status, is_test
|
||||
ORDER BY resource_type, status
|
||||
""")
|
||||
|
||||
general_stats = {}
|
||||
for row in cur.fetchall():
|
||||
res_type = row[0]
|
||||
if res_type not in general_stats:
|
||||
general_stats[res_type] = {
|
||||
'total': 0,
|
||||
'available': 0,
|
||||
'allocated': 0,
|
||||
'quarantined': 0,
|
||||
'test': 0,
|
||||
'production': 0
|
||||
}
|
||||
|
||||
general_stats[res_type]['total'] += row[3]
|
||||
general_stats[res_type][row[1]] += row[3]
|
||||
if row[2]:
|
||||
general_stats[res_type]['test'] += row[3]
|
||||
else:
|
||||
general_stats[res_type]['production'] += row[3]
|
||||
|
||||
# Zuweisungs-Statistiken
|
||||
cur.execute("""
|
||||
SELECT
|
||||
rp.resource_type,
|
||||
COUNT(DISTINCT l.customer_id) as unique_customers,
|
||||
COUNT(DISTINCT rp.allocated_to_license) as unique_licenses
|
||||
FROM resource_pools rp
|
||||
JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
WHERE rp.status = 'allocated'
|
||||
GROUP BY rp.resource_type
|
||||
""")
|
||||
|
||||
allocation_stats = {}
|
||||
for row in cur.fetchall():
|
||||
allocation_stats[row[0]] = {
|
||||
'unique_customers': row[1],
|
||||
'unique_licenses': row[2]
|
||||
}
|
||||
|
||||
# Historische Daten (letzte 30 Tage)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
DATE(action_timestamp) as date,
|
||||
action,
|
||||
COUNT(*) as count
|
||||
FROM resource_history
|
||||
WHERE action_timestamp >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY DATE(action_timestamp), action
|
||||
ORDER BY date, action
|
||||
""")
|
||||
|
||||
historical_data = {}
|
||||
for row in cur.fetchall():
|
||||
date_str = row[0].strftime('%Y-%m-%d')
|
||||
if date_str not in historical_data:
|
||||
historical_data[date_str] = {}
|
||||
historical_data[date_str][row[1]] = row[2]
|
||||
|
||||
# Top-Kunden nach Ressourcennutzung
|
||||
cur.execute("""
|
||||
SELECT
|
||||
c.name,
|
||||
rp.resource_type,
|
||||
COUNT(*) as count
|
||||
FROM resource_pools rp
|
||||
JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
WHERE rp.status = 'allocated'
|
||||
GROUP BY c.name, rp.resource_type
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
""")
|
||||
|
||||
top_customers = []
|
||||
for row in cur.fetchall():
|
||||
top_customers.append({
|
||||
'customer': row[0],
|
||||
'resource_type': row[1],
|
||||
'count': row[2]
|
||||
})
|
||||
|
||||
return render_template('resource_metrics.html',
|
||||
general_stats=general_stats,
|
||||
allocation_stats=allocation_stats,
|
||||
historical_data=historical_data,
|
||||
top_customers=top_customers)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Ressourcen-Metriken: {str(e)}")
|
||||
flash('Fehler beim Laden der Metriken!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@resource_bp.route('/resources/report', methods=['GET'])
|
||||
@login_required
|
||||
def resource_report():
|
||||
"""Generiert einen Ressourcen-Report"""
|
||||
from io import BytesIO
|
||||
import xlsxwriter
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Erstelle Excel-Datei im Speicher
|
||||
output = BytesIO()
|
||||
workbook = xlsxwriter.Workbook(output)
|
||||
|
||||
# Formatierungen
|
||||
header_format = workbook.add_format({
|
||||
'bold': True,
|
||||
'bg_color': '#4CAF50',
|
||||
'font_color': 'white',
|
||||
'border': 1
|
||||
})
|
||||
|
||||
date_format = workbook.add_format({'num_format': 'dd.mm.yyyy hh:mm'})
|
||||
|
||||
# Sheet 1: Übersicht
|
||||
overview_sheet = workbook.add_worksheet('Übersicht')
|
||||
|
||||
# Header
|
||||
headers = ['Ressourcen-Typ', 'Gesamt', 'Verfügbar', 'Zugewiesen', 'Quarantäne', 'Test', 'Produktion']
|
||||
for col, header in enumerate(headers):
|
||||
overview_sheet.write(0, col, header, header_format)
|
||||
|
||||
# Daten
|
||||
cur.execute("""
|
||||
SELECT
|
||||
resource_type,
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN status = 'available' THEN 1 END) as available,
|
||||
COUNT(CASE WHEN status = 'allocated' THEN 1 END) as allocated,
|
||||
COUNT(CASE WHEN status = 'quarantined' THEN 1 END) as quarantined,
|
||||
COUNT(CASE WHEN is_test = true THEN 1 END) as test,
|
||||
COUNT(CASE WHEN is_test = false THEN 1 END) as production
|
||||
FROM resource_pools
|
||||
GROUP BY resource_type
|
||||
ORDER BY resource_type
|
||||
""")
|
||||
|
||||
row = 1
|
||||
for data in cur.fetchall():
|
||||
for col, value in enumerate(data):
|
||||
overview_sheet.write(row, col, value)
|
||||
row += 1
|
||||
|
||||
# Sheet 2: Detailliste
|
||||
detail_sheet = workbook.add_worksheet('Detailliste')
|
||||
|
||||
# Header
|
||||
headers = ['Typ', 'Wert', 'Status', 'Test', 'Kunde', 'Lizenz', 'Zugewiesen am', 'Zugewiesen von']
|
||||
for col, header in enumerate(headers):
|
||||
detail_sheet.write(0, col, header, header_format)
|
||||
|
||||
# Daten
|
||||
cur.execute("""
|
||||
SELECT
|
||||
rp.resource_type,
|
||||
rp.resource_value,
|
||||
rp.status,
|
||||
rp.is_test,
|
||||
c.name as customer_name,
|
||||
l.license_key,
|
||||
rp.status_changed_at,
|
||||
rp.status_changed_by
|
||||
FROM resource_pools rp
|
||||
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""")
|
||||
|
||||
row = 1
|
||||
for data in cur.fetchall():
|
||||
for col, value in enumerate(data):
|
||||
if col == 6 and value: # Datum
|
||||
detail_sheet.write_datetime(row, col, value, date_format)
|
||||
else:
|
||||
detail_sheet.write(row, col, value if value is not None else '')
|
||||
row += 1
|
||||
|
||||
# Spaltenbreiten anpassen
|
||||
overview_sheet.set_column('A:A', 20)
|
||||
overview_sheet.set_column('B:G', 12)
|
||||
|
||||
detail_sheet.set_column('A:A', 15)
|
||||
detail_sheet.set_column('B:B', 30)
|
||||
detail_sheet.set_column('C:D', 12)
|
||||
detail_sheet.set_column('E:F', 25)
|
||||
detail_sheet.set_column('G:H', 20)
|
||||
|
||||
workbook.close()
|
||||
output.seek(0)
|
||||
|
||||
# Sende Datei
|
||||
filename = f"ressourcen_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Generieren des Reports: {str(e)}")
|
||||
flash('Fehler beim Generieren des Reports!', 'error')
|
||||
return redirect(url_for('resources.resources'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
388
v2_adminpanel/routes/session_routes.py
Normale Datei
388
v2_adminpanel/routes/session_routes.py
Normale Datei
@@ -0,0 +1,388 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash
|
||||
|
||||
import config
|
||||
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 models import get_active_sessions
|
||||
|
||||
# Create Blueprint
|
||||
session_bp = Blueprint('sessions', __name__)
|
||||
|
||||
|
||||
@session_bp.route("/sessions")
|
||||
@login_required
|
||||
def sessions():
|
||||
active_sessions = get_active_sessions()
|
||||
return render_template("sessions.html", sessions=active_sessions)
|
||||
|
||||
|
||||
@session_bp.route("/sessions/history")
|
||||
@login_required
|
||||
def session_history():
|
||||
"""Zeigt die Session-Historie"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Query parameters
|
||||
license_key = request.args.get('license_key', '')
|
||||
username = request.args.get('username', '')
|
||||
days = int(request.args.get('days', 7))
|
||||
|
||||
# Base query
|
||||
query = """
|
||||
SELECT
|
||||
s.id,
|
||||
s.license_key,
|
||||
s.username,
|
||||
s.device_id,
|
||||
s.login_time,
|
||||
s.logout_time,
|
||||
s.last_activity,
|
||||
s.active,
|
||||
l.customer_name,
|
||||
l.license_type,
|
||||
l.is_test
|
||||
FROM sessions s
|
||||
LEFT JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
|
||||
# Apply filters
|
||||
if license_key:
|
||||
query += " AND s.license_key = %s"
|
||||
params.append(license_key)
|
||||
|
||||
if username:
|
||||
query += " AND s.username ILIKE %s"
|
||||
params.append(f'%{username}%')
|
||||
|
||||
# Time filter
|
||||
query += " AND s.login_time >= CURRENT_TIMESTAMP - INTERVAL '%s days'"
|
||||
params.append(days)
|
||||
|
||||
query += " ORDER BY s.login_time DESC LIMIT 1000"
|
||||
|
||||
cur.execute(query, params)
|
||||
|
||||
sessions_list = []
|
||||
for row in cur.fetchall():
|
||||
session_duration = None
|
||||
if row[4] and row[5]: # login_time and logout_time
|
||||
duration = row[5] - row[4]
|
||||
hours = int(duration.total_seconds() // 3600)
|
||||
minutes = int((duration.total_seconds() % 3600) // 60)
|
||||
session_duration = f"{hours}h {minutes}m"
|
||||
elif row[4] and row[7]: # login_time and active
|
||||
duration = datetime.now(ZoneInfo("UTC")) - row[4]
|
||||
hours = int(duration.total_seconds() // 3600)
|
||||
minutes = int((duration.total_seconds() % 3600) // 60)
|
||||
session_duration = f"{hours}h {minutes}m (aktiv)"
|
||||
|
||||
sessions_list.append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'username': row[2],
|
||||
'device_id': row[3],
|
||||
'login_time': row[4],
|
||||
'logout_time': row[5],
|
||||
'last_activity': row[6],
|
||||
'active': row[7],
|
||||
'customer_name': row[8],
|
||||
'license_type': row[9],
|
||||
'is_test': row[10],
|
||||
'duration': session_duration
|
||||
})
|
||||
|
||||
# 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.login_time >= CURRENT_TIMESTAMP - INTERVAL '30 days'
|
||||
ORDER BY l.customer_name, s.license_key
|
||||
""")
|
||||
|
||||
available_licenses = []
|
||||
for row in cur.fetchall():
|
||||
available_licenses.append({
|
||||
'license_key': row[0],
|
||||
'customer_name': row[1] or 'Unbekannt'
|
||||
})
|
||||
|
||||
return render_template("session_history.html",
|
||||
sessions=sessions_list,
|
||||
available_licenses=available_licenses,
|
||||
filters={
|
||||
'license_key': license_key,
|
||||
'username': username,
|
||||
'days': days
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Session-Historie: {str(e)}")
|
||||
flash('Fehler beim Laden der Session-Historie!', 'error')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@session_bp.route("/session/terminate/<int:session_id>", methods=["POST"])
|
||||
@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, device_id
|
||||
FROM sessions
|
||||
WHERE id = %s AND active = true
|
||||
""", (session_id,))
|
||||
|
||||
session_info = cur.fetchone()
|
||||
if not session_info:
|
||||
flash('Session nicht gefunden oder bereits beendet!', 'error')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
|
||||
# Terminate session
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET active = false, logout_time = CURRENT_TIMESTAMP
|
||||
WHERE id = %s
|
||||
""", (session_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit log
|
||||
log_audit('SESSION_TERMINATE', 'session', session_id,
|
||||
additional_info=f"Session beendet für {session_info[1]} auf Lizenz {session_info[0]}")
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
@session_bp.route("/sessions/terminate-all/<license_key>", methods=["POST"])
|
||||
@login_required
|
||||
def terminate_all_sessions(license_key):
|
||||
"""Beendet alle aktiven Sessions einer Lizenz"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Count active sessions
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM sessions
|
||||
WHERE license_key = %s AND active = true
|
||||
""", (license_key,))
|
||||
|
||||
active_count = cur.fetchone()[0]
|
||||
|
||||
if active_count == 0:
|
||||
flash('Keine aktiven Sessions gefunden!', 'info')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
|
||||
# Terminate all sessions
|
||||
cur.execute("""
|
||||
UPDATE sessions
|
||||
SET active = false, logout_time = CURRENT_TIMESTAMP
|
||||
WHERE license_key = %s AND active = true
|
||||
""", (license_key,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit log
|
||||
log_audit('SESSION_TERMINATE_ALL', 'license', None,
|
||||
additional_info=f"{active_count} Sessions beendet für Lizenz {license_key}")
|
||||
|
||||
flash(f'{active_count} Sessions erfolgreich beendet!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Beenden der Sessions: {str(e)}")
|
||||
flash('Fehler beim Beenden der Sessions!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
|
||||
|
||||
@session_bp.route("/sessions/cleanup", methods=["POST"])
|
||||
@login_required
|
||||
def cleanup_sessions():
|
||||
"""Bereinigt alte inaktive Sessions"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
days = int(request.form.get('days', 30))
|
||||
|
||||
# Delete old inactive sessions
|
||||
cur.execute("""
|
||||
DELETE FROM sessions
|
||||
WHERE active = false
|
||||
AND logout_time < CURRENT_TIMESTAMP - INTERVAL '%s days'
|
||||
RETURNING id
|
||||
""", (days,))
|
||||
|
||||
deleted_ids = [row[0] for row in cur.fetchall()]
|
||||
deleted_count = len(deleted_ids)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Audit log
|
||||
if deleted_count > 0:
|
||||
log_audit('SESSION_CLEANUP', 'system', None,
|
||||
additional_info=f"{deleted_count} Sessions älter als {days} Tage gelöscht")
|
||||
|
||||
flash(f'{deleted_count} alte Sessions bereinigt!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Bereinigen der Sessions: {str(e)}")
|
||||
flash('Fehler beim Bereinigen der Sessions!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('sessions.session_history'))
|
||||
|
||||
|
||||
@session_bp.route("/sessions/statistics")
|
||||
@login_required
|
||||
def session_statistics():
|
||||
"""Zeigt Session-Statistiken"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Aktuelle Statistiken
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(DISTINCT s.license_key) as active_licenses,
|
||||
COUNT(DISTINCT s.username) as unique_users,
|
||||
COUNT(DISTINCT s.device_id) as unique_devices,
|
||||
COUNT(*) as total_active_sessions
|
||||
FROM sessions s
|
||||
WHERE s.active = true
|
||||
""")
|
||||
|
||||
current_stats = cur.fetchone()
|
||||
|
||||
# Sessions nach Lizenztyp
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.license_type,
|
||||
COUNT(*) as session_count
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.active = true
|
||||
GROUP BY l.license_type
|
||||
ORDER BY session_count DESC
|
||||
""")
|
||||
|
||||
sessions_by_type = []
|
||||
for row in cur.fetchall():
|
||||
sessions_by_type.append({
|
||||
'license_type': row[0],
|
||||
'count': row[1]
|
||||
})
|
||||
|
||||
# Top 10 Lizenzen nach aktiven Sessions
|
||||
cur.execute("""
|
||||
SELECT
|
||||
s.license_key,
|
||||
l.customer_name,
|
||||
COUNT(*) as session_count,
|
||||
l.device_limit
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_key = l.license_key
|
||||
WHERE s.active = true
|
||||
GROUP BY s.license_key, l.customer_name, l.device_limit
|
||||
ORDER BY session_count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
top_licenses = []
|
||||
for row in cur.fetchall():
|
||||
top_licenses.append({
|
||||
'license_key': row[0],
|
||||
'customer_name': row[1],
|
||||
'session_count': row[2],
|
||||
'device_limit': row[3]
|
||||
})
|
||||
|
||||
# Session-Verlauf (letzte 7 Tage)
|
||||
cur.execute("""
|
||||
SELECT
|
||||
DATE(login_time) as date,
|
||||
COUNT(*) as login_count,
|
||||
COUNT(DISTINCT license_key) as unique_licenses,
|
||||
COUNT(DISTINCT username) as unique_users
|
||||
FROM sessions
|
||||
WHERE login_time >= CURRENT_DATE - INTERVAL '7 days'
|
||||
GROUP BY DATE(login_time)
|
||||
ORDER BY date
|
||||
""")
|
||||
|
||||
session_history = []
|
||||
for row in cur.fetchall():
|
||||
session_history.append({
|
||||
'date': row[0].strftime('%Y-%m-%d'),
|
||||
'login_count': row[1],
|
||||
'unique_licenses': row[2],
|
||||
'unique_users': row[3]
|
||||
})
|
||||
|
||||
# Durchschnittliche Session-Dauer
|
||||
cur.execute("""
|
||||
SELECT
|
||||
AVG(EXTRACT(EPOCH FROM (logout_time - login_time))/3600) as avg_duration_hours
|
||||
FROM sessions
|
||||
WHERE active = false
|
||||
AND logout_time IS NOT NULL
|
||||
AND logout_time - login_time < INTERVAL '24 hours'
|
||||
AND login_time >= CURRENT_DATE - INTERVAL '30 days'
|
||||
""")
|
||||
|
||||
avg_duration = cur.fetchone()[0] or 0
|
||||
|
||||
return render_template("session_statistics.html",
|
||||
current_stats={
|
||||
'active_licenses': current_stats[0],
|
||||
'unique_users': current_stats[1],
|
||||
'unique_devices': current_stats[2],
|
||||
'total_sessions': current_stats[3]
|
||||
},
|
||||
sessions_by_type=sessions_by_type,
|
||||
top_licenses=top_licenses,
|
||||
session_history=session_history,
|
||||
avg_duration=round(avg_duration, 1))
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Session-Statistiken: {str(e)}")
|
||||
flash('Fehler beim Laden der Statistiken!', 'error')
|
||||
return redirect(url_for('sessions.sessions'))
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
33
v2_adminpanel/scheduler.py
Normale Datei
33
v2_adminpanel/scheduler.py
Normale Datei
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Scheduler module for handling background tasks
|
||||
"""
|
||||
import logging
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import config
|
||||
from utils.backup import create_backup
|
||||
|
||||
|
||||
def scheduled_backup():
|
||||
"""Führt ein geplantes Backup aus"""
|
||||
logging.info("Starte geplantes Backup...")
|
||||
create_backup(backup_type="scheduled", created_by="scheduler")
|
||||
|
||||
|
||||
def init_scheduler():
|
||||
"""Initialize and configure the scheduler"""
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# Configure daily backup job
|
||||
scheduler.add_job(
|
||||
scheduled_backup,
|
||||
'cron',
|
||||
hour=config.SCHEDULER_CONFIG['backup_hour'],
|
||||
minute=config.SCHEDULER_CONFIG['backup_minute'],
|
||||
id='daily_backup',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
scheduler.start()
|
||||
logging.info(f"Scheduler started. Daily backup scheduled at {config.SCHEDULER_CONFIG['backup_hour']:02d}:{config.SCHEDULER_CONFIG['backup_minute']:02d}")
|
||||
|
||||
return scheduler
|
||||
188
v2_adminpanel/test_blueprint_routes.py
Normale Datei
188
v2_adminpanel/test_blueprint_routes.py
Normale Datei
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test-Skript zur Verifizierung aller Blueprint-Routes
|
||||
Prüft ob alle Routes korrekt registriert sind und erreichbar sind
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app import app
|
||||
from flask import url_for
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_all_routes():
|
||||
"""Test alle registrierten Routes"""
|
||||
|
||||
print("=== Blueprint Route Test ===\n")
|
||||
|
||||
# Sammle alle Routes
|
||||
routes_by_blueprint = {}
|
||||
|
||||
with app.test_request_context():
|
||||
for rule in app.url_map.iter_rules():
|
||||
# Skip static files
|
||||
if rule.endpoint == 'static':
|
||||
continue
|
||||
|
||||
# Blueprint name is before the dot
|
||||
parts = rule.endpoint.split('.')
|
||||
if len(parts) == 2:
|
||||
blueprint_name = parts[0]
|
||||
function_name = parts[1]
|
||||
else:
|
||||
blueprint_name = 'app'
|
||||
function_name = rule.endpoint
|
||||
|
||||
if blueprint_name not in routes_by_blueprint:
|
||||
routes_by_blueprint[blueprint_name] = []
|
||||
|
||||
routes_by_blueprint[blueprint_name].append({
|
||||
'rule': str(rule),
|
||||
'endpoint': rule.endpoint,
|
||||
'methods': sorted(rule.methods - {'HEAD', 'OPTIONS'}),
|
||||
'function': function_name
|
||||
})
|
||||
|
||||
# Sortiere und zeige Routes nach Blueprint
|
||||
for blueprint_name in sorted(routes_by_blueprint.keys()):
|
||||
routes = routes_by_blueprint[blueprint_name]
|
||||
print(f"\n📦 Blueprint: {blueprint_name}")
|
||||
print(f" Anzahl Routes: {len(routes)}")
|
||||
print(" " + "-" * 50)
|
||||
|
||||
for route in sorted(routes, key=lambda x: x['rule']):
|
||||
methods = ', '.join(route['methods'])
|
||||
print(f" {route['rule']:<40} [{methods:<15}] -> {route['function']}")
|
||||
|
||||
# Zusammenfassung
|
||||
print("\n=== Zusammenfassung ===")
|
||||
total_routes = sum(len(routes) for routes in routes_by_blueprint.values())
|
||||
print(f"Gesamt Blueprints: {len(routes_by_blueprint)}")
|
||||
print(f"Gesamt Routes: {total_routes}")
|
||||
|
||||
# Erwartete Blueprints prüfen
|
||||
expected_blueprints = ['auth', 'admin', 'license', 'customer', 'resource',
|
||||
'session', 'batch', 'api', 'export']
|
||||
|
||||
print("\n=== Blueprint Status ===")
|
||||
for bp in expected_blueprints:
|
||||
if bp in routes_by_blueprint:
|
||||
print(f"✅ {bp:<10} - {len(routes_by_blueprint[bp])} routes")
|
||||
else:
|
||||
print(f"❌ {bp:<10} - FEHLT!")
|
||||
|
||||
# Prüfe ob noch Routes direkt in app.py sind
|
||||
if 'app' in routes_by_blueprint:
|
||||
print(f"\n⚠️ WARNUNG: {len(routes_by_blueprint['app'])} Routes sind noch direkt in app.py!")
|
||||
for route in routes_by_blueprint['app']:
|
||||
print(f" - {route['rule']}")
|
||||
|
||||
def test_route_accessibility():
|
||||
"""Test ob wichtige Routes erreichbar sind"""
|
||||
print("\n\n=== Route Erreichbarkeits-Test ===\n")
|
||||
|
||||
test_client = app.test_client()
|
||||
|
||||
# Wichtige Routes zum Testen (ohne Login)
|
||||
public_routes = [
|
||||
('GET', '/login', 'Login-Seite'),
|
||||
('GET', '/heartbeat', 'Session Heartbeat'),
|
||||
]
|
||||
|
||||
for method, route, description in public_routes:
|
||||
try:
|
||||
if method == 'GET':
|
||||
response = test_client.get(route)
|
||||
elif method == 'POST':
|
||||
response = test_client.post(route)
|
||||
|
||||
status = "✅" if response.status_code in [200, 302, 401] else "❌"
|
||||
print(f"{status} {method:<6} {route:<30} - Status: {response.status_code} ({description})")
|
||||
|
||||
# Bei Fehler mehr Details
|
||||
if response.status_code >= 400 and response.status_code != 401:
|
||||
print(f" ⚠️ Fehler-Details: {response.data[:200]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ {method:<6} {route:<30} - FEHLER: {str(e)}")
|
||||
|
||||
def check_duplicate_routes():
|
||||
"""Prüfe ob es doppelte Route-Definitionen gibt"""
|
||||
print("\n\n=== Doppelte Routes Check ===\n")
|
||||
|
||||
route_paths = {}
|
||||
duplicates_found = False
|
||||
|
||||
for rule in app.url_map.iter_rules():
|
||||
if rule.endpoint == 'static':
|
||||
continue
|
||||
|
||||
path = str(rule)
|
||||
if path in route_paths:
|
||||
print(f"⚠️ DUPLIKAT gefunden:")
|
||||
print(f" Route: {path}")
|
||||
print(f" 1. Endpoint: {route_paths[path]}")
|
||||
print(f" 2. Endpoint: {rule.endpoint}")
|
||||
duplicates_found = True
|
||||
else:
|
||||
route_paths[path] = rule.endpoint
|
||||
|
||||
if not duplicates_found:
|
||||
print("✅ Keine doppelten Routes gefunden!")
|
||||
|
||||
def check_template_references():
|
||||
"""Prüfe ob Template-Dateien für die Routes existieren"""
|
||||
print("\n\n=== Template Verfügbarkeits-Check ===\n")
|
||||
|
||||
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
|
||||
# Sammle alle verfügbaren Templates
|
||||
available_templates = []
|
||||
if os.path.exists(template_dir):
|
||||
for root, dirs, files in os.walk(template_dir):
|
||||
for file in files:
|
||||
if file.endswith(('.html', '.jinja2')):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), template_dir)
|
||||
available_templates.append(rel_path.replace('\\', '/'))
|
||||
|
||||
print(f"Gefundene Templates: {len(available_templates)}")
|
||||
|
||||
# Wichtige Templates prüfen
|
||||
required_templates = [
|
||||
'login.html',
|
||||
'index.html',
|
||||
'profile.html',
|
||||
'licenses.html',
|
||||
'customers.html',
|
||||
'resources.html',
|
||||
'sessions.html',
|
||||
'audit.html',
|
||||
'backups.html'
|
||||
]
|
||||
|
||||
for template in required_templates:
|
||||
if template in available_templates:
|
||||
print(f"✅ {template}")
|
||||
else:
|
||||
print(f"❌ {template} - FEHLT!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔍 Starte Blueprint-Verifizierung...\n")
|
||||
|
||||
try:
|
||||
test_all_routes()
|
||||
test_route_accessibility()
|
||||
check_duplicate_routes()
|
||||
check_template_references()
|
||||
|
||||
print("\n\n✅ Test abgeschlossen!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ Fehler beim Test: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@@ -124,4 +124,48 @@ def prepare_audit_export_data(audit_logs):
|
||||
str(log['new_values']) if log['new_values'] else '',
|
||||
log['additional_info'] or ''
|
||||
])
|
||||
return export_data
|
||||
return export_data
|
||||
|
||||
|
||||
def create_batch_export(licenses):
|
||||
"""Create Excel export for batch licenses"""
|
||||
export_data = []
|
||||
|
||||
for license in licenses:
|
||||
export_data.append({
|
||||
'Lizenzschlüssel': license['license_key'],
|
||||
'Kunde': license.get('customer_name', ''),
|
||||
'Email': license.get('email', ''),
|
||||
'Max. Benutzer': license.get('max_users', 1),
|
||||
'Geräte-Limit': license.get('device_limit', 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',
|
||||
'Test-Lizenz': 'Ja' if license.get('is_test', False) else 'Nein'
|
||||
})
|
||||
|
||||
df = pd.DataFrame(export_data)
|
||||
|
||||
# Create Excel file in memory
|
||||
output = BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False, sheet_name='Batch-Lizenzen')
|
||||
|
||||
# Auto-adjust column widths
|
||||
worksheet = writer.sheets['Batch-Lizenzen']
|
||||
for idx, col in enumerate(df.columns):
|
||||
max_length = max(df[col].astype(str).map(len).max(), len(col)) + 2
|
||||
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||
|
||||
output.seek(0)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"batch_licenses_{timestamp}.xlsx"
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren