diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e85fa4..f7e9f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.5.4] - 2026-02-16 +### Added +- Admin: Benutzer direkt anlegen mit Passwort und Rollenwahl +- Admin: Benutzer bearbeiten (Name, E-Mail, Rolle) per Modal +- Admin: Passwort zuruecksetzen mit optionalem E-Mail-Versand +- Admin: Info-Mail mit Zugangsdaten an Benutzer senden +- Passwort-Generator (crypto.getRandomValues) in Admin-UI +- Verifikationslink wird immer im Log ausgegeben (nicht nur ohne SMTP) + +### Changed +- Admin-Seite komplett ueberarbeitet mit Modals fuer alle Aktionen +- E-Mail-Funktionen um `send_user_info_email` erweitert + ## [0.5.3] - 2026-02-16 ### Changed - Zugangsdaten (AUTH_SECRET_KEY, SMTP-*) aus config.yaml in .env-Datei ausgelagert diff --git a/config.yaml b/config.yaml index 22df586..f469dca 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.5.3" +version: "0.5.4" bot: name: "MeshDD-Bot" diff --git a/meshbot/auth.py b/meshbot/auth.py index 9e9ac30..e55a985 100644 --- a/meshbot/auth.py +++ b/meshbot/auth.py @@ -126,6 +126,7 @@ async def _send_email(db, recipient: str, subject: str, html_body: str): async def send_verification_email(db, email: str, token: str): app_url = config.env("SMTP_APP_URL", "http://localhost:8080") verify_url = f"{app_url}/auth/verify?token={token}" + logger.info("Verification link for %s: %s", email, verify_url) subject = "MeshDD-Bot - E-Mail verifizieren" html_body = f"""
Der Link ist 24 Stunden gueltig.
""" - - if not config.env("SMTP_HOST"): - logger.info("SMTP not configured - verification link: %s", verify_url) - await _send_email(db, email, subject, html_body) + return verify_url async def send_reset_email(db, email: str, token: str): app_url = config.env("SMTP_APP_URL", "http://localhost:8080") reset_url = f"{app_url}/auth/reset-password?token={token}" + logger.info("Reset link for %s: %s", email, reset_url) subject = "MeshDD-Bot - Passwort zuruecksetzen" html_body = f"""Der Link ist 24 Stunden gueltig.
""" + await _send_email(db, email, subject, html_body) + return reset_url - if not config.env("SMTP_HOST"): - logger.info("SMTP not configured - reset link: %s", reset_url) +async def send_user_info_email(db, email: str, name: str, password: str = None): + app_url = config.env("SMTP_APP_URL", "http://localhost:8080") + login_url = f"{app_url}/login" + subject = "MeshDD-Bot - Deine Zugangsdaten" + pw_line = f"Passwort: {password}
" if password else "Dein Passwort wurde nicht geaendert.
" + html_body = f""" +Hallo {name},
+hier sind deine Zugangsdaten fuer MeshDD-Bot:
+E-Mail: {email}
+{pw_line} +Anmelden unter: {login_url}
+Bitte aendere dein Passwort nach dem ersten Login.
+""" await _send_email(db, email, subject, html_body) @@ -338,6 +351,90 @@ def setup_auth_routes(app: web.Application, db): users = await db.get_all_users() return web.json_response(users) + async def admin_user_create(request: web.Request) -> web.Response: + require_admin_api(request) + data = await request.json() + name = data.get("name", "").strip() + email = data.get("email", "").strip().lower() + password = data.get("password", "") + role = data.get("role", "user") + + if not name or not email or not password: + return web.json_response({"error": "Name, E-Mail und Passwort erforderlich"}, status=400) + if len(password) < 8: + return web.json_response({"error": "Passwort muss mindestens 8 Zeichen lang sein"}, status=400) + if role not in ("user", "admin"): + return web.json_response({"error": "Ungueltige Rolle"}, status=400) + + existing = await db.get_user_by_email(email) + if existing: + return web.json_response({"error": "E-Mail bereits registriert"}, status=409) + + hashed = hash_password(password) + await db.create_user(email=email, password=hashed, name=name, role=role, is_verified=1) + + # Send info email if requested + if data.get("send_email"): + await send_user_info_email(db, email, name, password) + + users = await db.get_all_users() + return web.json_response(users) + + async def admin_user_update(request: web.Request) -> web.Response: + require_admin_api(request) + user_id = int(request.match_info["id"]) + data = await request.json() + + updates = {} + if "name" in data and data["name"].strip(): + updates["name"] = data["name"].strip() + if "email" in data and data["email"].strip(): + new_email = data["email"].strip().lower() + existing = await db.get_user_by_email(new_email) + if existing and existing["id"] != user_id: + return web.json_response({"error": "E-Mail bereits vergeben"}, status=409) + updates["email"] = new_email + if "role" in data and data["role"] in ("user", "admin"): + updates["role"] = data["role"] + + if not updates: + return web.json_response({"error": "Keine Aenderungen"}, status=400) + + await db.update_user(user_id, **updates) + users = await db.get_all_users() + return web.json_response(users) + + async def admin_user_reset_password(request: web.Request) -> web.Response: + require_admin_api(request) + user_id = int(request.match_info["id"]) + data = await request.json() + password = data.get("password", "") + + if not password or len(password) < 8: + return web.json_response({"error": "Passwort muss mindestens 8 Zeichen lang sein"}, status=400) + + hashed = hash_password(password) + await db.update_user(user_id, password=hashed) + + # Send info email if requested + if data.get("send_email"): + user = await db.get_user_by_id(user_id) + if user: + await send_user_info_email(db, user["email"], user["name"], password) + + users = await db.get_all_users() + return web.json_response(users) + + async def admin_user_send_info(request: web.Request) -> web.Response: + require_admin_api(request) + user_id = int(request.match_info["id"]) + user = await db.get_user_by_id(user_id) + if not user: + return web.json_response({"error": "Benutzer nicht gefunden"}, status=404) + + await send_user_info_email(db, user["email"], user["name"]) + return web.json_response({"ok": True, "message": f"Info-Mail an {user['email']} gesendet"}) + async def admin_user_delete(request: web.Request) -> web.Response: require_admin_api(request) user_id = int(request.match_info["id"]) @@ -360,6 +457,10 @@ def setup_auth_routes(app: web.Application, db): app.router.add_get("/api/auth/me", me_get) app.router.add_get("/api/admin/users", admin_users_get) + app.router.add_post("/api/admin/users", admin_user_create) + app.router.add_put("/api/admin/users/{id}", admin_user_update) app.router.add_post("/api/admin/users/{id}/role", admin_user_role) app.router.add_post("/api/admin/users/{id}/verify", admin_user_verify) + app.router.add_post("/api/admin/users/{id}/reset-password", admin_user_reset_password) + app.router.add_post("/api/admin/users/{id}/send-info", admin_user_send_info) app.router.add_delete("/api/admin/users/{id}", admin_user_delete) diff --git a/static/admin.html b/static/admin.html index 45c765f..c770654 100644 --- a/static/admin.html +++ b/static/admin.html @@ -58,7 +58,15 @@