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:
@@ -108,6 +108,27 @@ Berechtigungen für Provisioning:
|
||||
- Schreibrechte auf /etc/nginx/sites-available und /etc/nginx/sites-enabled
|
||||
- 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
|
||||
|
||||
- Development: [Website/launch_dev.sh](Website/launch_dev.sh)
|
||||
@@ -149,4 +170,12 @@ Optionen:
|
||||
- --skip-start
|
||||
- --skip-nginx
|
||||
- --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
|
||||
```
|
||||
@@ -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
@@ -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
|
||||
import os
|
||||
import json
|
||||
@@ -21,20 +20,14 @@ from markupsafe import escape
|
||||
from pymongo import MongoClient
|
||||
from pymongo.errors import PyMongoError
|
||||
from bson.objectid import ObjectId
|
||||
import verify
|
||||
import backup
|
||||
|
||||
import user as user_store
|
||||
|
||||
app = Flask(__name__)
|
||||
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_SAMESITE"] = "Strict"
|
||||
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"
|
||||
jwt = JWTManager(app)
|
||||
|
||||
|
||||
@app.after_request
|
||||
@@ -97,10 +90,6 @@ class _NoopMongoClientHandle:
|
||||
return None
|
||||
|
||||
|
||||
def _issue_access_token() -> str:
|
||||
return create_access_token(identity="license-validation-client")
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
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()
|
||||
|
||||
|
||||
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:
|
||||
if not doc:
|
||||
return doc
|
||||
@@ -1726,10 +1668,9 @@ def login():
|
||||
session['username'] = 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['access_token'] = _issue_access_token()
|
||||
|
||||
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'))
|
||||
|
||||
@@ -1748,7 +1689,6 @@ def register():
|
||||
school_name = (request.form.get("school_name") or "").strip()
|
||||
contact_person = (request.form.get("contact_person") or "").strip()
|
||||
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_repeat = request.form.get("password_repeat") or ""
|
||||
|
||||
@@ -1787,13 +1727,6 @@ def register():
|
||||
flash("Benutzer konnte nicht erstellt werden.", "error")
|
||||
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:
|
||||
user_store.make_admin(username)
|
||||
except Exception:
|
||||
@@ -1809,16 +1742,6 @@ def register():
|
||||
@app.route('/appointments', methods=['GET'])
|
||||
def appointments():
|
||||
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",
|
||||
"name": "Normal",
|
||||
@@ -1834,7 +1757,7 @@ def appointments():
|
||||
"name": "Pro",
|
||||
"headline": "Für Schulen mit erweitertem Bedarf",
|
||||
"features": [
|
||||
"Erweiterte Lizenzverwaltung",
|
||||
"Erweiterte Instanzverwaltung",
|
||||
"Detailberichte und Admin-Insights",
|
||||
"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(
|
||||
"appointments.html",
|
||||
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'])
|
||||
@login_required
|
||||
def book_option_package():
|
||||
@@ -2643,29 +2520,6 @@ def blog_post(post_id):
|
||||
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')
|
||||
@login_required
|
||||
def my_invoices():
|
||||
@@ -3056,86 +2910,6 @@ def admin_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'])
|
||||
@admin_required
|
||||
def admin_invoices():
|
||||
@@ -3250,159 +3024,11 @@ def impressum():
|
||||
def nutzungsbedingungen():
|
||||
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')
|
||||
def logout():
|
||||
session.pop('username', None)
|
||||
session.pop('display_name', None)
|
||||
session.pop('is_admin', None)
|
||||
session.pop('access_token', None)
|
||||
flash('Logged out successfully', 'info')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -123,15 +123,6 @@
|
||||
</form>
|
||||
<a class="action-link" href="{{ url_for('admin_download_core_logs') }}">Core-Logs herunterladen</a>
|
||||
</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 class="panel logs-panel">
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
{% block content %}
|
||||
<section class="packages-hero">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@@ -119,23 +119,12 @@
|
||||
</ul>
|
||||
<div class="package-actions">
|
||||
{% if 'username' in session %}
|
||||
{% if package.slug == 'test' %}
|
||||
<form method="POST" action="{{ url_for('start_test_package') }}">
|
||||
<input type="hidden" name="package" value="{{ package.slug }}">
|
||||
<button class="btn" type="submit">Testversion starten</button>
|
||||
</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 %}
|
||||
<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>
|
||||
{% else %}
|
||||
{% if package.slug == 'test' %}
|
||||
<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 %}
|
||||
<a class="btn secondary" href="{{ url_for('login') }}">Mit Konto buchen</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
@@ -144,13 +133,9 @@
|
||||
|
||||
<section class="notice">
|
||||
{% if 'username' in session %}
|
||||
{% if active_test_license %}
|
||||
<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 %}
|
||||
<p><strong>Hinweis:</strong> Buchungsanfragen werden direkt vom Admin-Team verarbeitet und Ihrer Instanz zugeordnet.</p>
|
||||
{% 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 %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -383,7 +383,6 @@
|
||||
<details class="nav-dropdown">
|
||||
<summary class="nav-btn">Nutzerbereich</summary>
|
||||
<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_instance_management') }}">Meine Instanz</a>
|
||||
<a href="{{ url_for('user_chat') }}">Chat mit Admin</a>
|
||||
@@ -397,8 +396,6 @@
|
||||
<div class="dropdown-menu">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Dashboard</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_chats') }}">Admin-Chat</a>
|
||||
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||
|
||||
@@ -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>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Lizenz- und Fristenkontrolle</h2>
|
||||
<p>Lizenzlaufzeiten werden transparent angezeigt, inklusive Hinweise auf bald ablaufende Nutzungen.</p>
|
||||
<h2>Instanz- und Fristenkontrolle</h2>
|
||||
<p>Instanzzustände und wichtige Wartungsfristen werden transparent angezeigt, inklusive klarer Hinweise für den Betrieb.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>Support mit klaren Prioritäten</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<section class="page-hero">
|
||||
<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 class="split">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -22,7 +22,7 @@
|
||||
<h2>Beispiel 2: Verbundschule mit mehreren Standorten</h2>
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
{% block content %}
|
||||
<section class="auth-wrap">
|
||||
<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') }}">
|
||||
<div class="field">
|
||||
<label for="school_name">Name der Schule</label>
|
||||
@@ -117,10 +117,6 @@
|
||||
<label for="password_repeat">Passwort wiederholen</label>
|
||||
<input id="password_repeat" name="password_repeat" type="password" autocomplete="new-password" required>
|
||||
</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>
|
||||
</form>
|
||||
<p class="auth-meta">Bereits registriert? <a href="{{ url_for('login') }}">Zum Login</a></p>
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user