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
|
- Code ist vollständig implementiert und getestet
|
||||||
- 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
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-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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren