diff --git a/JOURNAL.md b/JOURNAL.md index 3200725..4990ec9 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -963,4 +963,69 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier - ✅ Frontend mit Generate-Button und JavaScript - ✅ Validierung und Fehlerbehandlung - ✅ Audit-Log-Integration -- ✅ Form-Action-Bug behoben \ No newline at end of file +- ✅ 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 \ No newline at end of file diff --git a/v2/cookies.txt b/v2/cookies.txt index 4f0d095..5a9d927 100644 --- a/v2/cookies.txt +++ b/v2/cookies.txt @@ -2,4 +2,4 @@ # https://curl.se/docs/http-cookies.html # 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 diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index eee8809..2127cf3 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -981,6 +981,140 @@ def create_license(): 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") @login_required def licenses(): diff --git a/v2_adminpanel/templates/audit_log.html b/v2_adminpanel/templates/audit_log.html index 8bf8a1b..4ad8dd0 100644 --- a/v2_adminpanel/templates/audit_log.html +++ b/v2_adminpanel/templates/audit_log.html @@ -27,6 +27,7 @@ .action-AUTO_LOGOUT { color: #fd7e14; } .action-EXPORT { color: #ffc107; } .action-GENERATE_KEY { color: #20c997; } + .action-CREATE_BATCH { color: #6610f2; } {% endblock %} @@ -65,6 +66,7 @@ +
Die Lizenzen wurden in der Datenbank gespeichert und dem Kunden zugeordnet.
+Kunde: {{ customer }}
+E-Mail: {{ email or 'Nicht angegeben' }}
+Gültig von: {{ valid_from }}
+Gültig bis: {{ valid_until }}
+Exportieren Sie die generierten Lizenzen für den Kunden:
+| # | +Lizenzschlüssel | +Typ | +Aktionen | +
|---|---|---|---|
| {{ loop.index }} | +{{ license.key }} | ++ {% if license.type == 'full' %} + Vollversion + {% else %} + Testversion + {% endif %} + | ++ + | +