shinkan-jinkendo/backend/password_reset_mail.py
Lars 32b28a93f3
Some checks failed
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Failing after 5m43s
Harden login, sessions, and media access before the dev deploy.
Limit brute-force attempts, require admin TOTP, and keep session tokens out of media URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 07:45:53 +02:00

69 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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, purpose)
VALUES (%s, %s, %s, CURRENT_TIMESTAMP, 'password_reset')
""",
(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)