Refactor license management and remove unused features

- Removed the JWT authentication setup and related access token handling.
- Deleted the test license activation functionality and associated routes.
- Removed the admin license management interface and related templates.
- Cleaned up the appointments page by removing test license options.
- Updated various templates to reflect changes in terminology from "license" to "instance".
- Removed the verify module and its associated functions for license key management.
- Deleted unused templates related to license management.
- Added a new empty JSON file for licenses backup.
This commit is contained in:
2026-04-18 20:21:33 +02:00
parent d9b812812f
commit 280d25dda9
15 changed files with 45 additions and 1008 deletions
+29
View File
@@ -108,6 +108,27 @@ Berechtigungen für Provisioning:
- Schreibrechte auf /etc/nginx/sites-available und /etc/nginx/sites-enabled - Schreibrechte auf /etc/nginx/sites-available und /etc/nginx/sites-enabled
- nginx -t und nginx reload/signal-Rechte - nginx -t und nginx reload/signal-Rechte
## Admin Homepage: Live-Logs
Die Admin-Seite unter /admin/system zeigt jetzt Live-Logs direkt im Frontend.
Enthaltene Quellen:
- Core Docker Logs (website + mongodb)
- invario-hosts-sync.service
- invario-stack-autostart.service
- nginx.service
Verhalten:
- Auto-Refresh alle 20 Sekunden
- Manuelles Aktualisieren per Button
- Auswahl der Log-Quelle per Dropdown
Hinweis:
- Wenn die Website ohne Zugriff auf systemd/journalctl läuft (z. B. in eingeschränkten Containern), bleiben Core-Docker-Logs verfügbar, während Service-Logs als nicht verfügbar angezeigt werden können.
## Betriebsmodi Website ## Betriebsmodi Website
- Development: [Website/launch_dev.sh](Website/launch_dev.sh) - Development: [Website/launch_dev.sh](Website/launch_dev.sh)
@@ -150,3 +171,11 @@ Optionen:
- --skip-nginx - --skip-nginx
- --skip-hosts-sync - --skip-hosts-sync
- --skip-autostart - --skip-autostart
## Quick Verify
```bash
sudo ./gitea.sh status
sudo systemctl status invario-hosts-sync --no-pager
sudo systemctl status invario-stack-autostart --no-pager
```
-75
View File
@@ -1,75 +0,0 @@
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)
+2 -376
View File
@@ -1,4 +1,3 @@
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, send_file, after_this_request from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session, send_file, after_this_request
import os import os
import json import json
@@ -21,20 +20,14 @@ 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
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4s" app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4s"
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY", app.secret_key)
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=12)
app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Strict" app.config["SESSION_COOKIE_SAMESITE"] = "Strict"
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("SESSION_COOKIE_SECURE", "0") == "1" app.config["SESSION_COOKIE_SECURE"] = os.environ.get("SESSION_COOKIE_SECURE", "0") == "1"
app.config["PREFERRED_URL_SCHEME"] = "https" if os.environ.get("SESSION_COOKIE_SECURE") == "1" else "http" app.config["PREFERRED_URL_SCHEME"] = "https" if os.environ.get("SESSION_COOKIE_SECURE") == "1" else "http"
jwt = JWTManager(app)
@app.after_request @app.after_request
@@ -97,10 +90,6 @@ class _NoopMongoClientHandle:
return None return None
def _issue_access_token() -> str:
return create_access_token(identity="license-validation-client")
def _utc_now_iso() -> str: def _utc_now_iso() -> str:
return datetime.utcnow().isoformat(timespec="seconds") + "Z" return datetime.utcnow().isoformat(timespec="seconds") + "Z"
@@ -1429,53 +1418,6 @@ def _get_instance_for_user(username: str, display_name: str) -> dict | None:
client.close() client.close()
def _activate_test_license_for_user(username: str, school_name: str, package_name: str = "Normal") -> tuple[bool, str]:
"""Create a one-time test license for the user if none exists yet."""
user_name = _sanitize_text(username or "", 80)
school = _sanitize_text(school_name or "", 200)
if not user_name:
return False, "Benutzername fehlt für Test-Key."
client = None
try:
client, col = _get_collection("licenses")
normalized_package = _sanitize_text(package_name or "Normal", 40)
if normalized_package not in {"Normal", "Pro", "Bücherei", "Bücherei"}:
normalized_package = "Normal"
existing = col.find_one(
{
"username": user_name,
"status": {"$in": ["Aktiv", "Pausiert"]},
"plan": {"$regex": "^Test"},
}
)
if existing:
return False, "Ein Test-Key ist für dieses Schulkonto bereits vorhanden."
test_key = f"TEST-{verify.key_generator()[:18].upper()}"
col.insert_one(
{
"username": user_name,
"school_name": school or user_name,
"license_key": test_key,
"plan": f"Test {normalized_package}",
"status": "Aktiv",
"valid_until": (datetime.utcnow() + timedelta(days=30)).date().isoformat(),
"hwid_uuid": "",
"created_at": _utc_now_iso(),
"updated_at": _utc_now_iso(),
}
)
return True, test_key
except PyMongoError:
return False, "Test-Key konnte nicht aktiviert werden."
finally:
if client:
client.close()
def _with_public_id(doc: dict | None) -> dict | None: def _with_public_id(doc: dict | None) -> dict | None:
if not doc: if not doc:
return doc return doc
@@ -1726,10 +1668,9 @@ def login():
session['username'] = stored_user.get("username") session['username'] = stored_user.get("username")
session['display_name'] = stored_user.get("display_name") or stored_user.get("username") session['display_name'] = stored_user.get("display_name") or stored_user.get("username")
session['is_admin'] = stored_user.get("is_admin", False) session['is_admin'] = stored_user.get("is_admin", False)
session['access_token'] = _issue_access_token()
if request.is_json: if request.is_json:
return jsonify({"access_token": session['access_token'], "token_type": "Bearer"}), 200 return jsonify({"status": "ok"}), 200
return redirect(url_for('default')) return redirect(url_for('default'))
@@ -1748,7 +1689,6 @@ def register():
school_name = (request.form.get("school_name") or "").strip() school_name = (request.form.get("school_name") or "").strip()
contact_person = (request.form.get("contact_person") or "").strip() contact_person = (request.form.get("contact_person") or "").strip()
email = (request.form.get("email") or "").strip().lower() email = (request.form.get("email") or "").strip().lower()
activate_test_key = (request.form.get("activate_test_key") or "").strip().lower() in {"1", "on", "true", "yes"}
password = request.form.get("password") or "" password = request.form.get("password") or ""
password_repeat = request.form.get("password_repeat") or "" password_repeat = request.form.get("password_repeat") or ""
@@ -1787,13 +1727,6 @@ def register():
flash("Benutzer konnte nicht erstellt werden.", "error") flash("Benutzer konnte nicht erstellt werden.", "error")
return redirect(url_for("register")) return redirect(url_for("register"))
if activate_test_key:
activated, message = _activate_test_license_for_user(username, school_name)
if activated:
flash(f"Test-Key aktiviert: {message}", "success")
else:
flash(message, "error")
if is_first_user: if is_first_user:
user_store.make_admin(username) user_store.make_admin(username)
except Exception: except Exception:
@@ -1809,16 +1742,6 @@ def register():
@app.route('/appointments', methods=['GET']) @app.route('/appointments', methods=['GET'])
def appointments(): def appointments():
software_packages = [ software_packages = [
{
"slug": "test",
"name": "Testversion",
"headline": "30 Tage kostenlos zum Ausprobieren",
"features": [
"Sofortiger Zugang mit Test-Key",
"Ideal für Erstbewertung im Schulalltag",
"Ohne langfristige Bindung",
],
},
{ {
"slug": "normal", "slug": "normal",
"name": "Normal", "name": "Normal",
@@ -1834,7 +1757,7 @@ def appointments():
"name": "Pro", "name": "Pro",
"headline": "Für Schulen mit erweitertem Bedarf", "headline": "Für Schulen mit erweitertem Bedarf",
"features": [ "features": [
"Erweiterte Lizenzverwaltung", "Erweiterte Instanzverwaltung",
"Detailberichte und Admin-Insights", "Detailberichte und Admin-Insights",
"Priorisierter Support", "Priorisierter Support",
], ],
@@ -1851,58 +1774,12 @@ def appointments():
}, },
] ]
active_test_license = None
if session.get("username"):
client = None
try:
client, col = _get_collection("licenses")
active_test_license = col.find_one(
{
"username": session.get("username"),
"status": {"$in": ["Aktiv", "Pausiert"]},
"plan": {"$regex": "^Test"},
},
{"_id": 0},
)
except PyMongoError:
active_test_license = None
finally:
if client:
client.close()
return render_template( return render_template(
"appointments.html", "appointments.html",
software_packages=software_packages, software_packages=software_packages,
active_test_license=active_test_license,
) )
@app.route('/appointments/start-test', methods=['POST'])
@login_required
def start_test_package():
package_raw = _sanitize_text(request.form.get("package") or "normal", 40).lower()
package_map = {
"test": "Normal",
"normal": "Normal",
"pro": "Pro",
"buecherei": "Bücherei",
}
selected_package = package_map.get(package_raw)
if not selected_package:
flash("Ungültiges Paket ausgewählt.", "error")
return redirect(url_for("appointments"))
school_name = _sanitize_text(session.get("display_name") or session.get("username") or "", 200)
activated, message = _activate_test_license_for_user(session.get("username") or "", school_name, selected_package)
if activated:
flash(f"Testversion für Paket {selected_package} gestartet. Key: {message}", "success")
else:
flash(message, "error")
return redirect(url_for("appointments"))
@app.route('/appointments/book-option', methods=['POST']) @app.route('/appointments/book-option', methods=['POST'])
@login_required @login_required
def book_option_package(): def book_option_package():
@@ -2643,29 +2520,6 @@ def blog_post(post_id):
return render_template("blog_post.html", post=post) return render_template("blog_post.html", post=post)
@app.route('/my/licenses', methods=['GET', 'POST'])
@login_required
def my_licenses():
if request.method == 'POST':
flash("Weitergabe von Lizenzen ist deaktiviert.", "error")
return redirect(url_for("my_licenses"))
licenses = []
client = None
try:
client, col = _get_collection("licenses")
licenses = list(col.find({"username": session.get("username")}).sort("created_at", -1))
for item in licenses:
item["id"] = str(item.get("_id"))
except PyMongoError:
flash("Lizenzdaten konnten nicht geladen werden.", "error")
finally:
if client:
client.close()
return render_template("my_licenses.html", licenses=licenses)
@app.route('/my/invoices') @app.route('/my/invoices')
@login_required @login_required
def my_invoices(): def my_invoices():
@@ -3056,86 +2910,6 @@ def admin_tickets():
return render_template("admin_tickets.html", tickets=tickets) return render_template("admin_tickets.html", tickets=tickets)
@app.route('/admin/licenses', methods=['GET', 'POST'])
@admin_required
def admin_licenses():
client = None
if request.method == 'POST':
action = _sanitize_text(request.form.get("action") or "", 50)
license_id = _sanitize_text(request.form.get("license_id") or "", 64)
try:
client, col = _get_collection("licenses")
if action == "create":
username = _sanitize_text(request.form.get("username") or "", 80)
school_name = _sanitize_text(request.form.get("school_name") or "", 200)
license_key = _sanitize_text(request.form.get("license_key") or "", 120)
plan = _sanitize_text(request.form.get("plan") or "Standard", 80)
status = _sanitize_text(request.form.get("status") or "Aktiv", 40)
valid_until = _sanitize_text(request.form.get("valid_until") or "", 40)
if not username or not school_name:
flash("Bitte Benutzername und Schule angeben.", "error")
return redirect(url_for("admin_licenses"))
col.insert_one(
{
"username": username,
"school_name": school_name,
"license_key": license_key or f"LIC-{int(datetime.utcnow().timestamp())}-{username[:3].upper()}",
"plan": plan,
"status": status,
"valid_until": valid_until or "2027-12-31",
"created_at": _utc_now_iso(),
}
)
flash("Lizenz angelegt.", "success")
elif action == "update" and license_id:
col.update_one(
{"_id": ObjectId(license_id)},
{
"$set": {
"school_name": _sanitize_text(request.form.get("school_name") or "", 200),
"license_key": _sanitize_text(request.form.get("license_key") or "", 120),
"plan": _sanitize_text(request.form.get("plan") or "Standard", 80),
"status": _sanitize_text(request.form.get("status") or "Aktiv", 40),
"valid_until": _sanitize_text(request.form.get("valid_until") or "", 40),
}
},
)
flash("Lizenz aktualisiert.", "success")
elif action == "delete" and license_id:
col.delete_one({"_id": ObjectId(license_id)})
flash("Lizenz gelöscht.", "success")
else:
flash("Ungültige Aktion.", "error")
except Exception:
flash("Lizenzverwaltung fehlgeschlagen.", "error")
finally:
if client:
client.close()
return redirect(url_for("admin_licenses"))
licenses = []
try:
client, col = _get_collection("licenses")
licenses = list(col.find().sort("created_at", -1))
for item in licenses:
item["id"] = str(item.get("_id"))
except PyMongoError:
flash("Lizenzen konnten nicht geladen werden.", "error")
finally:
if client:
client.close()
users = _list_users_for_admin()
return render_template("admin_licenses.html", licenses=licenses, users=users)
@app.route('/admin/invoices', methods=['GET', 'POST']) @app.route('/admin/invoices', methods=['GET', 'POST'])
@admin_required @admin_required
def admin_invoices(): def admin_invoices():
@@ -3250,159 +3024,11 @@ 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 auswählen.", "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():
session.pop('username', None) session.pop('username', None)
session.pop('display_name', None) session.pop('display_name', None)
session.pop('is_admin', None) session.pop('is_admin', None)
session.pop('access_token', None)
flash('Logged out successfully', 'info') flash('Logged out successfully', 'info')
return redirect(url_for('login')) return redirect(url_for('login'))
-72
View File
@@ -1,72 +0,0 @@
{% extends "base.html" %}
{% block title %}Admin | Lizenzverwaltung{% endblock %}
{% block content %}
<section class="panel">
<h1>Lizenzverwaltung</h1>
<p>Lizenzen für Nutzer anlegen, bearbeiten und löschen.</p>
</section>
<section class="layout">
<form method="post" class="form-card">
<h3>Neue Lizenz</h3>
<input type="hidden" name="action" value="create">
<select name="username" required>
<option value="">Benutzer auswählen</option>
{% for user in users %}
<option value="{{ user.username }}">{{ user.username }} ({{ user.display_name }})</option>
{% endfor %}
</select>
<input type="text" name="school_name" placeholder="Schulname" required>
<input type="text" name="license_key" placeholder="Lizenz-Key (optional, eigener Wert möglich)">
<input type="text" name="plan" placeholder="Plan (z. B. Standard)">
<input type="text" name="status" placeholder="Status (Aktiv, Pausiert)">
<input type="text" name="valid_until" placeholder="Gültig bis (YYYY-MM-DD)">
<button type="submit">Lizenz anlegen</button>
</form>
<div class="list">
{% for item in licenses %}
<article class="entry">
<h3>{{ item.username }} - {{ item.school_name }}</h3>
<p><strong>Key:</strong> {{ item.license_key }} | <strong>Plan:</strong> {{ item.plan }}</p>
<p><strong>Status:</strong> {{ item.status }} | <strong>Gültig bis:</strong> {{ item.valid_until }}</p>
{% if item.transferred_at %}
<p><strong>Weitergegeben:</strong> {{ item.transferred_at }}</p>
{% endif %}
<form method="post" class="inline-form">
<input type="hidden" name="action" value="update">
<input type="hidden" name="license_id" value="{{ item.id }}">
<input type="text" name="school_name" value="{{ item.school_name }}" placeholder="Schulname">
<input type="text" name="license_key" value="{{ item.license_key }}" placeholder="Lizenz-Key">
<input type="text" name="plan" value="{{ item.plan }}" placeholder="Plan">
<input type="text" name="status" value="{{ item.status }}" placeholder="Status">
<input type="text" name="valid_until" value="{{ item.valid_until }}" placeholder="Gültig bis">
<button type="submit">Aktualisieren</button>
</form>
<form method="post">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="license_id" value="{{ item.id }}">
<button type="submit" class="danger">Löschen</button>
</form>
</article>
{% else %}
<article class="entry"><p>Noch keine Lizenzen vorhanden.</p></article>
{% endfor %}
</div>
</section>
<style>
.panel { margin-bottom: 1rem; }
.layout { display: grid; grid-template-columns: 320px 1fr; gap: 1rem; }
.form-card, .entry { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.form-card { display: grid; gap: 0.6rem; align-self: start; }
.list { display: grid; gap: 0.8rem; }
.inline-form { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 0.45rem; margin-top: 0.6rem; }
input, select { width: 100%; border: 1px solid #c9d8e3; border-radius: 10px; padding: 0.52rem; font: inherit; background: #fff; }
button { border: 1px solid #0a4c74; background: linear-gradient(120deg, #0c5a86 0%, #08486c 100%); color: #fff; padding: 0.45rem 0.8rem; border-radius: 999px; font-weight: 700; }
button.danger { border-color: #d04a49; background: #fff; color: #922e2e; margin-top: 0.55rem; }
@media (max-width: 980px) { .layout { grid-template-columns: 1fr; } .inline-form { grid-template-columns: 1fr; } }
</style>
{% endblock %}
-9
View File
@@ -123,15 +123,6 @@
</form> </form>
<a class="action-link" href="{{ url_for('admin_download_core_logs') }}">Core-Logs herunterladen</a> <a class="action-link" href="{{ url_for('admin_download_core_logs') }}">Core-Logs herunterladen</a>
</div> </div>
<h2 style="margin-top: 1rem;">Backup-Export / Einspielen</h2>
<div class="actions">
<a class="action-link" href="{{ url_for('download_backup') }}">Lizenz-Backup exportieren</a>
<form method="post" action="{{ url_for('upload_backup') }}" enctype="multipart/form-data" class="inline-restore-form">
<input type="file" name="file" accept=".json,application/json" required>
<button type="submit">Lizenz-Backup einspielen</button>
</form>
</div>
</section> </section>
<section class="panel logs-panel"> <section class="panel logs-panel">
+8 -23
View File
@@ -103,7 +103,7 @@
{% block content %} {% block content %}
<section class="packages-hero"> <section class="packages-hero">
<h1>Buchungsoptionen als Softwarepakete</h1> <h1>Buchungsoptionen als Softwarepakete</h1>
<p>Alle Pakete sind frei einsehbar, auch ohne Admin. Die Testversion kann nur mit aktivem Konto gestartet werden.</p> <p>Alle Pakete sind frei einsehbar, auch ohne Admin.</p>
<p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschützten Sessions und nachvollziehbaren Aktionen.</p> <p>Jedes Paket basiert auf derselben Sicherheitsarchitektur mit Rollenrechten, geschützten Sessions und nachvollziehbaren Aktionen.</p>
</section> </section>
@@ -119,23 +119,12 @@
</ul> </ul>
<div class="package-actions"> <div class="package-actions">
{% if 'username' in session %} {% if 'username' in session %}
{% if package.slug == 'test' %} <form method="POST" action="{{ url_for('book_option_package') }}">
<form method="POST" action="{{ url_for('start_test_package') }}"> <input type="hidden" name="package" value="{{ package.slug }}">
<input type="hidden" name="package" value="{{ package.slug }}"> <button class="btn" type="submit">Option buchen</button>
<button class="btn" type="submit">Testversion starten</button> </form>
</form>
{% else %}
<form method="POST" action="{{ url_for('book_option_package') }}">
<input type="hidden" name="package" value="{{ package.slug }}">
<button class="btn" type="submit">Option buchen</button>
</form>
{% endif %}
{% else %} {% else %}
{% if package.slug == 'test' %} <a class="btn secondary" href="{{ url_for('login') }}">Mit Konto buchen</a>
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto testen</a>
{% else %}
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto buchen</a>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</article> </article>
@@ -144,13 +133,9 @@
<section class="notice"> <section class="notice">
{% if 'username' in session %} {% if 'username' in session %}
{% if active_test_license %} <p><strong>Hinweis:</strong> Buchungsanfragen werden direkt vom Admin-Team verarbeitet und Ihrer Instanz zugeordnet.</p>
<p><strong>Aktive Testversion:</strong> {{ active_test_license.plan }} | Key: {{ active_test_license.license_key }} | Gültig bis: {{ active_test_license.valid_until }}</p>
{% else %}
<p><strong>Hinweis:</strong> Für Ihr Konto ist noch keine Testversion aktiv. Starten Sie oben die Testversion beim Paket Testversion.</p>
{% endif %}
{% else %} {% else %}
<p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um die Testversion zu starten.</p> <p><strong>Hinweis:</strong> Bitte einloggen oder registrieren, um Buchungsoptionen anzufragen.</p>
{% endif %} {% endif %}
</section> </section>
{% endblock %} {% endblock %}
-3
View File
@@ -383,7 +383,6 @@
<details class="nav-dropdown"> <details class="nav-dropdown">
<summary class="nav-btn">Nutzerbereich</summary> <summary class="nav-btn">Nutzerbereich</summary>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href="{{ url_for('my_licenses') }}">Meine Lizenzen</a>
<a href="{{ url_for('my_invoices') }}">Meine Rechnungen</a> <a href="{{ url_for('my_invoices') }}">Meine Rechnungen</a>
<a href="{{ url_for('my_instance_management') }}">Meine Instanz</a> <a href="{{ url_for('my_instance_management') }}">Meine Instanz</a>
<a href="{{ url_for('user_chat') }}">Chat mit Admin</a> <a href="{{ url_for('user_chat') }}">Chat mit Admin</a>
@@ -397,8 +396,6 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
<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_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>
+2 -2
View File
@@ -14,8 +14,8 @@
<p>Alle Geräte, Räume und Zuständigkeiten an einem Ort. Das verkürzt Suchzeiten und reduziert Rückfragen.</p> <p>Alle Geräte, Räume und Zuständigkeiten an einem Ort. Das verkürzt Suchzeiten und reduziert Rückfragen.</p>
</article> </article>
<article class="card"> <article class="card">
<h2>Lizenz- und Fristenkontrolle</h2> <h2>Instanz- und Fristenkontrolle</h2>
<p>Lizenzlaufzeiten werden transparent angezeigt, inklusive Hinweise auf bald ablaufende Nutzungen.</p> <p>Instanzzustände und wichtige Wartungsfristen werden transparent angezeigt, inklusive klarer Hinweise für den Betrieb.</p>
</article> </article>
<article class="card"> <article class="card">
<h2>Support mit klaren Prioritäten</h2> <h2>Support mit klaren Prioritäten</h2>
+1 -1
View File
@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<section class="page-hero"> <section class="page-hero">
<h1>Inventarsystem für Schulen</h1> <h1>Inventarsystem für Schulen</h1>
<p>Alle wichtigen Funktionen für den Schulalltag in einer klaren Oberfläche: von Inventar über Lizenzen bis zur nachvollziehbaren Historie.</p> <p>Alle wichtigen Funktionen für den Schulalltag in einer klaren Oberfläche: von Inventar bis zur nachvollziehbaren Historie.</p>
</section> </section>
<section class="split"> <section class="split">
-268
View File
@@ -1,268 +0,0 @@
{% 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 auswählen</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 auswählen</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 für User {{ item.user_id }} wirklich löschen?');">
<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 %}
-36
View File
@@ -1,36 +0,0 @@
{% extends "base.html" %}
{% block title %}Meine Lizenzen{% endblock %}
{% block content %}
<section class="panel">
<h1>Meine Lizenzen</h1>
<p>Hier sehen Sie Ihre aktiven und abgelaufenen Lizenzinformationen.</p>
</section>
<section class="grid">
{% if licenses %}
{% for item in licenses %}
<article class="card">
<h3>{{ item.plan }} Lizenz</h3>
<p><strong>Schule:</strong> {{ item.school_name }}</p>
<p><strong>Lizenzschlüssel:</strong> {{ item.license_key }}</p>
<p><strong>Status:</strong> {{ item.status }}</p>
<p><strong>Gültig bis:</strong> {{ item.valid_until }}</p>
</article>
{% endfor %}
{% else %}
<article class="card">
<h3>Keine Lizenzen vorhanden</h3>
<p>Aktuell sind noch keine Lizenzen hinterlegt.</p>
</article>
{% endif %}
</section>
<style>
.panel { margin-bottom: 1rem; }
.grid { display: grid; gap: 0.8rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.card { background: #fff; border: 1px solid #d8e1e8; border-radius: 14px; padding: 1rem; }
.card p { margin-top: 0.35rem; }
</style>
{% endblock %}
+1 -1
View File
@@ -22,7 +22,7 @@
<h2>Beispiel 2: Verbundschule mit mehreren Standorten</h2> <h2>Beispiel 2: Verbundschule mit mehreren Standorten</h2>
<ul> <ul>
<li><strong>Ausgangslage:</strong> Unterschiedliche Abläufe je Standort, keine gemeinsame Fristenübersicht.</li> <li><strong>Ausgangslage:</strong> Unterschiedliche Abläufe je Standort, keine gemeinsame Fristenübersicht.</li>
<li><strong>Umsetzung:</strong> Einheitliche Prozesse für Inventar, Lizenzen und Supporttickets.</li> <li><strong>Umsetzung:</strong> Einheitliche Prozesse für Inventar, Instanzen und Supporttickets.</li>
<li><strong>Ergebnis:</strong> Standortübergreifende Transparenz und bessere Planbarkeit in der Verwaltung.</li> <li><strong>Ergebnis:</strong> Standortübergreifende Transparenz und bessere Planbarkeit in der Verwaltung.</li>
</ul> </ul>
</article> </article>
+1 -5
View File
@@ -91,7 +91,7 @@
{% block content %} {% block content %}
<section class="auth-wrap"> <section class="auth-wrap">
<h1>Schulregistrierung</h1> <h1>Schulregistrierung</h1>
<p>Registrieren Sie Ihre Schule, um Buchungen, Lizenzen und Support zentral zu verwalten.</p> <p>Registrieren Sie Ihre Schule, um Buchungen, Instanzen und Support zentral zu verwalten.</p>
<form method="POST" action="{{ url_for('register') }}"> <form method="POST" action="{{ url_for('register') }}">
<div class="field"> <div class="field">
<label for="school_name">Name der Schule</label> <label for="school_name">Name der Schule</label>
@@ -117,10 +117,6 @@
<label for="password_repeat">Passwort wiederholen</label> <label for="password_repeat">Passwort wiederholen</label>
<input id="password_repeat" name="password_repeat" type="password" autocomplete="new-password" required> <input id="password_repeat" name="password_repeat" type="password" autocomplete="new-password" required>
</div> </div>
<label class="checkbox-line" for="activate_test_key">
<input id="activate_test_key" name="activate_test_key" type="checkbox" value="1">
<span>Direkt bei Registrierung einen Test-Key aktivieren</span>
</label>
<button class="auth-btn" type="submit">Konto erstellen</button> <button class="auth-btn" type="submit">Konto erstellen</button>
</form> </form>
<p class="auth-meta">Bereits registriert? <a href="{{ url_for('login') }}">Zum Login</a></p> <p class="auth-meta">Bereits registriert? <a href="{{ url_for('login') }}">Zum Login</a></p>
-136
View File
@@ -1,136 +0,0 @@
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)