diff --git a/app/flask-postgres/app/app.py b/app/flask-postgres/app/app.py index 1f0e1a4..8191fff 100644 --- a/app/flask-postgres/app/app.py +++ b/app/flask-postgres/app/app.py @@ -1,4 +1,6 @@ import logging +import csv +import io import os import re import shutil @@ -55,6 +57,14 @@ if not app.config.get("TESTING"): # app.logger.setLevel(logging.INFO) # app.logger.addHandler(file_handler) +def format_user_status(status): + mapping = { + 0: "0 - deaktiviert", + 1: "1 - aktiv", + 2: "2 - gesperrt", + 3: "3 - disabled", + } + return mapping.get(status, f"{status} - unbekannt") def format_level(level): mapping = { @@ -680,108 +690,47 @@ def admin_mandanten(): @user_admin_required def useradmin_mandant(): current_mandant_id = session.get("mandant_id") + current_user_id = session.get("user_id") conn = get_connection() cur = conn.cursor() + cur.execute(""" + SELECT name, level + FROM mandant + WHERE id = %s + """, (current_mandant_id,)) + mandant_row = cur.fetchone() + mandant_name = mandant_row[0] if mandant_row else f"Mandant {current_mandant_id}" + mandant_level = mandant_row[1] if mandant_row else None + cur.execute(""" SELECT u.id, u.email, u.name, - u.mandant_id, - u.last_login, - u.status, - m.name AS mandant_name, - m.kontakt_email AS mandant_email, - m.level AS mandant_level + u.status FROM app_user u - JOIN mandant m ON m.id = u.mandant_id WHERE u.mandant_id = %s ORDER BY u.name, u.email """, (current_mandant_id,)) - users = fetchall_dict(cur) + for user in users: + user["status_label"] = format_user_status(user["status"]) + user["can_delete"] = user["id"] != current_user_id + cur.close() conn.close() - for user in users: - user["mandant_level_label"] = format_level(user["mandant_level"]) - return render_template( "useradmin_mandant.html", page_title="Useradministration", active_page="useradmin", users=users, - **get_current_user() - ) - -@app.errorhandler(403) -def forbidden(_error): - return render_template( - "403.html", - page_title="Kein Zugriff", - active_page="", - **get_current_user() - ), 403 - -@app.route("/pwdchange", methods=["GET", "POST"]) -@login_required -def pwdchange(): - error_message = "" - success_message = "" - - if request.method == "POST": - current_password = request.form.get("current_password", "") - new_password = request.form.get("new_password", "") - confirm_password = request.form.get("confirm_password", "") - - if not current_password or not new_password or not confirm_password: - error_message = "Bitte alle Felder ausfüllen." - elif new_password != confirm_password: - error_message = "Die neuen Passwörter stimmen nicht überein." - elif len(new_password) < 8: - error_message = "Das neue Passwort muss mindestens 8 Zeichen lang sein." - else: - conn = get_connection() - cur = conn.cursor() - - cur.execute(""" - SELECT password_hash - FROM app_user - WHERE id = %s - """, (session["user_id"],)) - row = cur.fetchone() - - if row is None: - error_message = "Benutzer nicht gefunden." - else: - stored_hash = row[0] - - if not check_password_hash(stored_hash, current_password): - error_message = "Das aktuelle Passwort ist falsch." - else: - new_hash = generate_password_hash(new_password) - - cur.execute(""" - UPDATE app_user - SET password_hash = %s - WHERE id = %s - """, (new_hash, session["user_id"])) - conn.commit() - - success_message = "Passwort wurde erfolgreich geändert." - - cur.close() - conn.close() - - return render_template( - "pwdchange.html", - page_title="Passwort ändern", - active_page="profil", - error_message=error_message, - success_message=success_message, + mandant_name=mandant_name, + mandant_level_label=format_level(mandant_level) if mandant_level is not None else "-", + current_user_id=current_user_id, **get_current_user() ) @@ -1954,4 +1903,160 @@ def course_assessment(course_id): total_questions=total_questions, passed=passed, **get_current_user() - ) \ No newline at end of file + ) + +@app.route("/useradmin/mandant/upload", methods=["GET", "POST"]) +@user_admin_required +def useradmin_user_upload(): + current_mandant_id = session.get("mandant_id") + form_error = None + success_message = None + row_errors = [] + + conn = get_connection() + cur = conn.cursor() + + cur.execute(""" + SELECT name + FROM mandant + WHERE id = %s + """, (current_mandant_id,)) + mandant_row = cur.fetchone() + mandant_name = mandant_row[0] if mandant_row else f"Mandant {current_mandant_id}" + + if request.method == "POST": + uploaded_file = request.files.get("csv_file") + + if not uploaded_file or uploaded_file.filename == "": + form_error = "Bitte eine CSV-Datei auswählen." + else: + try: + content = uploaded_file.read().decode("utf-8-sig") + except Exception: + form_error = "Die Datei konnte nicht gelesen werden. Bitte UTF-8 CSV verwenden." + + if not form_error: + reader = csv.reader(io.StringIO(content), delimiter=";") + rows = list(reader) + + if not rows: + form_error = "Die Datei ist leer." + + parsed_users = [] + + if not form_error: + email_pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$" + seen_emails_in_file = set() + + for line_no, row in enumerate(rows, start=1): + if not row or all(not str(col).strip() for col in row): + continue + + if len(row) != 4: + row_errors.append(f"Zeile {line_no}: Es müssen genau 4 Felder vorhanden sein.") + continue + + name = row[0].strip() + email = row[1].strip().lower() + status = row[2].strip() + password = row[3].strip() + + line_problems = [] + + if not name: + line_problems.append("Name fehlt") + if not email: + line_problems.append("E-Mail fehlt") + elif not re.match(email_pattern, email): + line_problems.append("E-Mail ungültig") + if not status: + line_problems.append("Status fehlt") + elif status not in ("0", "1"): + line_problems.append("Status muss 0 oder 1 sein") + if not password: + line_problems.append("Passwort fehlt") + elif ";" in password: + line_problems.append("Passwort darf kein ';' enthalten") + elif len(password) < 8: + line_problems.append("Passwort muss mindestens 8 Zeichen lang sein") + + if email: + if email in seen_emails_in_file: + line_problems.append("E-Mail doppelt in Datei") + else: + seen_emails_in_file.add(email) + + cur.execute(""" + SELECT id + FROM app_user + WHERE lower(email) = %s + """, (email,)) + existing_user = cur.fetchone() + if existing_user: + line_problems.append("E-Mail existiert bereits") + + if line_problems: + row_errors.append(f"Zeile {line_no}: " + ", ".join(line_problems)) + else: + parsed_users.append({ + "name": name, + "email": email, + "status": int(status), + "password_hash": generate_password_hash(password), + }) + + if row_errors: + form_error = "Fehlerhafte Datei. Bitte prüfen Sie die markierten Zeilen." + else: + for u in parsed_users: + cur.execute(""" + INSERT INTO app_user (email, name, mandant_id, password_hash, status) + VALUES (%s, %s, %s, %s, %s) + """, ( + u["email"], + u["name"], + current_mandant_id, + u["password_hash"], + u["status"] + )) + + conn.commit() + success_message = f"{len(parsed_users)} neue Benutzer angelegt." + + cur.close() + conn.close() + + return render_template( + "useradmin_user_upload.html", + page_title="CSV-Upload Benutzer", + active_page="useradmin", + form_error=form_error, + success_message=success_message, + row_errors=row_errors, + mandant_name=mandant_name, + **get_current_user() + ) + +@app.route("/useradmin/mandant/user//delete", methods=["POST"]) +@user_admin_required +def useradmin_user_delete(user_id): + current_mandant_id = session.get("mandant_id") + current_user_id = session.get("user_id") + + if user_id == current_user_id: + abort(403) + + conn = get_connection() + cur = conn.cursor() + + cur.execute(""" + DELETE FROM app_user + WHERE id = %s + AND mandant_id = %s + """, (user_id, current_mandant_id)) + conn.commit() + + cur.close() + conn.close() + + return redirect(url_for("useradmin_mandant")) \ No newline at end of file diff --git a/app/flask-postgres/app/templates/useradmin_mandant.html b/app/flask-postgres/app/templates/useradmin_mandant.html index 0b91de5..b2847b9 100644 --- a/app/flask-postgres/app/templates/useradmin_mandant.html +++ b/app/flask-postgres/app/templates/useradmin_mandant.html @@ -4,18 +4,32 @@

Benutzerübersicht

+

+ Level des Mandanten: {{ mandant_level_label }} +

- + + +
+ Status: + 0 - deaktiviert + 1 - aktiv + 2 - gesperrt + 3 - disabled +
@@ -25,9 +39,6 @@ - - - @@ -37,20 +48,30 @@ - - - - + {% endfor %}
Name E-Mail StatusLetzter LoginMandantLevel Aktionen
{{ user.id }} {{ user.name }} {{ user.email }}{{ user.status }}{{ user.last_login or "-" }}{{ user.mandant_name }}{{ user.mandant_level_label }}{{ user.status_label }} - +
+ Bearbeiten + + {% if user.can_delete %} +
+ +
+ {% else %} + + {% endif %} +
+
diff --git a/app/flask-postgres/app/templates/useradmin_user_upload.html b/app/flask-postgres/app/templates/useradmin_user_upload.html new file mode 100644 index 0000000..9104c05 --- /dev/null +++ b/app/flask-postgres/app/templates/useradmin_user_upload.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block content %} + + + +
+
+ + {% if form_error %} +
{{ form_error }}
+ {% endif %} + + {% if success_message %} +
{{ success_message }}
+ {% endif %} + + {% if row_errors %} +
+ Fehlerdetails: +
    + {% for err in row_errors %} +
  • {{ err }}
  • + {% endfor %} +
+
+ {% endif %} + +
+
+ +
+ + +
+ +
+

Format

+
name;email;status;passwort
+ +

Beispiel

+
Max Mustermann;max@example.com;1;Geheimes123
+Erika Musterfrau;erika@example.com;0;Passwort999
+ +

Hinweise

+
    +
  • Status nur 0 oder 1
  • +
  • Passwort mindestens 8 Zeichen
  • +
  • E-Mail muss eindeutig und gültig sein
  • +
  • Trennzeichen ist Semikolon
  • +
+
+ +
+ +
+ + Zurück +
+
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/flask-postgres/images/comic/avatar1.jpeg b/app/flask-postgres/images/comic/avatar1.jpeg new file mode 100644 index 0000000..44d850c Binary files /dev/null and b/app/flask-postgres/images/comic/avatar1.jpeg differ diff --git a/app/flask-postgres/images/comic/avatar10.png b/app/flask-postgres/images/comic/avatar10.png new file mode 100644 index 0000000..b846847 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar10.png differ diff --git a/app/flask-postgres/images/comic/avatar11.png b/app/flask-postgres/images/comic/avatar11.png new file mode 100644 index 0000000..6061028 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar11.png differ diff --git a/app/flask-postgres/images/comic/avatar2.png b/app/flask-postgres/images/comic/avatar2.png new file mode 100644 index 0000000..ff400dd Binary files /dev/null and b/app/flask-postgres/images/comic/avatar2.png differ diff --git a/app/flask-postgres/images/comic/avatar3.png b/app/flask-postgres/images/comic/avatar3.png new file mode 100644 index 0000000..d07d3b6 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar3.png differ diff --git a/app/flask-postgres/images/comic/avatar4.png b/app/flask-postgres/images/comic/avatar4.png new file mode 100644 index 0000000..a469a55 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar4.png differ diff --git a/app/flask-postgres/images/comic/avatar5.png b/app/flask-postgres/images/comic/avatar5.png new file mode 100644 index 0000000..ce234eb Binary files /dev/null and b/app/flask-postgres/images/comic/avatar5.png differ diff --git a/app/flask-postgres/images/comic/avatar6.png b/app/flask-postgres/images/comic/avatar6.png new file mode 100644 index 0000000..6ff348d Binary files /dev/null and b/app/flask-postgres/images/comic/avatar6.png differ diff --git a/app/flask-postgres/images/comic/avatar7.png b/app/flask-postgres/images/comic/avatar7.png new file mode 100644 index 0000000..4d96942 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar7.png differ diff --git a/app/flask-postgres/images/comic/avatar8.png b/app/flask-postgres/images/comic/avatar8.png new file mode 100644 index 0000000..c35409f Binary files /dev/null and b/app/flask-postgres/images/comic/avatar8.png differ diff --git a/app/flask-postgres/images/comic/avatar9.png b/app/flask-postgres/images/comic/avatar9.png new file mode 100644 index 0000000..a7e43f6 Binary files /dev/null and b/app/flask-postgres/images/comic/avatar9.png differ diff --git a/app/flask-postgres/styles/site.css b/app/flask-postgres/styles/site.css index 90e4e5f..0b87bde 100644 --- a/app/flask-postgres/styles/site.css +++ b/app/flask-postgres/styles/site.css @@ -1090,4 +1090,128 @@ button { background: #22c55e; /* grün */ border-radius: 8px; transition: width 0.3s ease; +} + +/* =============================== + CSV Upload User + =============================== */ + +.upload-panel { + max-width: 1100px; +} + +.upload-form { + margin-top: 8px; +} + +.upload-grid { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 28px; + align-items: start; +} + +.upload-main { + background: #f8fbff; + border: 1px solid #dce3ea; + border-radius: 16px; + padding: 24px; +} + +.upload-label { + display: block; + margin-bottom: 12px; + font-weight: 700; + color: #0d2f57; + font-size: 20px; +} + +.upload-info-card { + background: #f8fbff; + border: 1px solid #dce3ea; + border-radius: 16px; + padding: 24px; +} + +.upload-info-card h2 { + margin: 0 0 10px; + font-size: 22px; + color: #0d2f57; +} + +.upload-info-card h2:not(:first-child) { + margin-top: 22px; +} + +.upload-code { + display: inline-block; + padding: 10px 14px; + border-radius: 10px; + background: #eef4fb; + border: 1px solid #dce3ea; + color: #0d2f57; + font-family: monospace; + font-size: 18px; +} + +.upload-example { + margin: 0; + padding: 14px 16px; + border-radius: 12px; + background: #0f172a; + color: #e2e8f0; + font-family: monospace; + font-size: 16px; + line-height: 1.5; + overflow-x: auto; +} + +.upload-hints { + margin: 0; + padding-left: 18px; +} + +.upload-hints li { + margin-bottom: 8px; +} + +.upload-actions { + margin-top: 24px; +} + +.error-list { + margin: 10px 0 0; + padding-left: 18px; +} + +@media (max-width: 900px) { + .upload-grid { + grid-template-columns: 1fr; + } +} + +.admin-actions-spaced { + margin-bottom: 22px; +} + +.status-legend { + display: flex; + flex-wrap: wrap; + gap: 12px 18px; + align-items: center; + margin-bottom: 18px; + padding: 12px 14px; + background: #f8fbff; + border: 1px solid #dce3ea; + border-radius: 12px; + color: #334155; +} + +.status-legend strong { + color: #0d2f57; +} + +.admin-subtitle { + margin: 6px 0 0; + color: #526172; } \ No newline at end of file diff --git a/app/flask-postgres/tests/userliste-fail.csv b/app/flask-postgres/tests/userliste-fail.csv new file mode 100644 index 0000000..2e070f5 --- /dev/null +++ b/app/flask-postgres/tests/userliste-fail.csv @@ -0,0 +1,3 @@ +Max Mustermann;mm@kolb.cc;0;geheim123 +Susi Sonnenschein;suso@gmail;0;test +Petra Schön;ps@kolb.cc;4;passwort;ooo \ No newline at end of file diff --git a/app/flask-postgres/tests/userliste.csv b/app/flask-postgres/tests/userliste.csv new file mode 100644 index 0000000..5947bd9 --- /dev/null +++ b/app/flask-postgres/tests/userliste.csv @@ -0,0 +1,3 @@ +Max Mustermann;mm@kolb.cc;0;geheim123 +Susi Sonnenschein;suso@gmail.com;0;testtest +Petra Schön;ps@kolb.cc;1;passwort \ No newline at end of file