Add license key management features and backup functionality

- Updated base template to include a link for license key management.
- Created a new template for license key management with forms for generating and allocating keys, as well as backup options.
- Implemented backup.py for loading and saving licenses to MongoDB, including export and import functionalities.
- Added verify.py for license key verification, user ID generation, and key registration.
This commit is contained in:
2026-03-26 22:58:06 +01:00
parent b43cb43961
commit f00d12660c
6 changed files with 631 additions and 1014 deletions
+75
View File
@@ -0,0 +1,75 @@
import os
from datetime import datetime
from pymongo import MongoClient
from pymongo.errors import PyMongoError
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
LICENSE_COLLECTION = "licenses"
def _get_collection():
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
return client, client[MONGO_DB_NAME][LICENSE_COLLECTION]
def load_licenses() -> dict:
"""Load all licenses from MongoDB and return them as a list."""
client = None
try:
client, col = _get_collection()
return list(col.find({}, {"_id": 0}).sort("created_at", 1))
except PyMongoError:
return []
finally:
if client:
client.close()
def save_licenses(data: dict) -> bool:
"""Replace MongoDB licenses with provided list data."""
if not isinstance(data, list):
return False
client = None
try:
client, col = _get_collection()
col.delete_many({})
prepared = []
for item in data:
if not isinstance(item, dict):
continue
license_key = str(item.get("license_key", "")).strip()
user_id = str(item.get("user_id", "")).strip()
if not license_key:
continue
prepared.append(
{
"user_id": user_id,
"license_key": license_key,
"hwid_uuid": str(item.get("hwid_uuid", "")).strip(),
"created_at": item.get("created_at") or datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
)
if prepared:
col.insert_many(prepared)
return True
except PyMongoError:
return False
finally:
if client:
client.close()
def export_backup() -> dict:
"""Export current licenses content from MongoDB."""
return load_licenses()
def import_backup(data: dict) -> bool:
"""Import and restore licenses in MongoDB from backup data."""
return save_licenses(data)
+151 -1
View File
@@ -1,9 +1,11 @@
from flask_jwt_extended import JWTManager, create_access_token, jwt_required from flask_jwt_extended import JWTManager, create_access_token, jwt_required
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session, send_file
import os import os
import json
import calendar import calendar
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
from functools import wraps from functools import wraps
from io import BytesIO
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import bleach import bleach
@@ -11,6 +13,8 @@ from markupsafe import escape
from pymongo import MongoClient from pymongo import MongoClient
from pymongo.errors import PyMongoError from pymongo.errors import PyMongoError
from bson.objectid import ObjectId from bson.objectid import ObjectId
import verify
import backup
import user as user_store import user as user_store
@@ -1211,6 +1215,152 @@ def impressum():
def nutzungsbedingungen(): def nutzungsbedingungen():
return render_template("nutzungsbedingungen.html") return render_template("nutzungsbedingungen.html")
@app.route("/admin/lizenz_key")
@app.route("/admin/license-management")
@admin_required
def admin_license_keys():
if 'username' not in session:
return redirect(url_for('login'))
keys = verify.load_file()
users = _list_users_for_admin()
return render_template("lizenz-managment.html", keys=keys, users=users)
@app.route("/admin/generate_new", methods=["POST"])
@admin_required
def generate_new():
if 'username' not in session:
return redirect(url_for('login'))
new_license = verify.new_key()
if not new_license:
flash("License generation failed (database unavailable).", "error")
return redirect(url_for('admin_license_keys'))
flash(f"New key generated: {new_license}", "success")
return redirect(url_for('admin_license_keys'))
@app.route("/admin/allocate_key", methods=["POST"])
@admin_required
def allocate_key():
if 'username' not in session:
return redirect(url_for('login'))
username = _sanitize_text(request.form.get("username") or "", 80)
license_key = _sanitize_text(request.form.get("license_key") or "", 160)
if not username or not license_key:
flash("Bitte Benutzer und Lizenz-Key auswaehlen.", "error")
return redirect(url_for('admin_license_keys'))
if not _find_user(username):
flash("Ausgewaehlter Benutzer wurde nicht gefunden.", "error")
return redirect(url_for('admin_license_keys'))
client = None
try:
client, col = _get_collection("licenses")
result = col.update_one(
{"license_key": license_key},
{
"$set": {
"username": username,
"updated_at": _utc_now_iso(),
}
},
)
if result.matched_count == 0:
flash("Lizenz-Key nicht gefunden.", "error")
return redirect(url_for('admin_license_keys'))
except PyMongoError:
flash("Key-Zuweisung fehlgeschlagen.", "error")
return redirect(url_for('admin_license_keys'))
finally:
if client:
client.close()
flash("Lizenz-Key wurde erfolgreich zugewiesen.", "success")
return redirect(url_for('admin_license_keys'))
@app.route("/admin/remove_key/<user_id>", methods=["POST"])
@admin_required
def remove_key(user_id):
if 'username' not in session:
return redirect(url_for('login'))
if verify.remove_key(user_id):
flash(f"Key for user {user_id} removed", "success")
else:
flash(f"No key found for user {user_id}", "error")
return redirect(url_for('admin_license_keys'))
@app.route('/admin/download_backup', methods=['GET'])
@admin_required
def download_backup():
if 'username' not in session:
return redirect(url_for('login'))
licenses_data = backup.export_backup()
json_str = json.dumps(licenses_data, indent=2)
return send_file(
BytesIO(json_str.encode('utf-8')),
mimetype='application/json',
as_attachment=True,
download_name='licenses_backup.json'
)
@app.route('/admin/upload_backup', methods=['POST'])
@admin_required
def upload_backup():
if 'username' not in session:
return redirect(url_for('login'))
if 'file' not in request.files:
flash('No file provided', 'error')
return redirect(url_for('admin_license_keys'))
file = request.files['file']
if file.filename == '':
flash('No file selected', 'error')
return redirect(url_for('admin_license_keys'))
try:
content = file.read().decode('utf-8')
data = json.loads(content)
if backup.import_backup(data):
flash('Licenses backup restored successfully', 'success')
else:
flash('Failed to restore backup - invalid format', 'error')
except json.JSONDecodeError:
flash('Invalid JSON file', 'error')
except Exception as e:
flash(f'Error uploading backup: {str(e)}', 'error')
return redirect(url_for('admin_license_keys'))
@app.route("/validate__information", methods=['POST'])
def validate__information():
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No JSON data provided"}), 400
license_key = data.get("license")
hwid_uuid = data.get("hwid")
if not license_key or not hwid_uuid:
return jsonify({"error": "Missing 'license' or 'hwid' in JSON data"}), 400
if verify.check(license_key, hwid_uuid):
return jsonify({"status": "ok"}), 200
else:
return jsonify({"status": "invalid"}), 402
@app.route('/logout') @app.route('/logout')
def logout(): def logout():
File diff suppressed because it is too large Load Diff
+1
View File
@@ -380,6 +380,7 @@
<a href="{{ url_for('admin_dashboard') }}">Dashboard</a> <a href="{{ url_for('admin_dashboard') }}">Dashboard</a>
<a href="{{ url_for('admin_users') }}">Userverwaltung</a> <a href="{{ url_for('admin_users') }}">Userverwaltung</a>
<a href="{{ url_for('admin_licenses') }}">Lizenzverwaltung</a> <a href="{{ url_for('admin_licenses') }}">Lizenzverwaltung</a>
<a href="{{ url_for('admin_license_keys') }}">Lizenz-Key Verwaltung</a>
<a href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a> <a href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a>
<a href="{{ url_for('admin_chats') }}">Admin-Chat</a> <a href="{{ url_for('admin_chats') }}">Admin-Chat</a>
<a href="{{ url_for('admin_tickets') }}">Support-Center</a> <a href="{{ url_for('admin_tickets') }}">Support-Center</a>
+268
View File
@@ -0,0 +1,268 @@
{% extends "base.html" %}
{% block title %}Admin | Lizenz-Key Verwaltung{% endblock %}
{% block head %}
{{ super() }}
<style>
.key-header {
margin-bottom: 1rem;
}
.key-header p {
margin-top: 0.4rem;
max-width: 70ch;
}
.key-layout {
display: grid;
grid-template-columns: 360px 1fr;
gap: 1rem;
}
.key-card {
background: #ffffff;
border: 1px solid #d4e0e9;
border-radius: 14px;
padding: 1rem;
box-shadow: 0 10px 28px rgba(20, 40, 55, 0.05);
}
.key-card h3 {
font-size: 1.02rem;
color: #143a55;
margin-bottom: 0.65rem;
}
.key-form {
display: grid;
gap: 0.55rem;
}
.key-form label {
font-size: 0.86rem;
font-weight: 700;
color: #3f5f74;
}
.key-form select,
.key-form input,
.key-form button {
width: 100%;
font: inherit;
border-radius: 10px;
padding: 0.52rem 0.62rem;
}
.key-form select,
.key-form input {
border: 1px solid #c7d7e2;
background: #ffffff;
color: #17394f;
}
.btn-main,
.btn-alt,
.btn-danger {
border: none;
cursor: pointer;
font-weight: 700;
}
.btn-main {
color: #ffffff;
background: linear-gradient(120deg, #0b5b89 0%, #0b4262 100%);
}
.btn-alt {
color: #0f3d59;
border: 1px solid #bfd2df;
background: #f8fbfd;
}
.btn-danger {
color: #7e2d2d;
border: 1px solid #e8b8b8;
background: #fff3f3;
}
.key-actions {
display: grid;
gap: 0.5rem;
}
.key-actions a {
text-align: center;
}
.key-table-wrap {
overflow-x: auto;
}
.key-table {
width: 100%;
border-collapse: collapse;
}
.key-table th,
.key-table td {
border-bottom: 1px solid #e0e8ef;
text-align: left;
padding: 0.68rem 0.5rem;
vertical-align: middle;
}
.key-table th {
font-size: 0.82rem;
color: #4b697d;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.key-table td {
color: #17384f;
font-size: 0.95rem;
}
.code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.85rem;
background: #f2f6fa;
border: 1px solid #d9e4ed;
border-radius: 8px;
padding: 0.2rem 0.35rem;
word-break: break-all;
}
.badge {
display: inline-block;
border-radius: 999px;
padding: 0.2rem 0.5rem;
font-size: 0.78rem;
font-weight: 700;
}
.badge.assigned {
border: 1px solid #9fd2bf;
color: #14684a;
background: #edfaf4;
}
.badge.unassigned {
border: 1px solid #ebd08c;
color: #8a6113;
background: #fff8e8;
}
.remove-form {
display: inline;
}
@media (max-width: 1024px) {
.key-layout {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<section class="key-header">
<h1>Lizenz-Key Verwaltung</h1>
<p>Generieren, zuweisen und sichern Sie Lizenz-Keys im gleichen Stil wie der restliche Admin-Bereich.</p>
</section>
<section class="key-layout">
<aside class="key-card">
<h3>Key erstellen</h3>
<form method="POST" action="{{ url_for('generate_new') }}" class="key-form">
<button type="submit" class="btn-main">Neuen Key generieren</button>
</form>
<h3 style="margin-top: 1rem;">Key zuweisen</h3>
<form method="POST" action="{{ url_for('allocate_key') }}" class="key-form">
<label for="alloc_user">Benutzer</label>
<select id="alloc_user" name="username" required>
<option value="">Benutzer auswaehlen</option>
{% for user in users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<label for="alloc_key">Lizenz-Key</label>
<select id="alloc_key" name="license_key" required>
<option value="">Key auswaehlen</option>
{% for item in keys %}
<option value="{{ item.license_key }}">{{ item.user_id or '----' }} | {{ item.license_key[:22] }}{% if item.license_key|length > 22 %}...{% endif %}</option>
{% endfor %}
</select>
<button type="submit" class="btn-main">Key zuweisen</button>
</form>
<h3 style="margin-top: 1rem;">Backup</h3>
<div class="key-actions">
<a href="{{ url_for('download_backup') }}" class="btn-alt">Backup herunterladen</a>
<form method="POST" action="{{ url_for('upload_backup') }}" enctype="multipart/form-data" class="key-form">
<input type="file" name="file" accept=".json" required>
<button type="submit" class="btn-danger" onclick="return confirm('Backup wirklich einspielen? Aktuelle Keys werden ersetzt.');">Backup einspielen</button>
</form>
</div>
</aside>
<article class="key-card">
<h3>Alle Keys</h3>
<div class="key-table-wrap">
<table class="key-table">
<thead>
<tr>
<th>User ID</th>
<th>Lizenz-Key</th>
<th>Benutzer</th>
<th>HWID</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for item in keys %}
<tr>
<td>{{ item.user_id or '-' }}</td>
<td><span class="code">{{ item.license_key }}</span></td>
<td>{{ item.username or '-' }}</td>
<td>
{% if item.hwid_uuid %}
<span class="code">{{ item.hwid_uuid }}</span>
{% else %}
-
{% endif %}
</td>
<td>
{% if item.username %}
<span class="badge assigned">Zugewiesen</span>
{% else %}
<span class="badge unassigned">Nicht zugewiesen</span>
{% endif %}
</td>
<td>
{% if item.user_id %}
<form method="POST" action="{{ url_for('remove_key', user_id=item.user_id) }}" class="remove-form" onsubmit="return confirm('Key fuer User {{ item.user_id }} wirklich loeschen?');">
<button type="submit" class="btn-danger">Entfernen</button>
</form>
{% else %}
-
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6">Keine Lizenz-Keys gefunden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</section>
{% endblock %}
+136
View File
@@ -0,0 +1,136 @@
import os
import secrets
from pymongo import MongoClient
from pymongo.errors import PyMongoError
from datetime import datetime
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
INVOICE_UPLOAD_DIR = os.path.join(BASE_DIR, "static", "uploads", "invoices")
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Invario_Website")
LICENSE_COLLECTION = "licenses"
def _get_collection():
client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
return client, client[MONGO_DB_NAME][LICENSE_COLLECTION]
def _next_user_id(col) -> str:
existing_ids = []
for item in col.find({}, {"user_id": 1, "_id": 0}):
user_id = str(item.get("user_id", "")).strip()
if user_id.isdigit():
existing_ids.append(int(user_id))
next_id = (max(existing_ids) + 1) if existing_ids else 1
return f"{next_id:04d}"
def load_file() -> list:
client = None
try:
client, col = _get_collection()
docs = list(col.find({}, {"_id": 0}).sort("created_at", 1))
return docs
except PyMongoError:
return []
finally:
if client:
client.close()
def check(license_key, hwid_uuid) -> bool:
client = None
try:
client, col = _get_collection()
doc = col.find_one({"license_key": str(license_key)})
if not doc:
return False
current_hwid = str(doc.get("hwid_uuid", ""))
if current_hwid == str(hwid_uuid):
return True
if current_hwid == "":
col.update_one(
{"_id": doc.get("_id")},
{
"$set": {
"hwid_uuid": str(hwid_uuid),
"updated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
},
)
return True
return False
except PyMongoError:
return False
finally:
if client:
client.close()
def register_new_Key(data_stream):
if not isinstance(data_stream, list):
return False
client = None
try:
client, col = _get_collection()
col.delete_many({})
prepared = []
for item in data_stream:
if not isinstance(item, dict):
continue
prepared.append(
{
"user_id": str(item.get("user_id", "")).strip(),
"license_key": str(item.get("license_key", "")).strip(),
"hwid_uuid": str(item.get("hwid_uuid", "")).strip(),
"created_at": item.get("created_at") or datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
)
if prepared:
col.insert_many(prepared)
return True
except PyMongoError:
return False
finally:
if client:
client.close()
def new_key() -> str:
generated_key = key_generator()
client = None
try:
client, col = _get_collection()
col.insert_one(
{
"user_id": _next_user_id(col),
"license_key": generated_key,
"hwid_uuid": "",
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
}
)
except PyMongoError:
return ""
finally:
if client:
client.close()
return generated_key
def remove_key(user_id: str) -> bool:
client = None
try:
client, col = _get_collection()
result = col.delete_one({"user_id": str(user_id)})
return result.deleted_count > 0
except PyMongoError:
return False
finally:
if client:
client.close()
def key_generator():
return secrets.token_urlsafe(32)