feat: Kunden-Management (Organisationen, Aktivierungscodes, Konten)
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 <noreply@anthropic.com>
This commit is contained in:
parent
ed38f103f2
commit
b44096b48f
@ -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)
|
||||
|
||||
59
templates/admin/kunden_detail.html
Normal file
59
templates/admin/kunden_detail.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}{{ organization.name if organization else organization_id }} — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<p><a href="/admin/kunden">← Kunden</a></p>
|
||||
<h2>{{ organization.name if organization else organization_id }}</h2>
|
||||
|
||||
<section>
|
||||
<h3>Aktivierungscodes</h3>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Code</th><th>Status</th><th>Angelegt</th></tr></thead>
|
||||
<tbody>
|
||||
{% for ac in activation_codes %}
|
||||
<tr>
|
||||
<td><code>{{ ac.code }}</code></td>
|
||||
<td>{{ "aktiv" if ac.active else "inaktiv" }}</td>
|
||||
<td>{{ ac.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<form method="post" action="/admin/kunden/{{ organization_id }}/activation-codes">
|
||||
<button type="submit">Neuen Aktivierungscode erzeugen</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Kundenkonten</h3>
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>E-Mail</th><th>Admin</th><th>Angelegt</th></tr></thead>
|
||||
<tbody>
|
||||
{% for b in benutzer %}
|
||||
<tr>
|
||||
<td>{{ b.email }}</td>
|
||||
<td>{{ "ja" if b.ist_admin else "nein" }}</td>
|
||||
<td>{{ b.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neues Konto anlegen</summary>
|
||||
<form method="post" action="/admin/kunden/{{ organization_id }}/konten">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<label>
|
||||
<input type="checkbox" name="ist_admin">
|
||||
Admin-Zugriff (organisationsübergreifend)
|
||||
</label>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
{% endblock %}
|
||||
29
templates/admin/kunden_index.html
Normal file
29
templates/admin/kunden_index.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "admin/layout.html" %}
|
||||
{% block title %}Kunden — Tuxflotte Admin{% endblock %}
|
||||
{% block admin_content %}
|
||||
<h2>Kunden</h2>
|
||||
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Angelegt</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for org in organizations %}
|
||||
<tr>
|
||||
<td>{{ org.name }}</td>
|
||||
<td>{{ org.created_at }}</td>
|
||||
<td><a href="/admin/kunden/{{ org.id }}" role="button" class="outline">Verwalten</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neue Organisation anlegen</summary>
|
||||
<form method="post" action="/admin/kunden">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user