Lizenzkey Generator
Dieser Commit ist enthalten in:
69
JOURNAL.md
69
JOURNAL.md
@@ -896,4 +896,71 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
||||
- Code ist vollständig implementiert und getestet
|
||||
- CAPTCHA wird nach 2 Fehlversuchen angezeigt
|
||||
- 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
|
||||
@@ -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 1749327638 admin_session hlywIUxTA0lRA4PyUOO2OrC5YNt7-2__FEVhP7H_Jac
|
||||
#HttpOnly_localhost FALSE / FALSE 1749329677 admin_session DJ5-gm8DCBYcqZyLqo7pYzvq-FoFBRAYkvWPn37aAo4
|
||||
|
||||
@@ -18,6 +18,9 @@ import logging
|
||||
import random
|
||||
import hashlib
|
||||
import requests
|
||||
import secrets
|
||||
import string
|
||||
import re
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -548,6 +551,50 @@ def verify_recaptcha(response):
|
||||
logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
|
||||
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"])
|
||||
def login():
|
||||
# Timing-Attack Schutz - Start Zeit merken
|
||||
@@ -672,6 +719,51 @@ def heartbeat():
|
||||
'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("/")
|
||||
@login_required
|
||||
def dashboard():
|
||||
@@ -841,11 +933,16 @@ def create_license():
|
||||
if request.method == "POST":
|
||||
name = request.form["customer_name"]
|
||||
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"]
|
||||
valid_from = request.form["valid_from"]
|
||||
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()
|
||||
cur = conn.cursor()
|
||||
|
||||
|
||||
5
v2_adminpanel/cookies.txt
Normale Datei
5
v2_adminpanel/cookies.txt
Normale Datei
@@ -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
|
||||
@@ -26,6 +26,7 @@
|
||||
.action-LOGOUT { color: #6c757d; }
|
||||
.action-AUTO_LOGOUT { color: #fd7e14; }
|
||||
.action-EXPORT { color: #ffc107; }
|
||||
.action-GENERATE_KEY { color: #20c997; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -63,6 +64,7 @@
|
||||
<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="EXPORT" {% if filter_action == 'EXPORT' %}selected{% endif %}>📥 Export</option>
|
||||
<option value="GENERATE_KEY" {% if filter_action == 'GENERATE_KEY' %}selected{% endif %}>🔑 Key generiert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@@ -113,6 +115,7 @@
|
||||
{% elif log[3] == 'LOGOUT' %}🚪 Abmeldung
|
||||
{% elif log[3] == 'AUTO_LOGOUT' %}⏰ Auto-Logout
|
||||
{% elif log[3] == 'EXPORT' %}📥 Export
|
||||
{% elif log[3] == 'GENERATE_KEY' %}🔑 Key generiert
|
||||
{% else %}{{ log[3] }}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</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="col-md-6">
|
||||
<label for="customerName" class="form-label">Kundenname</label>
|
||||
@@ -28,7 +28,16 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<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 class="col-md-4">
|
||||
<label for="licenseType" class="form-label">Lizenztyp</label>
|
||||
@@ -52,4 +61,99 @@
|
||||
</div>
|
||||
</form>
|
||||
</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 %}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren