Lizenzkey Generator

Dieser Commit ist enthalten in:
2025-06-07 22:58:11 +02:00
Ursprung b0c8e8efc0
Commit 0379391736
6 geänderte Dateien mit 281 neuen und 5 gelöschten Zeilen

Datei anzeigen

@@ -897,3 +897,70 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
- CAPTCHA wird nach 2 Fehlversuchen angezeigt - CAPTCHA wird nach 2 Fehlversuchen angezeigt
- Ohne konfigurierte Keys wird CAPTCHA-Prüfung übersprungen - Ohne konfigurierte Keys wird CAPTCHA-Prüfung übersprungen
- Für Produktion müssen nur die Keys in .env eingetragen werden - Für Produktion müssen nur die Keys in .env eingetragen werden
### 2025-06-07 - License Key Generator implementiert
- Automatische Generierung von Lizenzschlüsseln mit definiertem Format
**Implementiertes Format:**
`AF-YYYYMMFT-XXXX-YYYY-ZZZZ`
- **AF** = Account Factory (feste Produktkennung)
- **YYYY** = Jahr (z.B. 2025)
- **MM** = Monat (z.B. 06)
- **FT** = Lizenztyp (F=Fullversion, T=Testversion)
- **XXXX-YYYY-ZZZZ** = Zufällige alphanumerische Zeichen (ohne verwirrende wie 0/O, 1/I/l)
**Beispiele:**
- Vollversion: `AF-202506F-A7K9-M3P2-X8R4`
- Testversion: `AF-202512T-B2N5-K8L3-Q9W7`
**Implementierte Features:**
1. **Backend-Funktionen (app.py):**
- `generate_license_key()` - Generiert Keys mit kryptografisch sicherem Zufallsgenerator
- `validate_license_key()` - Validiert das Key-Format mit Regex
- Verwendet `secrets` statt `random` für Sicherheit
- Erlaubte Zeichen: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (ohne verwirrende)
2. **API-Endpoint:**
- POST `/api/generate-license-key` - JSON API für Key-Generierung
- Prüft auf Duplikate in der Datenbank (max. 10 Versuche)
- Audit-Log-Eintrag bei jeder Generierung
- Login-Required geschützt
3. **Frontend-Verbesserungen (index.html):**
- Generate-Button neben License Key Input
- Placeholder und Pattern-Attribut für Format-Hinweis
- Auto-Uppercase bei manueller Eingabe
- Visuelles Feedback bei erfolgreicher Generierung
- Format-Hinweis unter dem Eingabefeld
4. **JavaScript-Features:**
- AJAX-basierte Key-Generierung ohne Seiten-Reload
- Automatische Prüfung bei Lizenztyp-Änderung
- Ladeindikator während der Generierung
- Fehlerbehandlung mit Benutzer-Feedback
- Standard-Datum-Einstellungen (heute + 1 Jahr)
5. **Validierung:**
- Server-seitige Format-Validierung beim Speichern
- Flash-Message bei ungültigem Format
- Automatische Großschreibung des Keys
- Pattern-Validierung im HTML-Formular
6. **Weitere Fixes:**
- Form Action von "/" auf "/create" korrigiert
- Flash-Messages mit Bootstrap Toasts implementiert
- GENERATE_KEY Aktion zum Audit-Log hinzugefügt (Farbe: #20c997)
**Technische Details:**
- Keine vorhersagbaren Muster durch `secrets.choice()`
- Datum im Key zeigt Erstellungszeitpunkt
- Lizenztyp direkt im Key erkennbar
- Kollisionsprüfung gegen Datenbank
**Status:**
- ✅ Backend-Generierung vollständig implementiert
- ✅ Frontend mit Generate-Button und JavaScript
- ✅ Validierung und Fehlerbehandlung
- ✅ Audit-Log-Integration
- ✅ Form-Action-Bug behoben

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 1749327638 admin_session hlywIUxTA0lRA4PyUOO2OrC5YNt7-2__FEVhP7H_Jac #HttpOnly_localhost FALSE / FALSE 1749329677 admin_session DJ5-gm8DCBYcqZyLqo7pYzvq-FoFBRAYkvWPn37aAo4

Datei anzeigen

@@ -18,6 +18,9 @@ import logging
import random import random
import hashlib import hashlib
import requests import requests
import secrets
import string
import re
load_dotenv() load_dotenv()
@@ -548,6 +551,50 @@ def verify_recaptcha(response):
logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}") logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
return False return False
def generate_license_key(license_type='full'):
"""
Generiert einen Lizenzschlüssel im Format: AF-YYYYMMFT-XXXX-YYYY-ZZZZ
AF = Account Factory (Produktkennung)
YYYY = Jahr
MM = Monat
FT = F für Fullversion, T für Testversion
XXXX-YYYY-ZZZZ = Zufällige alphanumerische Zeichen
"""
# Erlaubte Zeichen (ohne verwirrende wie 0/O, 1/I/l)
chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
# Datum-Teil
now = datetime.now()
date_part = now.strftime('%Y%m')
type_char = 'F' if license_type == 'full' else 'T'
# Zufällige Teile generieren (3 Blöcke à 4 Zeichen)
parts = []
for _ in range(3):
part = ''.join(secrets.choice(chars) for _ in range(4))
parts.append(part)
# Key zusammensetzen
key = f"AF-{date_part}{type_char}-{parts[0]}-{parts[1]}-{parts[2]}"
return key
def validate_license_key(key):
"""
Validiert das License Key Format
Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ
"""
if not key:
return False
# Pattern für das spezifische Format
# AF- (fest) + 6 Ziffern (YYYYMM) + F oder T + - + 4 Zeichen + - + 4 Zeichen + - + 4 Zeichen
pattern = r'^AF-\d{6}[FT]-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
# Großbuchstaben für Vergleich
return bool(re.match(pattern, key.upper()))
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
# Timing-Attack Schutz - Start Zeit merken # Timing-Attack Schutz - Start Zeit merken
@@ -672,6 +719,51 @@ def heartbeat():
'username': session.get('username') 'username': session.get('username')
}) })
@app.route("/api/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
@app.route("/") @app.route("/")
@login_required @login_required
def dashboard(): def dashboard():
@@ -841,11 +933,16 @@ def create_license():
if request.method == "POST": if request.method == "POST":
name = request.form["customer_name"] name = request.form["customer_name"]
email = request.form["email"] email = request.form["email"]
license_key = request.form["license_key"] license_key = request.form["license_key"].upper() # Immer Großbuchstaben
license_type = request.form["license_type"] license_type = request.form["license_type"]
valid_from = request.form["valid_from"] valid_from = request.form["valid_from"]
valid_until = request.form["valid_until"] valid_until = request.form["valid_until"]
# 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('create_license'))
conn = get_connection() conn = get_connection()
cur = conn.cursor() cur = conn.cursor()

5
v2_adminpanel/cookies.txt Normale Datei
Datei anzeigen

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1749329847 admin_session aojqyq4GcSt5oT7NJPeg7UHPoEZUVkn-s1Kr-EAnJWM

Datei anzeigen

@@ -26,6 +26,7 @@
.action-LOGOUT { color: #6c757d; } .action-LOGOUT { color: #6c757d; }
.action-AUTO_LOGOUT { color: #fd7e14; } .action-AUTO_LOGOUT { color: #fd7e14; }
.action-EXPORT { color: #ffc107; } .action-EXPORT { color: #ffc107; }
.action-GENERATE_KEY { color: #20c997; }
</style> </style>
{% endblock %} {% endblock %}
@@ -63,6 +64,7 @@
<option value="LOGOUT" {% if filter_action == 'LOGOUT' %}selected{% endif %}>🚪 Abmeldung</option> <option value="LOGOUT" {% if filter_action == 'LOGOUT' %}selected{% endif %}>🚪 Abmeldung</option>
<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>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
@@ -113,6 +115,7 @@
{% elif log[3] == 'LOGOUT' %}🚪 Abmeldung {% elif log[3] == 'LOGOUT' %}🚪 Abmeldung
{% 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
{% else %}{{ log[3] }} {% else %}{{ log[3] }}
{% endif %} {% endif %}
</span> </span>

Datei anzeigen

@@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<form method="post" action="/" accept-charset="UTF-8"> <form method="post" action="/create" accept-charset="UTF-8">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label for="customerName" class="form-label">Kundenname</label> <label for="customerName" class="form-label">Kundenname</label>
@@ -28,7 +28,16 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="licenseKey" class="form-label">Lizenzschlüssel</label> <label for="licenseKey" class="form-label">Lizenzschlüssel</label>
<input type="text" class="form-control" id="licenseKey" name="license_key" required> <div class="input-group">
<input type="text" class="form-control" id="licenseKey" name="license_key"
placeholder="AF-YYYYMMFT-XXXX-YYYY-ZZZZ" required
pattern="AF-\d{6}[FT]-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}"
title="Format: AF-YYYYMMFT-XXXX-YYYY-ZZZZ">
<button type="button" class="btn btn-outline-primary" onclick="generateLicenseKey()">
🔑 Generieren
</button>
</div>
<div class="form-text">Format: AF-YYYYMMFT-XXXX-YYYY-ZZZZ (F=Full, T=Test)</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="licenseType" class="form-label">Lizenztyp</label> <label for="licenseType" class="form-label">Lizenztyp</label>
@@ -52,4 +61,99 @@
</div> </div>
</form> </form>
</div> </div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
{% for category, message in messages %}
<div class="toast show align-items-center text-white bg-{{ 'danger' if category == 'error' else 'success' }} border-0" role="alert">
<div class="d-flex">
<div class="toast-body">
{{ message }}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<script>
// License Key Generator
function generateLicenseKey() {
const licenseType = document.getElementById('licenseType').value;
// Zeige Ladeindikator
const button = event.target;
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '⏳ Generiere...';
// API-Call
fetch('/api/generate-license-key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({type: licenseType})
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('licenseKey').value = data.key;
// Visuelles Feedback
document.getElementById('licenseKey').classList.add('border-success');
setTimeout(() => {
document.getElementById('licenseKey').classList.remove('border-success');
}, 2000);
} else {
alert('Fehler bei der Key-Generierung: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Fehler:', error);
alert('Netzwerkfehler bei der Key-Generierung');
})
.finally(() => {
// Button zurücksetzen
button.disabled = false;
button.innerHTML = originalText;
});
}
// Event Listener für Lizenztyp-Änderung
document.getElementById('licenseType').addEventListener('change', function() {
const keyField = document.getElementById('licenseKey');
if (keyField.value && keyField.value.startsWith('AF-')) {
// Prüfe ob der Key zum neuen Typ passt
const currentType = this.value;
const keyType = keyField.value.charAt(9); // Position des F/T im Key
if ((currentType === 'full' && keyType === 'T') ||
(currentType === 'test' && keyType === 'F')) {
if (confirm('Der aktuelle Key passt nicht zum gewählten Lizenztyp. Neuen Key generieren?')) {
generateLicenseKey();
}
}
}
});
// Auto-Uppercase für License Key Input
document.getElementById('licenseKey').addEventListener('input', function(e) {
e.target.value = e.target.value.toUpperCase();
});
// Setze heutiges Datum als Standard für valid_from
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 als Standard
const oneYearLater = new Date();
oneYearLater.setFullYear(oneYearLater.getFullYear() + 1);
document.getElementById('validUntil').value = oneYearLater.toISOString().split('T')[0];
});
</script>
{% endblock %} {% endblock %}