From 1d0256d81a4f183fdac2bd99bf2c5cdf4a2c1e6c Mon Sep 17 00:00:00 2001 From: Bkolb Date: Tue, 31 Mar 2026 20:38:33 +0200 Subject: [PATCH] =?UTF-8?q?admin=5Fmandanten=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/flask-postgres/app/app.py | 210 +++++++++++++++++- app/flask-postgres/app/templates/403.html | 18 ++ .../app/templates/admin_mandanten.html | 116 ++++++++++ app/flask-postgres/app/templates/index.html | 22 +- app/flask-postgres/app/templates/login.html | 20 +- app/flask-postgres/app/templates/profil.html | 56 +++++ app/flask-postgres/deploy_flask.sh | 51 +++++ app/flask-postgres/styles/site.css | 95 ++++++++ 8 files changed, 572 insertions(+), 16 deletions(-) create mode 100644 app/flask-postgres/app/templates/403.html create mode 100644 app/flask-postgres/app/templates/admin_mandanten.html create mode 100644 app/flask-postgres/app/templates/profil.html create mode 100644 app/flask-postgres/deploy_flask.sh diff --git a/app/flask-postgres/app/app.py b/app/flask-postgres/app/app.py index 58b936d..be194e4 100644 --- a/app/flask-postgres/app/app.py +++ b/app/flask-postgres/app/app.py @@ -5,6 +5,7 @@ from functools import wraps from logging.handlers import RotatingFileHandler import psycopg2 + from flask import ( Flask, redirect, @@ -13,6 +14,7 @@ from flask import ( send_from_directory, session, url_for, + abort, ) from werkzeug.security import check_password_hash, generate_password_hash @@ -145,15 +147,46 @@ def ensure_default_admin(): """, ("KOLB", "Kolb Compliance", "info@kolb.cc", 0)) mandant_id = cur.fetchone()[0] + cur.execute(""" + SELECT id FROM app_group + WHERE mandant_id = %s AND group_name = %s + """, (1, "Administratoren")) + group_row = cur.fetchone() + + if group_row: + admin_group_id = group_row[0] + else: + cur.execute(""" + INSERT INTO app_group (mandant_id, group_name) + VALUES (%s, %s) + RETURNING id + """, (1, "Administratoren")) + admin_group_id = cur.fetchone()[0] + cur.execute("SELECT id FROM app_user WHERE email = %s", ("admin@kolb.cc",)) user_row = cur.fetchone() - if not user_row: + if user_row: + admin_user_id = user_row[0] + else: password_hash = generate_password_hash("topsecret") cur.execute(""" INSERT INTO app_user (email, name, mandant_id, password_hash, status) VALUES (%s, %s, %s, %s, %s) - """, ("admin@kolb.cc", "Admin", mandant_id, password_hash, 1)) + RETURNING id + """, ("admin@kolb.cc", "Admin", 1, password_hash, 1)) + admin_user_id = cur.fetchone()[0] + + cur.execute(""" + SELECT 1 FROM user_group + WHERE user_id = %s AND group_id = %s AND mandant_id = %s + """, (admin_user_id, admin_group_id, 1)) + + if cur.fetchone() is None: + cur.execute(""" + INSERT INTO user_group (user_id, group_id, mandant_id) + VALUES (%s, %s, %s) + """, (admin_user_id, admin_group_id, 1)) conn.commit() cur.close() @@ -186,6 +219,68 @@ def get_current_user(): "is_logged_in": bool(session.get("user_id")), } +def fetchone_dict(cur): + row = cur.fetchone() + if row is None: + return None + columns = [desc[0] for desc in cur.description] + return dict(zip(columns, row)) + + +def fetchall_dict(cur): + rows = cur.fetchall() + columns = [desc[0] for desc in cur.description] + return [dict(zip(columns, row)) for row in rows] + + +def user_is_admin(): + user_id = session.get("user_id") + if not user_id: + return False + + conn = get_connection() + cur = conn.cursor() + + cur.execute(""" + SELECT 1 + FROM app_user u + JOIN user_group ug ON ug.user_id = u.id + JOIN app_group g ON g.id = ug.group_id + WHERE u.id = %s + AND ug.mandant_id = 1 + AND g.mandant_id = 1 + AND g.group_name = 'Administratoren' + LIMIT 1 + """, (user_id,)) + + result = cur.fetchone() + + cur.close() + conn.close() + + return result is not None + + +def get_current_user(): + return { + "user_id": session.get("user_id"), + "user_name": session.get("user_name"), + "user_email": session.get("user_email"), + "is_logged_in": bool(session.get("user_id")), + "is_admin": user_is_admin() if session.get("user_id") else False, + } + + +def admin_required(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + if not session.get("user_id"): + return redirect(url_for("login", next=request.path)) + if not user_is_admin(): + abort(403) + return view_func(*args, **kwargs) + return wrapper + def login_required(view_func): @wraps(view_func) @@ -324,4 +419,113 @@ def serve_style(filename): @app.route("/files/") def serve_file(filename): - return send_from_directory("/app/files", filename) \ No newline at end of file + return send_from_directory("/app/files", filename) + +#temporär +@app.route("/pwd//") +def generate_pwd_hash(password, key): + if key != "geheim": + return "Forbidden", 403 + + return f"
{generate_password_hash(password)}
" + +@app.route("/profil") +@login_required +def profil(): + conn = get_connection() + cur = conn.cursor() + + cur.execute(""" + SELECT u.id, u.email, u.name, u.mandant_id, u.last_login, u.status, + m.name AS mandant_name, m.kuerzel AS mandant_kuerzel + FROM app_user u + JOIN mandant m ON m.id = u.mandant_id + WHERE u.id = %s + """, (session["user_id"],)) + + user_data = fetchone_dict(cur) + + cur.close() + conn.close() + + return render_template( + "profil.html", + page_title="Profil", + active_page="profil", + profile=user_data, + **get_current_user() + ) + +@app.route("/admin/mandanten", methods=["GET", "POST"]) +@admin_required +def admin_mandanten(): + conn = get_connection() + cur = conn.cursor() + + if request.method == "POST": + action = request.form.get("action") + + if action == "create": + kuerzel = request.form.get("kuerzel", "").strip() + name = request.form.get("name", "").strip() + kontakt_email = request.form.get("kontakt_email", "").strip() + level = request.form.get("level", "0").strip() + + cur.execute(""" + INSERT INTO mandant (kuerzel, name, kontakt_email, level) + VALUES (%s, %s, %s, %s) + """, (kuerzel, name, kontakt_email or None, int(level or 0))) + conn.commit() + + elif action == "update": + mandant_id = request.form.get("id") + kuerzel = request.form.get("kuerzel", "").strip() + name = request.form.get("name", "").strip() + kontakt_email = request.form.get("kontakt_email", "").strip() + level = request.form.get("level", "0").strip() + + cur.execute(""" + UPDATE mandant + SET kuerzel = %s, + name = %s, + kontakt_email = %s, + level = %s + WHERE id = %s + """, (kuerzel, name, kontakt_email or None, int(level or 0), int(mandant_id))) + conn.commit() + + elif action == "delete": + mandant_id = request.form.get("id") + cur.execute("DELETE FROM mandant WHERE id = %s", (int(mandant_id),)) + conn.commit() + + cur.close() + conn.close() + return redirect(url_for("admin_mandanten")) + + cur.execute(""" + SELECT id, kuerzel, name, kontakt_email, level + FROM mandant + ORDER BY id + """) + mandanten = fetchall_dict(cur) + + cur.close() + conn.close() + + return render_template( + "admin_mandanten.html", + page_title="Admin - Mandanten", + active_page="admin", + mandanten=mandanten, + **get_current_user() + ) + +@app.errorhandler(403) +def forbidden(_error): + return render_template( + "403.html", + page_title="Kein Zugriff", + active_page="", + **get_current_user() + ), 403 \ No newline at end of file diff --git a/app/flask-postgres/app/templates/403.html b/app/flask-postgres/app/templates/403.html new file mode 100644 index 0000000..617ccb1 --- /dev/null +++ b/app/flask-postgres/app/templates/403.html @@ -0,0 +1,18 @@ + + + + + + {{ page_title }} + + + +
+
+

Kein Zugriff

+

Sie haben keine Berechtigung für diese Seite.

+

Zurück zur Startseite

+
+
+ + \ No newline at end of file diff --git a/app/flask-postgres/app/templates/admin_mandanten.html b/app/flask-postgres/app/templates/admin_mandanten.html new file mode 100644 index 0000000..c065c63 --- /dev/null +++ b/app/flask-postgres/app/templates/admin_mandanten.html @@ -0,0 +1,116 @@ + + + + + + {{ page_title }} + + + + + +
+
+

Mandantenverwaltung

+ +

Neuen Mandanten anlegen

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +

Mandanten

+ + {% for mandant in mandanten %} +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ {% endfor %} +
+
+ + \ No newline at end of file diff --git a/app/flask-postgres/app/templates/index.html b/app/flask-postgres/app/templates/index.html index 7808cc1..43bf40a 100644 --- a/app/flask-postgres/app/templates/index.html +++ b/app/flask-postgres/app/templates/index.html @@ -15,18 +15,26 @@ - diff --git a/app/flask-postgres/app/templates/login.html b/app/flask-postgres/app/templates/login.html index a500424..ecba7de 100644 --- a/app/flask-postgres/app/templates/login.html +++ b/app/flask-postgres/app/templates/login.html @@ -16,15 +16,23 @@ diff --git a/app/flask-postgres/app/templates/profil.html b/app/flask-postgres/app/templates/profil.html new file mode 100644 index 0000000..4fcc55e --- /dev/null +++ b/app/flask-postgres/app/templates/profil.html @@ -0,0 +1,56 @@ + + + + + + {{ page_title }} + + + + + +
+
+

Profil

+ + + + + + + + +
ID{{ profile.id }}
Name{{ profile.name }}
E-Mail{{ profile.email }}
Mandant{{ profile.mandant_name }} ({{ profile.mandant_kuerzel }})
Status{{ profile.status }}
Letzter Login{{ profile.last_login }}
+
+
+ + \ No newline at end of file diff --git a/app/flask-postgres/deploy_flask.sh b/app/flask-postgres/deploy_flask.sh new file mode 100644 index 0000000..2e6121f --- /dev/null +++ b/app/flask-postgres/deploy_flask.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +SRC_ROOT="/Volumes/MacBook SD/Projekte/compliance-verification/app/flask-postgres" +DST_ROOT="/Volumes/docker/flask-postgres" + +NAS_USER="BKolb" +NAS_HOST="192.168.0.10" +CONTAINER_NAME="flask_web" + +echo "Starte Deployment..." + +[ -d "$SRC_ROOT/app" ] || { echo "Quelle app fehlt: $SRC_ROOT/app"; exit 1; } +[ -d "$SRC_ROOT/images" ] || { echo "Quelle images fehlt: $SRC_ROOT/images"; exit 1; } +[ -d "$SRC_ROOT/styles" ] || { echo "Quelle styles fehlt: $SRC_ROOT/styles"; exit 1; } + +[ -d "$DST_ROOT/app" ] || { echo "Ziel app fehlt: $DST_ROOT/app"; exit 1; } +[ -d "$DST_ROOT/images" ] || { echo "Ziel images fehlt: $DST_ROOT/images"; exit 1; } +[ -d "$DST_ROOT/styles" ] || { echo "Ziel styles fehlt: $DST_ROOT/styles"; exit 1; } + +echo "Synchronisiere app/ ..." +rsync -av --delete \ + --exclude '.DS_Store' \ + --exclude '._*' \ + --exclude '__pycache__/' \ + --exclude '*.pyc' \ + --exclude 'images/' \ + --exclude 'styles/' \ + --exclude 'files/' \ + --exclude 'Dockerfile.txt' \ + "$SRC_ROOT/app/" "$DST_ROOT/app/" + +echo "Synchronisiere images/ ..." +rsync -av --delete \ + --exclude '.DS_Store' \ + --exclude '._*' \ + --exclude 'videos/' \ + "$SRC_ROOT/images/" "$DST_ROOT/images/" + +echo "Synchronisiere styles/ ..." +rsync -av --delete \ + --exclude '.DS_Store' \ + --exclude '._*' \ + "$SRC_ROOT/styles/" "$DST_ROOT/styles/" + +echo "files/ wird bewusst nicht angefasst." + +echo "Starte Container manuell neu ..." +#ssh "${NAS_USER}@${NAS_HOST}" "/usr/bin/docker restart ${CONTAINER_NAME}" + +echo "Deployment abgeschlossen." \ No newline at end of file diff --git a/app/flask-postgres/styles/site.css b/app/flask-postgres/styles/site.css index 09642b6..bfa08c4 100644 --- a/app/flask-postgres/styles/site.css +++ b/app/flask-postgres/styles/site.css @@ -323,4 +323,99 @@ p { .check-list li { margin-bottom: 8px; +} + +.user-menu { + position: relative; + display: inline-block; +} + +.user-menu-toggle { + background: #376da6; + color: #fff; + border: 0; + border-radius: 999px; + padding: 10px 18px; + font-weight: 600; + cursor: pointer; + min-width: 140px; +} + +.user-menu-dropdown { + display: none; + position: absolute; + right: 0; + top: 44px; + min-width: 180px; + background: #ffffff; + border-radius: 12px; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18); + overflow: hidden; + z-index: 200; +} + +.user-menu-dropdown a { + display: block; + padding: 12px 16px; + color: #1b2430; + text-decoration: none; + min-width: unset; + text-align: left; + border-radius: 0; +} + +.user-menu-dropdown a:hover { + background: #eef4fb; + color: #0d2f57; +} + +.user-menu:hover .user-menu-dropdown { + display: block; +} + +.admin-form, +.admin-card-form { + margin-bottom: 24px; +} + +.admin-card { + background: #f8fbff; + border: 1px solid #dce3ea; + border-radius: 16px; + padding: 20px; + margin-bottom: 20px; +} + +.admin-actions { + display: flex; + gap: 12px; + margin-top: 16px; +} + +.btn-danger { + display: inline-block; + padding: 12px 18px; + border: 0; + border-radius: 10px; + background: #b62323; + color: #ffffff; + font-weight: 700; + cursor: pointer; +} + +.admin-table { + width: 100%; + border-collapse: collapse; +} + +.admin-table th, +.admin-table td { + padding: 12px 14px; + border-bottom: 1px solid #dce3ea; + text-align: left; +} + +.admin-table th { + width: 220px; + color: #0d2f57; } \ No newline at end of file