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)
|