From b44096b48f571d5ac3a877b79ad527ca0c6668d1 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Wed, 5 Aug 2026 09:03:34 +0200 Subject: [PATCH] feat: Kunden-Management (Organisationen, Aktivierungscodes, Konten) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit routers/admin_kunden.py mit echter Funktionalität: Organisationen auflisten/anlegen, pro Organisation Aktivierungscodes erzeugen und Kundenkonten anlegen (direkt in der Kundenplattform-eigenen benutzer-Tabelle, kein anode-Aufruf nötig). Löst das bisherige manuelle create_user.py-Skript für den Alltagsgebrauch ab. Co-Authored-By: Claude Sonnet 5 --- routers/admin_kunden.py | 87 +++++++++++++++++++++++++++++- templates/admin/kunden_detail.html | 59 ++++++++++++++++++++ templates/admin/kunden_index.html | 29 ++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 templates/admin/kunden_detail.html create mode 100644 templates/admin/kunden_index.html diff --git a/routers/admin_kunden.py b/routers/admin_kunden.py index a917c8f..54240de 100644 --- a/routers/admin_kunden.py +++ b/routers/admin_kunden.py @@ -1,14 +1,97 @@ -from fastapi import APIRouter, Depends, Request +from uuid import uuid4 +import bcrypt +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import RedirectResponse + +from anode_client import anode_request from auth import require_admin +from db import get_database_connection from templating import templates router = APIRouter(prefix="/admin/kunden") +def fetch_benutzer_fuer_organisation(organization_id: str) -> list[dict]: + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT email, ist_admin, created_at FROM benutzer WHERE organization_id = %s ORDER BY email", + (organization_id,), + ) + return [ + {"email": email, "ist_admin": ist_admin, "created_at": created_at} + for email, ist_admin, created_at in cur.fetchall() + ] + + @router.get("") def index(request: Request, user: dict = Depends(require_admin)): + result = anode_request("GET", "/api/v1/organizations") + organizations = result.get("organizations", []) if result.get("success") else [] + return templates.TemplateResponse( - request, "admin/platzhalter.html", {"user": user, "titel": "Kunden"} + request, "admin/kunden_index.html", {"user": user, "organizations": organizations} ) + + +@router.post("") +def organisation_anlegen(name: str = Form(...), user: dict = Depends(require_admin)): + anode_request("POST", "/api/v1/organizations", json={"name": name}) + + return RedirectResponse("/admin/kunden", status_code=303) + + +@router.get("/{organization_id}") +def detail(request: Request, organization_id: str, user: dict = Depends(require_admin)): + orgs_result = anode_request("GET", "/api/v1/organizations") + organizations = orgs_result.get("organizations", []) if orgs_result.get("success") else [] + organization = next((o for o in organizations if o["id"] == organization_id), None) + + codes_result = anode_request("GET", f"/api/v1/organizations/{organization_id}/activation-codes") + activation_codes = codes_result.get("activation_codes", []) if codes_result.get("success") else [] + + benutzer = fetch_benutzer_fuer_organisation(organization_id) + + return templates.TemplateResponse( + request, + "admin/kunden_detail.html", + { + "user": user, + "organization": organization, + "organization_id": organization_id, + "activation_codes": activation_codes, + "benutzer": benutzer, + }, + ) + + +@router.post("/{organization_id}/activation-codes") +def aktivierungscode_anlegen(organization_id: str, user: dict = Depends(require_admin)): + anode_request("POST", f"/api/v1/organizations/{organization_id}/activation-codes") + + return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303) + + +@router.post("/{organization_id}/konten") +def konto_anlegen( + organization_id: str, + email: str = Form(...), + password: str = Form(...), + ist_admin: str = Form(default=""), + user: dict = Depends(require_admin), +): + password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + """ + INSERT INTO benutzer (id, organization_id, email, password_hash, ist_admin) + VALUES (%s, %s, %s, %s, %s) + """, + (uuid4(), organization_id, email, password_hash, ist_admin == "on"), + ) + + return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303) diff --git a/templates/admin/kunden_detail.html b/templates/admin/kunden_detail.html new file mode 100644 index 0000000..cee6d7e --- /dev/null +++ b/templates/admin/kunden_detail.html @@ -0,0 +1,59 @@ +{% extends "admin/layout.html" %} +{% block title %}{{ organization.name if organization else organization_id }} — Tuxflotte Admin{% endblock %} +{% block admin_content %} +

← Kunden

+

{{ organization.name if organization else organization_id }}

+ +
+

Aktivierungscodes

+
+ + + + {% for ac in activation_codes %} + + + + + + {% endfor %} + +
CodeStatusAngelegt
{{ ac.code }}{{ "aktiv" if ac.active else "inaktiv" }}{{ ac.created_at }}
+
+
+ +
+
+ +
+

Kundenkonten

+
+ + + + {% for b in benutzer %} + + + + + + {% endfor %} + +
E-MailAdminAngelegt
{{ b.email }}{{ "ja" if b.ist_admin else "nein" }}{{ b.created_at }}
+
+
+ Neues Konto anlegen +
+ + + + + + +
+
+
+{% endblock %} diff --git a/templates/admin/kunden_index.html b/templates/admin/kunden_index.html new file mode 100644 index 0000000..6ea5b20 --- /dev/null +++ b/templates/admin/kunden_index.html @@ -0,0 +1,29 @@ +{% extends "admin/layout.html" %} +{% block title %}Kunden — Tuxflotte Admin{% endblock %} +{% block admin_content %} +

Kunden

+ +
+ + + + {% for org in organizations %} + + + + + + {% endfor %} + +
NameAngelegt
{{ org.name }}{{ org.created_at }}Verwalten
+
+ +
+Neue Organisation anlegen +
+ + + +
+
+{% endblock %}