Dieser Commit ist enthalten in:
2025-06-07 23:13:06 +02:00
Ursprung 0379391736
Commit b20a3216fb
8 geänderte Dateien mit 506 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -963,4 +963,69 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
- ✅ Frontend mit Generate-Button und JavaScript - ✅ Frontend mit Generate-Button und JavaScript
- ✅ Validierung und Fehlerbehandlung - ✅ Validierung und Fehlerbehandlung
- ✅ Audit-Log-Integration - ✅ Audit-Log-Integration
- ✅ Form-Action-Bug behoben - ✅ Form-Action-Bug behoben
### 2025-06-07 - Batch-Lizenzgenerierung implementiert
- Mehrere Lizenzen auf einmal für einen Kunden erstellen
**Implementierte Features:**
1. **Batch-Formular (/batch):**
- Kunde und E-Mail eingeben
- Anzahl der Lizenzen (1-100)
- Lizenztyp (Vollversion/Testversion)
- Gültigkeitszeitraum für alle Lizenzen
- Vorschau-Modal zeigt Key-Format
- Standard-Datum-Einstellungen (heute + 1 Jahr)
2. **Backend-Verarbeitung:**
- Route `/batch` für GET (Formular) und POST (Generierung)
- Generiert die angegebene Anzahl eindeutiger Keys
- Speichert alle in einer Transaktion
- Kunde wird automatisch angelegt (falls nicht vorhanden)
- ON CONFLICT für existierende Kunden
- Audit-Log-Eintrag mit CREATE_BATCH Aktion
3. **Ergebnis-Seite:**
- Zeigt alle generierten Lizenzen in Tabellenform
- Kundeninformationen und Gültigkeitszeitraum
- Einzelne Keys können kopiert werden (📋 Button)
- Alle Keys auf einmal kopieren
- Druckfunktion für physische Ausgabe
- Link zur Lizenzübersicht mit Kundenfilter
4. **Export-Funktionalität:**
- Route `/batch/export` für CSV-Download
- Speichert Batch-Daten in Session für Export
- CSV mit UTF-8 BOM für Excel-Kompatibilität
- Enthält Kundeninfo, Generierungsdatum und alle Keys
- Format: Nr;Lizenzschlüssel;Typ
- Dateiname: batch_licenses_KUNDE_TIMESTAMP.csv
5. **Integration:**
- Batch-Button in Navigation (Dashboard, Einzellizenz-Seite)
- CREATE_BATCH Aktion im Audit-Log (Farbe: #6610f2)
- Session-basierte Export-Daten
- Flash-Messages für Feedback
**Sicherheit:**
- Limit von 100 Lizenzen pro Batch
- Login-Required für alle Routen
- Transaktionale Datenbank-Operationen
- Validierung der Eingaben
**Beispiel-Workflow:**
1. Admin geht zu `/batch`
2. Gibt Kunde "Firma GmbH", Anzahl "25", Typ "Vollversion" ein
3. System generiert 25 eindeutige Keys
4. Ergebnis-Seite zeigt alle Keys
5. Admin kann CSV exportieren oder Keys kopieren
6. Kunde erhält die Lizenzen
**Status:**
- ✅ Batch-Formular vollständig implementiert
- ✅ Backend-Generierung mit Transaktionen
- ✅ Export als CSV
- ✅ Copy-to-Clipboard Funktionalität
- ✅ Audit-Log-Integration
- ✅ Navigation aktualisiert

Datei anzeigen

@@ -2,4 +2,4 @@
# https://curl.se/docs/http-cookies.html # https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk. # This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1749329677 admin_session DJ5-gm8DCBYcqZyLqo7pYzvq-FoFBRAYkvWPn37aAo4 #HttpOnly_localhost FALSE / FALSE 1749330711 admin_session kL1GAl-qrgTLBwfFpeQLngAfFne2ehrnKbWE3M3awqE

Datei anzeigen

@@ -981,6 +981,140 @@ def create_license():
return render_template("index.html", username=session.get('username')) return render_template("index.html", username=session.get('username'))
@app.route("/batch", methods=["GET", "POST"])
@login_required
def batch_licenses():
"""Batch-Generierung mehrerer Lizenzen für einen Kunden"""
if request.method == "POST":
# Formulardaten
name = request.form["customer_name"]
email = request.form["email"]
license_type = request.form["license_type"]
quantity = int(request.form["quantity"])
valid_from = request.form["valid_from"]
valid_until = request.form["valid_until"]
# Sicherheitslimit
if quantity < 1 or quantity > 100:
flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
return redirect(url_for('batch_licenses'))
conn = get_connection()
cur = conn.cursor()
try:
# Kunde einfügen (falls nicht vorhanden)
cur.execute("""
INSERT INTO customers (name, email, created_at)
VALUES (%s, %s, NOW())
ON CONFLICT (name, email) DO UPDATE SET name=EXCLUDED.name
RETURNING id
""", (name, email))
customer_id = cur.fetchone()[0]
# Lizenzen generieren und speichern
generated_licenses = []
for i in range(quantity):
# Eindeutigen Key generieren
attempts = 0
while attempts < 10:
license_key = generate_license_key(license_type)
cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
if not cur.fetchone():
break
attempts += 1
# Lizenz einfügen
cur.execute("""
INSERT INTO licenses (license_key, customer_id, license_type,
valid_from, valid_until, is_active, created_at)
VALUES (%s, %s, %s, %s, %s, true, NOW())
RETURNING id
""", (license_key, customer_id, license_type, valid_from, valid_until))
license_id = cur.fetchone()[0]
generated_licenses.append({
'id': license_id,
'key': license_key,
'type': license_type
})
conn.commit()
# Audit-Log
log_audit('CREATE_BATCH', 'license',
new_values={'customer': name, 'quantity': quantity, 'type': license_type},
additional_info=f"Batch-Generierung von {quantity} Lizenzen")
# Session für Export speichern
session['batch_export'] = {
'customer': name,
'email': email,
'licenses': generated_licenses,
'valid_from': valid_from,
'valid_until': valid_until,
'timestamp': datetime.now().isoformat()
}
flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
return render_template("batch_result.html",
customer=name,
email=email,
licenses=generated_licenses,
valid_from=valid_from,
valid_until=valid_until)
except Exception as e:
conn.rollback()
logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
flash('Fehler bei der Batch-Generierung!', 'error')
return redirect(url_for('batch_licenses'))
finally:
cur.close()
conn.close()
# GET Request
return render_template("batch_form.html")
@app.route("/batch/export")
@login_required
def export_batch():
"""Exportiert die zuletzt generierten Batch-Lizenzen"""
batch_data = session.get('batch_export')
if not batch_data:
flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
return redirect(url_for('batch_licenses'))
# CSV generieren
output = io.StringIO()
output.write('\ufeff') # UTF-8 BOM für Excel
# Header
output.write(f"Kunde: {batch_data['customer']}\n")
output.write(f"E-Mail: {batch_data['email']}\n")
output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
output.write("\n")
output.write("Nr;Lizenzschlüssel;Typ\n")
# Lizenzen
for i, license in enumerate(batch_data['licenses'], 1):
typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
output.write(f"{i};{license['key']};{typ_text}\n")
output.seek(0)
# Audit-Log
log_audit('EXPORT', 'batch_licenses',
additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
return send_file(
io.BytesIO(output.getvalue().encode('utf-8-sig')),
mimetype='text/csv',
as_attachment=True,
download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
@app.route("/licenses") @app.route("/licenses")
@login_required @login_required
def licenses(): def licenses():

Datei anzeigen

@@ -27,6 +27,7 @@
.action-AUTO_LOGOUT { color: #fd7e14; } .action-AUTO_LOGOUT { color: #fd7e14; }
.action-EXPORT { color: #ffc107; } .action-EXPORT { color: #ffc107; }
.action-GENERATE_KEY { color: #20c997; } .action-GENERATE_KEY { color: #20c997; }
.action-CREATE_BATCH { color: #6610f2; }
</style> </style>
{% endblock %} {% endblock %}
@@ -65,6 +66,7 @@
<option value="AUTO_LOGOUT" {% if filter_action == 'AUTO_LOGOUT' %}selected{% endif %}>⏰ Auto-Logout</option> <option value="AUTO_LOGOUT" {% if filter_action == 'AUTO_LOGOUT' %}selected{% endif %}>⏰ Auto-Logout</option>
<option value="EXPORT" {% if filter_action == 'EXPORT' %}selected{% endif %}>📥 Export</option> <option value="EXPORT" {% if filter_action == 'EXPORT' %}selected{% endif %}>📥 Export</option>
<option value="GENERATE_KEY" {% if filter_action == 'GENERATE_KEY' %}selected{% endif %}>🔑 Key generiert</option> <option value="GENERATE_KEY" {% if filter_action == 'GENERATE_KEY' %}selected{% endif %}>🔑 Key generiert</option>
<option value="CREATE_BATCH" {% if filter_action == 'CREATE_BATCH' %}selected{% endif %}>🔑 Batch erstellt</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@@ -116,6 +118,7 @@
{% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout {% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout
{% elif log[3] == 'EXPORT' %}📥 Export {% elif log[3] == 'EXPORT' %}📥 Export
{% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert {% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert
{% elif log[3] == 'CREATE_BATCH' %}🔑 Batch erstellt
{% else %}{{ log[3] }} {% else %}{{ log[3] }}
{% endif %} {% endif %}
</span> </span>

Datei anzeigen

@@ -0,0 +1,143 @@
{% extends "base.html" %}
{% block title %}Batch-Lizenzen erstellen{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>🔑 Batch-Lizenzen erstellen</h2>
<div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/create" class="btn btn-secondary"> Einzellizenz</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
</div>
</div>
<div class="alert alert-info">
<strong> Batch-Generierung:</strong> Erstellen Sie mehrere Lizenzen auf einmal für einen Kunden.
Die Lizenzen werden automatisch generiert und können anschließend als CSV exportiert werden.
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" action="/batch" accept-charset="UTF-8">
<div class="row g-3">
<div class="col-md-6">
<label for="customerName" class="form-label">Kundenname</label>
<input type="text" class="form-control" id="customerName" name="customer_name"
placeholder="Firma GmbH" accept-charset="UTF-8" required>
</div>
<div class="col-md-6">
<label for="email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="email" name="email"
placeholder="kontakt@firma.de" accept-charset="UTF-8">
</div>
<div class="col-md-3">
<label for="quantity" class="form-label">Anzahl Lizenzen</label>
<input type="number" class="form-control" id="quantity" name="quantity"
min="1" max="100" value="10" required>
<div class="form-text">Max. 100 Lizenzen pro Batch</div>
</div>
<div class="col-md-3">
<label for="licenseType" class="form-label">Lizenztyp</label>
<select class="form-select" id="licenseType" name="license_type" required>
<option value="full">Vollversion</option>
<option value="test">Testversion</option>
</select>
</div>
<div class="col-md-3">
<label for="validFrom" class="form-label">Gültig ab</label>
<input type="date" class="form-control" id="validFrom" name="valid_from" required>
</div>
<div class="col-md-3">
<label for="validUntil" class="form-label">Gültig bis</label>
<input type="date" class="form-control" id="validUntil" name="valid_until" required>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-lg">
🔑 Batch generieren
</button>
<button type="button" class="btn btn-outline-secondary" onclick="previewKeys()">
👁️ Vorschau
</button>
</div>
</form>
<!-- Vorschau Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Vorschau der generierten Keys</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Es werden <strong id="previewQuantity">10</strong> Lizenzen im folgenden Format generiert:</p>
<div class="bg-light p-3 rounded font-monospace" id="previewFormat">
AF-YYYYMMFT-XXXX-YYYY-ZZZZ
</div>
<p class="mt-3 mb-0">Die Keys werden automatisch eindeutig generiert und in der Datenbank gespeichert.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Setze heutiges Datum als Standard
document.addEventListener('DOMContentLoaded', function() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('validFrom').value = today;
// Setze valid_until auf 1 Jahr später
const oneYearLater = new Date();
oneYearLater.setFullYear(oneYearLater.getFullYear() + 1);
document.getElementById('validUntil').value = oneYearLater.toISOString().split('T')[0];
});
// Vorschau-Funktion
function previewKeys() {
const quantity = document.getElementById('quantity').value;
const type = document.getElementById('licenseType').value;
const typeChar = type === 'full' ? 'F' : 'T';
const date = new Date();
const dateStr = date.getFullYear() + ('0' + (date.getMonth() + 1)).slice(-2);
document.getElementById('previewQuantity').textContent = quantity;
document.getElementById('previewFormat').textContent = `AF-${dateStr}${typeChar}-XXXX-YYYY-ZZZZ`;
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
}
// Validierung
document.getElementById('quantity').addEventListener('input', function(e) {
if (e.target.value > 100) {
e.target.value = 100;
}
if (e.target.value < 1) {
e.target.value = 1;
}
});
</script>
{% endblock %}

Datei anzeigen

@@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}Batch-Lizenzen generiert{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>✅ Batch-Lizenzen erfolgreich generiert</h2>
<div>
<a href="/batch" class="btn btn-primary">🔑 Weitere Batch erstellen</a>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
</div>
</div>
<div class="alert alert-success">
<h5 class="alert-heading">🎉 {{ licenses|length }} Lizenzen wurden erfolgreich generiert!</h5>
<p class="mb-0">Die Lizenzen wurden in der Datenbank gespeichert und dem Kunden zugeordnet.</p>
</div>
<!-- Kunden-Info -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📋 Kundeninformationen</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Kunde:</strong> {{ customer }}</p>
<p><strong>E-Mail:</strong> {{ email or 'Nicht angegeben' }}</p>
</div>
<div class="col-md-6">
<p><strong>Gültig von:</strong> {{ valid_from }}</p>
<p><strong>Gültig bis:</strong> {{ valid_until }}</p>
</div>
</div>
</div>
</div>
<!-- Export-Optionen -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📥 Export-Optionen</h5>
</div>
<div class="card-body">
<p>Exportieren Sie die generierten Lizenzen für den Kunden:</p>
<div class="d-flex gap-2">
<a href="/batch/export" class="btn btn-success">
📄 Als CSV exportieren
</a>
<button class="btn btn-outline-primary" onclick="copyAllKeys()">
📋 Alle Keys kopieren
</button>
<button class="btn btn-outline-secondary" onclick="window.print()">
🖨️ Drucken
</button>
</div>
</div>
</div>
<!-- Generierte Lizenzen -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">🔑 Generierte Lizenzen</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th width="50">#</th>
<th>Lizenzschlüssel</th>
<th width="120">Typ</th>
<th width="100">Aktionen</th>
</tr>
</thead>
<tbody>
{% for license in licenses %}
<tr>
<td>{{ loop.index }}</td>
<td class="font-monospace">{{ license.key }}</td>
<td>
{% if license.type == 'full' %}
<span class="badge bg-success">Vollversion</span>
{% else %}
<span class="badge bg-info">Testversion</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-outline-secondary"
onclick="copyKey('{{ license.key }}')"
title="Kopieren">
📋
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Hinweis -->
<div class="alert alert-info mt-4">
<strong>💡 Tipp:</strong> Die generierten Lizenzen sind sofort aktiv und können verwendet werden.
Sie finden alle Lizenzen auch in der <a href="/licenses?search={{ customer }}">Lizenzübersicht</a>.
</div>
</div>
<!-- Textarea für Kopieren (unsichtbar) -->
<textarea id="copyArea" style="position: absolute; left: -9999px;"></textarea>
<style>
@media print {
.btn, .alert-info, .card-header h5 { display: none !important; }
.card { border: 1px solid #000 !important; }
}
</style>
<script>
// Einzelnen Key kopieren
function copyKey(key) {
navigator.clipboard.writeText(key).then(function() {
// Visuelles Feedback
event.target.textContent = '✓';
setTimeout(() => {
event.target.textContent = '📋';
}, 2000);
});
}
// Alle Keys kopieren
function copyAllKeys() {
const keys = [];
{% for license in licenses %}
keys.push('{{ license.key }}');
{% endfor %}
const text = keys.join('\n');
const textarea = document.getElementById('copyArea');
textarea.value = text;
textarea.select();
try {
document.execCommand('copy');
// Visuelles Feedback
event.target.textContent = '✓ Kopiert!';
setTimeout(() => {
event.target.textContent = '📋 Alle Keys kopieren';
}, 2000);
} catch (err) {
// Fallback für moderne Browser
navigator.clipboard.writeText(text);
}
}
</script>
{% endblock %}

Datei anzeigen

@@ -22,6 +22,7 @@
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div> <div>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a> <a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/batch" class="btn btn-primary">🔑 Batch-Lizenzen</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a> <a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a> <a href="/customers" class="btn btn-secondary">👥 Kunden</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a> <a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>

Datei anzeigen

@@ -8,6 +8,7 @@
<h2>Neue Lizenz erstellen</h2> <h2>Neue Lizenz erstellen</h2>
<div> <div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a> <a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/batch" class="btn btn-primary">🔑 Batch</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a> <a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a> <a href="/customers" class="btn btn-secondary">👥 Kunden</a>
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a> <a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>