All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
- Integrated a new password reset mechanism for user accounts, allowing admins to send reset links via email. - Updated the management password reset functionality to differentiate between direct password setting and email link requests. - Added validation to ensure at least one active club admin remains when modifying club member roles. - Improved the user interface for password management in the admin panel, providing clearer feedback and options for password resets.
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""Gemeinsame Passwort-Link-Erzeugung (Sessions) + Mailtext — wie /auth/forgot-password."""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import secrets
|
||
from datetime import datetime, timedelta
|
||
from typing import Any
|
||
|
||
RESET_TOKEN_PREFIX = "reset_"
|
||
|
||
|
||
def public_reset_link(token: str) -> str:
|
||
base = (os.getenv("APP_URL") or "https://shinkan.jinkendo.de").rstrip("/")
|
||
return f"{base}/reset-password?token={token}"
|
||
|
||
|
||
def revoke_pending_password_resets_for_profile(cur: Any, profile_id: int) -> None:
|
||
"""Entfernt alte Reset-Sessions eines Profils, damit nur der neueste Link aktiv ist."""
|
||
cur.execute(
|
||
"""
|
||
DELETE FROM sessions
|
||
WHERE profile_id = %s AND token LIKE %s
|
||
""",
|
||
(profile_id, f"{RESET_TOKEN_PREFIX}%"),
|
||
)
|
||
|
||
|
||
def insert_password_reset_session(cur: Any, profile_id: int, *, hours_valid: int = 1) -> str:
|
||
"""
|
||
Legt reset_<token>-Session an. Gibt den Klartext-Token zurück (wie bei forgot-password).
|
||
"""
|
||
raw = secrets.token_urlsafe(32)
|
||
expires = datetime.now() + timedelta(hours=hours_valid)
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO sessions (token, profile_id, expires_at, created_at)
|
||
VALUES (%s, %s, %s, CURRENT_TIMESTAMP)
|
||
""",
|
||
(f"{RESET_TOKEN_PREFIX}{raw}", profile_id, expires.isoformat()),
|
||
)
|
||
return raw
|
||
|
||
|
||
def password_reset_email_body(*, recipient_name: str | None, token: str, intro: str) -> str:
|
||
name = (recipient_name or "").strip() or "Kollege/Kollegin"
|
||
link = public_reset_link(token)
|
||
return f"""Hallo {name},
|
||
|
||
{intro}
|
||
|
||
Neues Passwort setzen:
|
||
{link}
|
||
|
||
Der Link ist 1 Stunde gültig. Erst wenn du ihn nutzt und ein neues Passwort wählst, wird dein bestehendes
|
||
Passwort ersetzt — bis dahin kannst du dich wie gewohnt anmelden.
|
||
|
||
Falls du diese Anfrage nicht erwartest, ignoriere diese E-Mail; dein Zugang bleibt unverändert.
|
||
|
||
Dein Shinkan Jinkendo Team
|
||
"""
|
||
|
||
|
||
def issue_password_reset_via_email(cur: Any, send_email_fn, *, profile_id: int, email: str, name: str | None, intro: str) -> bool:
|
||
"""Session anlegen und Mail schicken (send_email_fn wie routers.auth.send_email)."""
|
||
revoke_pending_password_resets_for_profile(cur, profile_id)
|
||
raw_token = insert_password_reset_session(cur, profile_id)
|
||
body = password_reset_email_body(recipient_name=name, token=raw_token, intro=intro)
|
||
return send_email_fn(email, "Passwort-Link – Shinkan Jinkendo", body)
|