{{ item.invoice_number }}
+Nutzer: {{ item.username }} | Zeitraum: {{ item.period }}
+Betrag: {{ '%.2f'|format(item.amount_eur) }} EUR | Status: {{ item.status }} | Faellig: {{ item.due_date }}
+ + +diff --git a/.gitignore b/.gitignore
index 169f5d4..2c982bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.venv
__pycache__
Inventarsystem_Lizenz_Verwaltung
-main.dist
\ No newline at end of file
+main.dist
+.mongo-data
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..64f45ae
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "python-envs.pythonProjects": [
+ {
+ "path": "Website/.venv",
+ "envManager": "ms-python.python:venv",
+ "packageManager": "ms-python.python:pip"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Inventarsystem-Lizenz-Verwaltung/run.sh b/Inventarsystem-Lizenz-Verwaltung/run.sh
index 4ec93a3..0b36539 100755
--- a/Inventarsystem-Lizenz-Verwaltung/run.sh
+++ b/Inventarsystem-Lizenz-Verwaltung/run.sh
@@ -40,10 +40,10 @@ if settings_path.is_file():
version_file.write_text(f"{version}\n", encoding="utf-8")
print(f"Prepared {version_file} with version: {version}")
PY
-python -m nuitka --standalone --follow-imports --include-data-dir=templates=templates --include-data-dir=static=static --include-data-file=licenses.json=licenses.json --assume-yes-for-downloads --output-dir=Inventarsystem_Lizenz_Verwaltung --remove-output main.py
-if [[ -x "./Inventarsystem_Lizenz_Verwaltung/main.bin" ]]; then
- ./Inventarsystem_Lizenz_Verwaltung/main.bin
+python -m nuitka --standalone --follow-imports --include-data-dir=templates=templates --include-data-dir=static=static --include-data-file=licenses.json=licenses.json --assume-yes-for-downloads --output-dir=build --remove-output main.py
+if [[ -x "./build/main.bin" ]]; then
+ ./build/main.bin
else
- echo "Build step finished but executable not found at ./Inventarsystem_Lizenz_Verwaltung/main.bin"
+ echo "Build step finished but executable not found at ./build/main.bin"
exit 1
fi
\ No newline at end of file
diff --git a/Website/data/appointments.json b/Website/data/appointments.json
index b9371ef..004293e 100644
--- a/Website/data/appointments.json
+++ b/Website/data/appointments.json
@@ -12,5 +12,33 @@
"response": "Es geht leider nicht um die von ihnen angefragte Uhrzeit, bitte versuchen sie es zwei Tage sp\u00e4ter",
"responded_at": "2026-03-23T16:06:52Z",
"responded_by": "Aiirondev"
+ },
+ {
+ "id": "a-1774287654152",
+ "username": "Aiirondev",
+ "display_name": "Maximilian Gr\u00fcndinger",
+ "date": "2026-03-26",
+ "time": "10:00",
+ "subject": "Test",
+ "note": "Test",
+ "status": "Bestaetigt",
+ "created_at": "2026-03-23T17:40:54Z",
+ "response": "Wunderbar bis dann!",
+ "responded_at": "2026-03-23T17:41:10Z",
+ "responded_by": "Aiirondev"
+ },
+ {
+ "id": "a-1774296722216",
+ "username": "Aiirondev",
+ "display_name": "Maximilian Gr\u00fcndinger",
+ "date": "2026-03-27",
+ "time": "14:30",
+ "subject": "Erst Gespr\u00e4ch f\u00fcr Grunschule ...",
+ "note": "Erste kontakt aufnahme",
+ "status": "Bestaetigt",
+ "created_at": "2026-03-23T20:12:02Z",
+ "response": "Ja passt so, wir kommen vorbei",
+ "responded_at": "2026-03-23T20:12:43Z",
+ "responded_by": "Aiirondev"
}
]
\ No newline at end of file
diff --git a/Website/main.py b/Website/main.py
index 8f905b5..40f2caf 100644
--- a/Website/main.py
+++ b/Website/main.py
@@ -9,6 +9,11 @@ from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
import bleach
from markupsafe import escape
+from pymongo import MongoClient
+from pymongo.errors import PyMongoError
+from bson.objectid import ObjectId
+
+import user as user_store
app = Flask(__name__)
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4s"
@@ -35,12 +40,102 @@ DATA_DIR = BASE_DIR / "data"
USERS_FILE = DATA_DIR / "users.json"
APPOINTMENTS_FILE = DATA_DIR / "appointments.json"
POSTS_FILE = DATA_DIR / "posts.json"
+MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
+MONGO_DB_NAME = os.environ.get("MONGO_DB_NAME", "Inventarsystem")
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"
+
+
+def _get_mongo_client() -> MongoClient:
+ return MongoClient(MONGO_URI, serverSelectionTimeoutMS=1200)
+
+
+def _get_mongo_db():
+ client = _get_mongo_client()
+ return client, client[MONGO_DB_NAME]
+
+
+def _normalize_user_doc(doc: dict | None) -> dict | None:
+ if not doc:
+ return None
+ return {
+ "username": doc.get("Username") or doc.get("username"),
+ "display_name": doc.get("name") or doc.get("display_name") or doc.get("Username"),
+ "is_admin": bool(doc.get("Admin") or doc.get("is_admin", False)),
+ "created_at": doc.get("created_at"),
+ }
+
+
+def _get_collection(name: str):
+ client, db = _get_mongo_db()
+ return client, db[name]
+
+
+def _list_users_for_admin() -> list:
+ docs = user_store.get_all_users() or []
+ users = []
+ for doc in docs:
+ normalized = _normalize_user_doc(doc)
+ if normalized and normalized.get("username"):
+ users.append(normalized)
+ users.sort(key=lambda item: (item.get("is_admin", False), item.get("username", "")), reverse=True)
+ return users
+
+
+def _ensure_user_license(username: str, display_name: str) -> None:
+ client = None
+ try:
+ client, licenses = _get_collection("licenses")
+ if licenses.find_one({"username": username}):
+ return
+ licenses.insert_one(
+ {
+ "username": username,
+ "school_name": f"{display_name} Schule",
+ "license_key": f"LIC-{int(datetime.utcnow().timestamp())}-{username[:3].upper()}",
+ "plan": "Standard",
+ "status": "Aktiv",
+ "valid_until": "2027-12-31",
+ "created_at": _utc_now_iso(),
+ }
+ )
+ except PyMongoError:
+ return
+ finally:
+ if client:
+ client.close()
+
+
+def _ensure_user_invoice(username: str) -> None:
+ client = None
+ try:
+ client, invoices = _get_collection("invoices")
+ if invoices.find_one({"username": username}):
+ return
+ invoices.insert_one(
+ {
+ "username": username,
+ "invoice_number": f"INV-{datetime.utcnow().strftime('%Y%m')}-{username[:3].upper()}",
+ "period": datetime.utcnow().strftime("%m/%Y"),
+ "amount_eur": 79.0,
+ "status": "Offen",
+ "due_date": "2026-12-31",
+ "created_at": _utc_now_iso(),
+ }
+ )
+ except PyMongoError:
+ return
+ finally:
+ if client:
+ client.close()
+
+
def _ensure_data_files() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
if not USERS_FILE.exists():
@@ -93,14 +188,28 @@ def _validate_username(username: str) -> bool:
def _find_user(username: str):
- username_key = (username or "").strip().lower()
+ username_key = (username or "").strip()
if not username_key:
return None
- users = _read_json(USERS_FILE)
- for entry in users:
- if entry.get("username", "").lower() == username_key:
- return entry
- return None
+
+ try:
+ # First try an exact lookup using the original username.
+ raw_user = user_store.get_user(username_key)
+ normalized = _normalize_user_doc(raw_user)
+ if normalized:
+ return normalized
+
+ # Fallback: case-insensitive lookup to avoid role-check failures
+ # when session username casing differs from stored Username casing.
+ all_users = user_store.get_all_users() or []
+ for entry in all_users:
+ candidate = (entry.get("Username") or entry.get("username") or "").strip()
+ if candidate.lower() == username_key.lower():
+ return _normalize_user_doc(entry)
+
+ return None
+ except Exception:
+ return None
def login_required(view_func):
@@ -154,13 +263,20 @@ def login():
flash('Bitte Benutzername und Passwort eingeben.', 'error')
return redirect(url_for('login'))
- stored_user = _find_user(username)
+ try:
+ stored_user_raw = user_store.check_nm_pwd(username, password)
+ except Exception:
+ stored_user_raw = None
- if stored_user and check_password_hash(stored_user.get("password_hash", ""), password):
+ stored_user = _normalize_user_doc(stored_user_raw)
+
+ if stored_user:
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()
+ _ensure_user_license(session['username'], session['display_name'])
+ _ensure_user_invoice(session['username'])
if request.is_json:
return jsonify({"access_token": session['access_token'], "token_type": "Bearer"}), 200
@@ -205,18 +321,18 @@ def register():
display_name = _sanitize_text(display_name, 100)
- users = _read_json(USERS_FILE)
- is_admin = len(users) == 0
- users.append(
- {
- "username": username,
- "display_name": display_name,
- "password_hash": generate_password_hash(password),
- "is_admin": is_admin,
- "created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
- }
- )
- _write_json(USERS_FILE, users)
+ try:
+ existing_users = user_store.get_all_users() or []
+ is_first_user = len(existing_users) == 0
+ if not user_store.add_user(username, password, display_name, ""):
+ flash("Benutzer konnte nicht erstellt werden.", "error")
+ return redirect(url_for("register"))
+ if is_first_user:
+ user_store.make_admin(username)
+ except Exception:
+ flash("MongoDB ist derzeit nicht erreichbar.", "error")
+ return redirect(url_for("register"))
+
flash("Registrierung erfolgreich. Bitte jetzt einloggen.", "success")
return redirect(url_for("login"))
@@ -441,6 +557,419 @@ def blog_post(post_id):
return render_template("blog_post.html", post=post)
+@app.route('/my/licenses')
+@login_required
+def my_licenses():
+ licenses = []
+ client = None
+ try:
+ client, col = _get_collection("licenses")
+ licenses = list(col.find({"username": session.get("username")}, {"_id": 0}).sort("created_at", -1))
+ 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():
+ invoices = []
+ client = None
+ try:
+ client, col = _get_collection("invoices")
+ invoices = list(col.find({"username": session.get("username")}, {"_id": 0}).sort("created_at", -1))
+ except PyMongoError:
+ flash("Rechnungen konnten nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return render_template("my_invoices.html", invoices=invoices)
+
+
+@app.route('/chat', methods=['GET', 'POST'])
+@login_required
+def user_chat():
+ client = None
+ if request.method == 'POST':
+ message = _sanitize_text(request.form.get("message") or "", 3000)
+ if not message:
+ flash("Bitte eine Nachricht eingeben.", "error")
+ return redirect(url_for("user_chat"))
+ try:
+ client, col = _get_collection("chat_messages")
+ col.insert_one(
+ {
+ "username": session.get("username"),
+ "sender": session.get("display_name"),
+ "sender_role": "user",
+ "message": message,
+ "created_at": _utc_now_iso(),
+ }
+ )
+ flash("Nachricht gesendet.", "success")
+ except PyMongoError:
+ flash("Nachricht konnte nicht gesendet werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return redirect(url_for("user_chat"))
+
+ messages = []
+ try:
+ client, col = _get_collection("chat_messages")
+ messages = list(col.find({"username": session.get("username")}, {"_id": 0}).sort("created_at", 1))
+ except PyMongoError:
+ flash("Chat konnte nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return render_template("chat.html", messages=messages)
+
+
+@app.route('/tickets', methods=['GET', 'POST'])
+@login_required
+def user_tickets():
+ client = None
+ if request.method == 'POST':
+ title = _sanitize_text(request.form.get("title") or "", 200)
+ description = _sanitize_text(request.form.get("description") or "", 5000)
+ priority = _sanitize_text(request.form.get("priority") or "Normal", 30)
+ if not title or not description:
+ flash("Bitte Titel und Beschreibung ausfuellen.", "error")
+ return redirect(url_for("user_tickets"))
+ try:
+ client, col = _get_collection("support_tickets")
+ col.insert_one(
+ {
+ "username": session.get("username"),
+ "display_name": session.get("display_name"),
+ "title": title,
+ "description": description,
+ "priority": priority,
+ "status": "Offen",
+ "admin_response": "",
+ "created_at": _utc_now_iso(),
+ "updated_at": _utc_now_iso(),
+ }
+ )
+ flash("Support-Ticket erstellt.", "success")
+ except PyMongoError:
+ flash("Support-Ticket konnte nicht erstellt werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return redirect(url_for("user_tickets"))
+
+ tickets = []
+ try:
+ client, col = _get_collection("support_tickets")
+ tickets = list(col.find({"username": session.get("username")}).sort("created_at", -1))
+ for ticket in tickets:
+ ticket["id"] = str(ticket.get("_id"))
+ except PyMongoError:
+ flash("Tickets konnten nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+
+ return render_template("tickets.html", tickets=tickets)
+
+
+@app.route('/admin/users', methods=['GET', 'POST'])
+@admin_required
+def admin_users():
+ if request.method == 'POST':
+ action = _sanitize_text(request.form.get("action") or "", 50)
+ username = _sanitize_text(request.form.get("username") or "", 80)
+
+ if not username:
+ flash("Bitte Benutzername angeben.", "error")
+ return redirect(url_for("admin_users"))
+
+ if action == "make_admin":
+ user_store.make_admin(username)
+ flash("Benutzer zum Admin gemacht.", "success")
+ elif action == "remove_admin":
+ user_store.remove_admin(username)
+ flash("Admin-Rechte entfernt.", "success")
+ elif action == "delete_user":
+ if username == session.get("username"):
+ flash("Sie koennen Ihr eigenes Konto nicht loeschen.", "error")
+ return redirect(url_for("admin_users"))
+ user_store.delete_user(username)
+ flash("Benutzer geloescht.", "success")
+ else:
+ flash("Ungueltige Aktion.", "error")
+ return redirect(url_for("admin_users"))
+
+ users = _list_users_for_admin()
+ return render_template("admin_users.html", users=users)
+
+
+@app.route('/admin/chats', methods=['GET', 'POST'])
+@admin_required
+def admin_chats():
+ client = None
+ selected_user = _sanitize_text(request.args.get("username") or request.form.get("username") or "", 80)
+
+ if request.method == 'POST':
+ message = _sanitize_text(request.form.get("message") or "", 3000)
+ if not selected_user or not message:
+ flash("Bitte Empfaenger und Nachricht angeben.", "error")
+ return redirect(url_for("admin_chats"))
+ try:
+ client, col = _get_collection("chat_messages")
+ col.insert_one(
+ {
+ "username": selected_user,
+ "sender": session.get("display_name") or session.get("username"),
+ "sender_role": "admin",
+ "message": message,
+ "created_at": _utc_now_iso(),
+ }
+ )
+ flash("Antwort gesendet.", "success")
+ except PyMongoError:
+ flash("Antwort konnte nicht gesendet werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return redirect(url_for("admin_chats", username=selected_user))
+
+ conversations = []
+ messages = []
+ try:
+ client, col = _get_collection("chat_messages")
+ conversations = sorted(col.distinct("username"))
+ if selected_user:
+ messages = list(col.find({"username": selected_user}, {"_id": 0}).sort("created_at", 1))
+ except PyMongoError:
+ flash("Admin-Chat konnte nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+
+ return render_template(
+ "admin_chats.html",
+ conversations=conversations,
+ selected_user=selected_user,
+ messages=messages,
+ )
+
+
+@app.route('/admin/tickets', methods=['GET', 'POST'])
+@admin_required
+def admin_tickets():
+ client = None
+ if request.method == 'POST':
+ ticket_id = _sanitize_text(request.form.get("ticket_id") or "", 64)
+ status = _sanitize_text(request.form.get("status") or "", 40)
+ admin_response = _sanitize_text(request.form.get("admin_response") or "", 5000)
+ if not ticket_id:
+ flash("Ticket-ID fehlt.", "error")
+ return redirect(url_for("admin_tickets"))
+
+ try:
+ client, col = _get_collection("support_tickets")
+ col.update_one(
+ {"_id": ObjectId(ticket_id)},
+ {
+ "$set": {
+ "status": status or "In Bearbeitung",
+ "admin_response": admin_response,
+ "updated_at": _utc_now_iso(),
+ }
+ },
+ )
+ flash("Ticket aktualisiert.", "success")
+ except Exception:
+ flash("Ticket konnte nicht aktualisiert werden.", "error")
+ finally:
+ if client:
+ client.close()
+ return redirect(url_for("admin_tickets"))
+
+ tickets = []
+ try:
+ client, col = _get_collection("support_tickets")
+ tickets = list(col.find().sort("created_at", -1))
+ for ticket in tickets:
+ ticket["id"] = str(ticket.get("_id"))
+ except PyMongoError:
+ flash("Support-Tickets konnten nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+
+ 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)
+ 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": 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": {
+ "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 geloescht.", "success")
+ else:
+ flash("Ungueltige 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()
+
+ return render_template("admin_licenses.html", licenses=licenses)
+
+
+@app.route('/admin/invoices', methods=['GET', 'POST'])
+@admin_required
+def admin_invoices():
+ client = None
+ if request.method == 'POST':
+ action = _sanitize_text(request.form.get("action") or "", 50)
+ invoice_id = _sanitize_text(request.form.get("invoice_id") or "", 64)
+
+ try:
+ client, col = _get_collection("invoices")
+
+ if action == "create":
+ username = _sanitize_text(request.form.get("username") or "", 80)
+ period = _sanitize_text(request.form.get("period") or "", 20)
+ due_date = _sanitize_text(request.form.get("due_date") or "", 20)
+ status = _sanitize_text(request.form.get("status") or "Offen", 40)
+ amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
+
+ try:
+ amount = float(amount_text)
+ except ValueError:
+ amount = 0.0
+
+ if not username or not period:
+ flash("Bitte Benutzername und Zeitraum angeben.", "error")
+ return redirect(url_for("admin_invoices"))
+
+ col.insert_one(
+ {
+ "username": username,
+ "invoice_number": f"INV-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}",
+ "period": period,
+ "amount_eur": amount,
+ "status": status,
+ "due_date": due_date or "2026-12-31",
+ "created_at": _utc_now_iso(),
+ }
+ )
+ flash("Rechnung angelegt.", "success")
+
+ elif action == "update" and invoice_id:
+ amount_text = _sanitize_text(request.form.get("amount_eur") or "0", 20)
+ try:
+ amount = float(amount_text)
+ except ValueError:
+ amount = 0.0
+
+ col.update_one(
+ {"_id": ObjectId(invoice_id)},
+ {
+ "$set": {
+ "status": _sanitize_text(request.form.get("status") or "Offen", 40),
+ "due_date": _sanitize_text(request.form.get("due_date") or "", 20),
+ "amount_eur": amount,
+ }
+ },
+ )
+ flash("Rechnung aktualisiert.", "success")
+
+ elif action == "delete" and invoice_id:
+ col.delete_one({"_id": ObjectId(invoice_id)})
+ flash("Rechnung geloescht.", "success")
+ else:
+ flash("Ungueltige Aktion.", "error")
+ except Exception:
+ flash("Rechnungsverwaltung fehlgeschlagen.", "error")
+ finally:
+ if client:
+ client.close()
+
+ return redirect(url_for("admin_invoices"))
+
+ invoices = []
+ try:
+ client, col = _get_collection("invoices")
+ invoices = list(col.find().sort("created_at", -1))
+ for item in invoices:
+ item["id"] = str(item.get("_id"))
+ except PyMongoError:
+ flash("Rechnungen konnten nicht geladen werden.", "error")
+ finally:
+ if client:
+ client.close()
+
+ return render_template("admin_invoices.html", invoices=invoices)
+
+
@app.route('/datenschutz')
def datenschutz():
return render_template("datenschutz.html")
diff --git a/Website/run.sh b/Website/run.sh
new file mode 100755
index 0000000..e400c8d
--- /dev/null
+++ b/Website/run.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Clean up any existing MongoDB repos to avoid conflicts
+echo "=== Cleaning up existing MongoDB repositories ==="
+sudo rm -f /etc/apt/sources.list.d/mongodb*.list
+sudo apt-key del 7F0CEB10 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 20691EEC35216C63CAF66CE1656408E390CFB1F5 4B7C549A058F8B6B 2069827F925C2E182330D4D4B5BEA7232F5C6971 E162F504A20CDF15827F718D4B7C549A058F8B6B 9DA31620334BD75D9DCB49F368818C72E52529D4 F5679A222C647C87527C2F8CB00A0BD1E2C63C11 2023-02-15 > /dev/null 2>&1 || true
+# Update system packages
+echo "=== Updating system packages ==="
+sudo apt update || { echo "Failed to update package lists"; exit 1; }
+# Add MongoDB repository depending on OS (Ubuntu Server or Linux Mint)
+echo "=== Adding MongoDB repository ==="
+# Detect OS id from /etc/os-release
+OS_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"')
+# Prefer Ubuntu base codename from /etc/os-release when available
+UBUNTU_BASE_CODENAME=$(awk -F= '/^UBUNTU_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
+if [ -z "$UBUNTU_BASE_CODENAME" ]; then
+ UBUNTU_BASE_CODENAME=$(lsb_release -cs 2>/dev/null || awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
+fi
+if [ "$OS_ID" = "linuxmint" ]; then
+ # Map Linux Mint codename to Ubuntu base codename when needed
+ MINT_CODENAME=$(lsb_release -cs 2>/dev/null || awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release | tr -d '"')
+ if [ -z "$UBUNTU_BASE_CODENAME" ] || [ "$UBUNTU_BASE_CODENAME" = "$MINT_CODENAME" ]; then
+ case "$MINT_CODENAME" in
+ xia) UBUNTU_BASE_CODENAME="noble" ;;
+ vanessa|vera|victoria) UBUNTU_BASE_CODENAME="jammy" ;;
+ ulyana|ulyssa|uma|una) UBUNTU_BASE_CODENAME="focal" ;;
+ esac
+ fi
+ echo "Detected Linux Mint ($MINT_CODENAME) → using Ubuntu base '$UBUNTU_BASE_CODENAME'"
+elif [ "$OS_ID" = "ubuntu" ];
+then
+ echo "Detected Ubuntu ($UBUNTU_BASE_CODENAME)"
+else
+ echo "Non-Ubuntu/Mint OS detected ($OS_ID). Skipping MongoDB apt setup."
+ exit 1
+fi
+# Select MongoDB series per Ubuntu base codename
+case "$UBUNTU_BASE_CODENAME" in
+ noble|jammy)
+ MONGO_SERIES="7.0" ;;
+ focal)
+ MONGO_SERIES="6.0" ;;
+ *)
+ echo "Unknown Ubuntu codename '$UBUNTU_BASE_CODENAME', defaulting to 7.0"
+ MONGO_SERIES="7.0" ;;
+esac
+# Use jammy repo path for noble until MongoDB publishes noble (avoid 404)
+MONGO_APT_CODENAME="$UBUNTU_BASE_CODENAME"
+if [ "$UBUNTU_BASE_CODENAME" = "noble" ]; then
+ MONGO_APT_CODENAME="jammy"
+ echo "Using jammy repo path for MongoDB on noble"
+fi
+# Install repo key and list using series and apt codename
+wget -qO - https://www.mongodb.org/static/pgp/server-${MONGO_SERIES}.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-${MONGO_SERIES}.gpg
+echo "deb [signed-by=/usr/share/keyrings/mongodb-server-${MONGO_SERIES}.gpg arch=amd64,arm64] https://repo.mongodb.org/apt/ubuntu ${MONGO_APT_CODENAME}/mongodb-org/${MONGO_SERIES} multiverse" | \
+ sudo tee /etc/apt/sources.list.d/mongodb-org-${MONGO_SERIES}.list
+# Install MongoDB
+sudo apt-get update || exit 1
+sudo apt-get install -y mongodb-org || exit 1
+
+if [[ -n "${CONDA_DEFAULT_ENV:-}" ]] && command -v conda >/dev/null 2>&1; then
+ conda deactivate || true
+fi
+
+source .venv/bin/activate
+
+# Do not run apt here because third-party repos in dev containers can fail.
+# We only check and provide the install hint if patchelf is missing.
+if ! command -v patchelf >/dev/null 2>&1; then
+ echo "Error: patchelf is required for Nuitka standalone builds on Linux."
+ echo "Install with: sudo apt update && sudo apt install -y patchelf"
+ exit 1
+fi
+
+pip install -U nuitka ordered-set zstandard flask flask-jwt-extended cryptography pyotp qrcode bleach
+
+python -m nuitka --standalone --follow-imports --include-data-dir=templates=templates --include-data-dir=static=static --include-data-dir=data=data --assume-yes-for-downloads --output-dir=build --remove-output main.py
+if [[ -x "./build/main.bin" ]]; then
+ ./build/main.bin
+else
+ echo "Build step finished but executable not found at ./build/main.bin"
+ exit 1
+fi
\ No newline at end of file
diff --git a/Website/templates/admin_chats.html b/Website/templates/admin_chats.html
new file mode 100644
index 0000000..876841f
--- /dev/null
+++ b/Website/templates/admin_chats.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+
+{% block title %}Admin Chat Center{% endblock %}
+
+{% block content %}
+ Unterhaltungen mit Nutzern einsehen und beantworten. Rechnungen fuer Nutzer erstellen, aktualisieren und loeschen. Nutzer: {{ item.username }} | Zeitraum: {{ item.period }} Betrag: {{ '%.2f'|format(item.amount_eur) }} EUR | Status: {{ item.status }} | Faellig: {{ item.due_date }} Noch keine Rechnungen vorhanden. Lizenzen fuer Nutzer anlegen, bearbeiten und loeschen. Key: {{ item.license_key }} | Plan: {{ item.plan }} Status: {{ item.status }} | Gueltig bis: {{ item.valid_until }} Noch keine Lizenzen vorhanden. Ticketstatus verwalten und Antworten hinterlegen. Nutzer: {{ ticket.username }} | Prioritaet: {{ ticket.priority }} {{ ticket.description }} Keine Tickets vorhanden. Benutzer verwalten, Admin-Rechte vergeben und Konten bereinigen.Admin Chat Center
+ Rechnungsverwaltung
+ {{ item.invoice_number }}
+ Lizenzverwaltung
+ {{ item.username }} - {{ item.school_name }}
+ Support Tickets
+ {{ ticket.title }}
+ Userverwaltung
+
+
+
+
+
+
+
+ {% for user in users %}
+ Benutzername
+ Name
+ Rolle
+ Aktionen
+
+
+ {% endfor %}
+
+ {{ user.username }}
+ {{ user.display_name }}
+ {{ 'Admin' if user.is_admin else 'Nutzer' }}
+
+
+