Initial commit
Dieser Commit ist enthalten in:
506
v2_adminpanel/routes/license_routes.py
Normale Datei
506
v2_adminpanel/routes/license_routes.py
Normale Datei
@ -0,0 +1,506 @@
|
||||
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():
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get filter parameters
|
||||
search = request.args.get('search', '').strip()
|
||||
data_source = request.args.get('data_source', 'real') # real, fake, all
|
||||
license_type = request.args.get('license_type', '') # '', full, test
|
||||
license_status = request.args.get('license_status', '') # '', active, expiring, expired, inactive
|
||||
sort = request.args.get('sort', 'created_at')
|
||||
order = request.args.get('order', 'desc')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 50
|
||||
|
||||
# Get licenses based on data source
|
||||
if data_source == 'fake':
|
||||
licenses_list = get_licenses(show_fake=True)
|
||||
licenses_list = [l for l in licenses_list if l.get('is_fake')]
|
||||
elif data_source == 'all':
|
||||
licenses_list = get_licenses(show_fake=True)
|
||||
else: # real
|
||||
licenses_list = get_licenses(show_fake=False)
|
||||
|
||||
# Type filtering
|
||||
if license_type:
|
||||
if license_type == 'full':
|
||||
licenses_list = [l for l in licenses_list if l.get('license_type') == 'full']
|
||||
elif license_type == 'test':
|
||||
licenses_list = [l for l in licenses_list if l.get('license_type') == 'test']
|
||||
|
||||
# Status filtering
|
||||
if license_status:
|
||||
now = datetime.now().date()
|
||||
filtered_licenses = []
|
||||
|
||||
for license in licenses_list:
|
||||
if license_status == 'active' and license.get('is_active'):
|
||||
# Active means is_active=true, regardless of expiration date
|
||||
filtered_licenses.append(license)
|
||||
elif license_status == 'expired' and license.get('valid_until') and license.get('valid_until') <= now:
|
||||
# Expired means past valid_until date, regardless of is_active
|
||||
filtered_licenses.append(license)
|
||||
elif license_status == 'inactive' and not license.get('is_active'):
|
||||
# Inactive means is_active=false, regardless of date
|
||||
filtered_licenses.append(license)
|
||||
|
||||
licenses_list = filtered_licenses
|
||||
|
||||
# Search filtering
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
licenses_list = [l for l in licenses_list if
|
||||
search_lower in str(l.get('license_key', '')).lower() or
|
||||
search_lower in str(l.get('customer_name', '')).lower() or
|
||||
search_lower in str(l.get('customer_email', '')).lower()]
|
||||
|
||||
# Calculate pagination
|
||||
total = len(licenses_list)
|
||||
total_pages = (total + per_page - 1) // per_page
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
licenses_list = licenses_list[start:end]
|
||||
|
||||
return render_template("licenses.html",
|
||||
licenses=licenses_list,
|
||||
search=search,
|
||||
data_source=data_source,
|
||||
license_type=license_type,
|
||||
license_status=license_status,
|
||||
sort=sort,
|
||||
order=order,
|
||||
page=page,
|
||||
total=total,
|
||||
total_pages=total_pages,
|
||||
per_page=per_page,
|
||||
now=datetime.now,
|
||||
timedelta=timedelta)
|
||||
|
||||
|
||||
@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 = {
|
||||
'license_key': request.form['license_key'],
|
||||
'license_type': request.form['license_type'],
|
||||
'valid_from': request.form['valid_from'],
|
||||
'valid_until': request.form['valid_until'],
|
||||
'is_active': 'is_active' in request.form,
|
||||
'device_limit': int(request.form.get('device_limit', 3))
|
||||
}
|
||||
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
SET license_key = %s, license_type = %s, valid_from = %s,
|
||||
valid_until = %s, is_active = %s, device_limit = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
new_values['license_key'],
|
||||
new_values['license_type'],
|
||||
new_values['valid_from'],
|
||||
new_values['valid_until'],
|
||||
new_values['is_active'],
|
||||
new_values['device_limit'],
|
||||
license_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
log_audit('UPDATE', 'license', license_id,
|
||||
old_values={
|
||||
'license_key': current_license.get('license_key'),
|
||||
'license_type': current_license.get('license_type'),
|
||||
'valid_from': str(current_license.get('valid_from', '')),
|
||||
'valid_until': str(current_license.get('valid_until', '')),
|
||||
'is_active': current_license.get('is_active'),
|
||||
'device_limit': current_license.get('device_limit', 3)
|
||||
},
|
||||
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):
|
||||
# Check for force parameter
|
||||
force_delete = request.form.get('force', 'false').lower() == 'true'
|
||||
|
||||
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'))
|
||||
|
||||
# Safety check: Don't delete active licenses unless forced
|
||||
if license_data.get('is_active') and not force_delete:
|
||||
flash(f'Lizenz {license_data["license_key"]} ist noch aktiv! Bitte deaktivieren Sie die Lizenz zuerst oder nutzen Sie "Erzwungenes Löschen".', 'warning')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
|
||||
# Check for recent activity (heartbeats in last 24 hours)
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM license_heartbeats
|
||||
WHERE license_id = %s
|
||||
AND timestamp > NOW() - INTERVAL '24 hours'
|
||||
""", (license_id,))
|
||||
recent_heartbeats = cur.fetchone()[0]
|
||||
|
||||
if recent_heartbeats > 0 and not force_delete:
|
||||
flash(f'Lizenz {license_data["license_key"]} hatte in den letzten 24 Stunden {recent_heartbeats} Aktivitäten! '
|
||||
f'Die Lizenz wird möglicherweise noch aktiv genutzt. Bitte prüfen Sie dies vor dem Löschen.', 'danger')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
except Exception as e:
|
||||
# If heartbeats table doesn't exist, continue
|
||||
logging.warning(f"Could not check heartbeats: {str(e)}")
|
||||
|
||||
# Check for active devices/activations
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM activations
|
||||
WHERE license_id = %s
|
||||
AND is_active = true
|
||||
""", (license_id,))
|
||||
active_devices = cur.fetchone()[0]
|
||||
|
||||
if active_devices > 0 and not force_delete:
|
||||
flash(f'Lizenz {license_data["license_key"]} hat {active_devices} aktive Geräte! '
|
||||
f'Bitte deaktivieren Sie alle Geräte vor dem Löschen.', 'danger')
|
||||
return redirect(url_for('licenses.licenses'))
|
||||
except Exception as e:
|
||||
# If activations table doesn't exist, continue
|
||||
logging.warning(f"Could not check activations: {str(e)}")
|
||||
|
||||
# Delete from sessions first
|
||||
cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_data['license_key'],))
|
||||
|
||||
# Delete from license_heartbeats if exists
|
||||
try:
|
||||
cur.execute("DELETE FROM license_heartbeats WHERE license_id = %s", (license_id,))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Delete from activations if exists
|
||||
try:
|
||||
cur.execute("DELETE FROM activations WHERE license_id = %s", (license_id,))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Delete the license
|
||||
cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log deletion with force flag
|
||||
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'],
|
||||
'was_active': license_data.get('is_active'),
|
||||
'forced': force_delete
|
||||
},
|
||||
additional_info=f"{'Forced deletion' if force_delete else 'Normal deletion'}")
|
||||
|
||||
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_fake wird später vom Kunden geerbt
|
||||
|
||||
# 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'))
|
||||
|
||||
# Neuer Kunde wird immer als Fake erstellt, da wir in der Testphase sind
|
||||
# TODO: Nach Testphase muss hier die Business-Logik angepasst werden
|
||||
is_fake = True
|
||||
cur.execute("""
|
||||
INSERT INTO customers (name, email, is_fake, created_at)
|
||||
VALUES (%s, %s, %s, NOW())
|
||||
RETURNING id
|
||||
""", (name, email, is_fake))
|
||||
customer_id = cur.fetchone()[0]
|
||||
customer_info = {'name': name, 'email': email, 'is_fake': is_fake}
|
||||
|
||||
# Audit-Log für neuen Kunden
|
||||
log_audit('CREATE', 'customer', customer_id,
|
||||
new_values={'name': name, 'email': email, 'is_fake': is_fake})
|
||||
else:
|
||||
# Bestehender Kunde - hole Infos für Audit-Log
|
||||
cur.execute("SELECT name, email, is_fake 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]}
|
||||
|
||||
# Lizenz erbt immer den is_fake Status vom Kunden
|
||||
is_fake = customer_data[2]
|
||||
|
||||
# Lizenz hinzufügen
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
||||
domain_count, ipv4_count, phone_count, device_limit, is_fake)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
||||
domain_count, ipv4_count, phone_count, device_limit, is_fake))
|
||||
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_fake = %s) as domains,
|
||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_fake = %s) as ipv4s,
|
||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_fake = %s) as phones
|
||||
""", (is_fake, is_fake, is_fake))
|
||||
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_fake = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_fake, 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_fake = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_fake, 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_fake = %s
|
||||
LIMIT %s FOR UPDATE
|
||||
""", (is_fake, 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_fake': is_fake
|
||||
})
|
||||
|
||||
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)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren