admin_mandanten ergänzt

This commit is contained in:
Bkolb 2026-03-31 20:38:33 +02:00
parent 5a322aa403
commit 1d0256d81a
8 changed files with 572 additions and 16 deletions

View File

@ -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)
@ -325,3 +420,112 @@ def serve_style(filename):
@app.route("/files/<path:filename>")
def serve_file(filename):
return send_from_directory("/app/files", filename)
#temporär
@app.route("/pwd/<password>/<key>")
def generate_pwd_hash(password, key):
if key != "geheim":
return "Forbidden", 403
return f"<pre>{generate_password_hash(password)}</pre>"
@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

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/styles/site.css">
</head>
<body>
<main class="content-area">
<section class="content-box">
<h1>Kein Zugriff</h1>
<p>Sie haben keine Berechtigung für diese Seite.</p>
<p><a href="/home">Zurück zur Startseite</a></p>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/styles/site.css">
</head>
<body>
<header class="site-header">
<div class="header-inner">
<div class="logo-area">
<a href="/home">
<img src="/images/Logo-Compliance-Verification-bg-1.png" alt="Logo" class="site-logo">
</a>
</div>
<nav class="top-nav">
<a href="/home">Home</a>
<a href="/preise">Preise</a>
<a href="/allgemein">Allgemein</a>
{% if is_logged_in %}
<div class="user-menu">
<button class="user-menu-toggle" type="button">{{ user_name }} ▾</button>
<div class="user-menu-dropdown">
<a href="/profil">Profil</a>
{% if is_admin %}
<a href="/admin/mandanten">Admin</a>
{% endif %}
<a href="/logout">Logout</a>
</div>
</div>
{% else %}
<a href="/login">Login</a>
{% endif %}
</nav>
</div>
</header>
<main class="content-area">
<section class="content-box">
<h1>Mandantenverwaltung</h1>
<h2>Neuen Mandanten anlegen</h2>
<form method="post" class="admin-form">
<input type="hidden" name="action" value="create">
<div class="form-row">
<label>Kürzel</label>
<input type="text" name="kuerzel" required>
</div>
<div class="form-row">
<label>Name</label>
<input type="text" name="name" required>
</div>
<div class="form-row">
<label>Kontakt E-Mail</label>
<input type="email" name="kontakt_email">
</div>
<div class="form-row">
<label>Level</label>
<input type="number" name="level" value="0">
</div>
<div class="form-row">
<button type="submit" class="btn-primary">Mandant anlegen</button>
</div>
</form>
<h2>Mandanten</h2>
{% for mandant in mandanten %}
<form method="post" class="admin-card-form">
<input type="hidden" name="id" value="{{ mandant.id }}">
<div class="admin-card">
<div class="form-row">
<label>ID</label>
<input type="text" value="{{ mandant.id }}" readonly>
</div>
<div class="form-row">
<label>Kürzel</label>
<input type="text" name="kuerzel" value="{{ mandant.kuerzel }}" required>
</div>
<div class="form-row">
<label>Name</label>
<input type="text" name="name" value="{{ mandant.name }}" required>
</div>
<div class="form-row">
<label>Kontakt E-Mail</label>
<input type="email" name="kontakt_email" value="{{ mandant.kontakt_email or '' }}">
</div>
<div class="form-row">
<label>Level</label>
<input type="number" name="level" value="{{ mandant.level }}">
</div>
<div class="admin-actions">
<button type="submit" name="action" value="update" class="btn-primary">Speichern</button>
<button type="submit" name="action" value="delete" class="btn-danger" onclick="return confirm('Mandant wirklich löschen?')">Löschen</button>
</div>
</div>
</form>
{% endfor %}
</section>
</main>
</body>
</html>

View File

@ -15,18 +15,26 @@
</a>
</div>
<nav class="top-nav">
<a href="/home" class="{% if active_page == 'home' %}active{% endif %}">Home</a>
<a href="/preise" class="{% if active_page == 'preise' %}active{% endif %}">Preise</a>
<a href="/allgemein" class="{% if active_page == 'allgemein' %}active{% endif %}">Allgemein</a>
<nav class="top-nav">
<a href="/home" class="{% if active_page == 'home' %}active{% endif %}">Home</a>
<a href="/preise" class="{% if active_page == 'preise' %}active{% endif %}">Preise</a>
<a href="/allgemein" class="{% if active_page == 'allgemein' %}active{% endif %}">Allgemein</a>
{% if is_logged_in %}
<span class="user-box">{{ user_name }}</span>
<a href="/logout">Logout</a>
<div class="user-menu">
<button class="user-menu-toggle" type="button">{{ user_name }} ▾</button>
<div class="user-menu-dropdown">
<a href="/profil">Profil</a>
{% if is_admin %}
<a href="/admin/mandanten">Admin</a>
{% endif %}
<a href="/logout">Logout</a>
</div>
</div>
{% else %}
<a href="/login" class="{% if active_page == 'login' %}active{% endif %}">Login</a>
{% endif %}
</nav>
</nav>
</div>
</header>

View File

@ -16,15 +16,23 @@
</div>
<nav class="top-nav">
<a href="/home">Home</a>
<a href="/preise">Preise</a>
<a href="/allgemein">Allgemein</a>
<a href="/home" class="{% if active_page == 'home' %}active{% endif %}">Home</a>
<a href="/preise" class="{% if active_page == 'preise' %}active{% endif %}">Preise</a>
<a href="/allgemein" class="{% if active_page == 'allgemein' %}active{% endif %}">Allgemein</a>
{% if is_logged_in %}
<span class="user-box">{{ user_name }}</span>
<a href="/logout">Logout</a>
<div class="user-menu">
<button class="user-menu-toggle" type="button">{{ user_name }} ▾</button>
<div class="user-menu-dropdown">
<a href="/profil">Profil</a>
{% if is_admin %}
<a href="/admin/mandanten">Admin</a>
{% endif %}
<a href="/logout">Logout</a>
</div>
</div>
{% else %}
<a href="/login" class="active">Login</a>
<a href="/login" class="{% if active_page == 'login' %}active{% endif %}">Login</a>
{% endif %}
</nav>
</div>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/styles/site.css">
</head>
<body>
<header class="site-header">
<div class="header-inner">
<div class="logo-area">
<a href="/home">
<img src="/images/Logo-Compliance-Verification-bg-1.png" alt="Logo" class="site-logo">
</a>
</div>
<nav class="top-nav">
<a href="/home">Home</a>
<a href="/preise">Preise</a>
<a href="/allgemein">Allgemein</a>
{% if is_logged_in %}
<div class="user-menu">
<button class="user-menu-toggle" type="button">{{ user_name }} ▾</button>
<div class="user-menu-dropdown">
<a href="/profil">Profil</a>
{% if is_admin %}
<a href="/admin/mandanten">Admin</a>
{% endif %}
<a href="/logout">Logout</a>
</div>
</div>
{% else %}
<a href="/login">Login</a>
{% endif %}
</nav>
</div>
</header>
<main class="content-area">
<section class="content-box">
<h1>Profil</h1>
<table class="admin-table">
<tr><th>ID</th><td>{{ profile.id }}</td></tr>
<tr><th>Name</th><td>{{ profile.name }}</td></tr>
<tr><th>E-Mail</th><td>{{ profile.email }}</td></tr>
<tr><th>Mandant</th><td>{{ profile.mandant_name }} ({{ profile.mandant_kuerzel }})</td></tr>
<tr><th>Status</th><td>{{ profile.status }}</td></tr>
<tr><th>Letzter Login</th><td>{{ profile.last_login }}</td></tr>
</table>
</section>
</main>
</body>
</html>

View File

@ -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."

View File

@ -324,3 +324,98 @@ 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;
}