Ajoute la réinitialisation de mot de passe par email (SMTP)

Un lien "Mot de passe oublié ?" (login.html) mène à /forgot-password :
si l'adresse saisie correspond à un compte, un jeton à haute entropie
(secrets.token_urlsafe, valable 1h) est généré et envoyé par email via
smtplib (auth/send_email.py, aucune dépendance ajoutée) — configuré
uniquement par variables d'environnement (SMTP_HOST/PORT/USER/PASSWORD/
FROM, voir .env.example et docker-compose.prod.yml), n'importe quel
serveur SMTP existant convient (Mailcow compris).

Seul le hash SHA-256 du jeton est stocké (auth/password_reset.py, table
_password_reset_tokens) : un jeton envoyé par email reste inutilisable
même en cas de fuite de la base. Le même message générique s'affiche que
l'adresse corresponde à un compte ou non, pour ne jamais permettre à ce
formulaire de servir à deviner quelles adresses sont déjà inscrites. Un
échec d'envoi (SMTP non configuré) est journalisé côté serveur seulement,
jamais révélé à l'utilisateur.

/reset-password/<token> vérifie le jeton (non expiré, non déjà utilisé),
applique les mêmes règles de mot de passe fort qu'à l'inscription (même
schéma visuel), puis consomme le jeton et remet à zéro le compteur
anti-bruteforce du compte (auth/set_password.py) — une identité prouvée
par email est une voie de récupération légitime même pour un compte
verrouillé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-28 20:50:49 +02:00
co-authored by Claude Sonnet 5
parent 9efe119936
commit d5a84413d1
15 changed files with 418 additions and 2 deletions
+52
View File
@@ -0,0 +1,52 @@
"""Envoi d'email via LE SERVEUR SMTP DE L'UTILISATEUR (Mailcow ou autre) —
aucune dépendance supplémentaire, smtplib est dans la bibliothèque standard
Python. Configuré uniquement par variables d'environnement (SMTP_HOST,
SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM) : ce module ne connaît et
ne stocke jamais le mot de passe SMTP en dur, à définir sur le poste/
serveur qui fait tourner l'appli."""
import os
import smtplib
from email.mime.text import MIMEText
from .password_reset import TOKEN_TTL_MINUTES
class EmailNotConfiguredError(Exception):
"""Levée si les variables d'environnement SMTP_* ne sont pas toutes
définies — jamais renvoyée telle quelle à l'utilisateur (voir
routes/auth/forgot_password.py, qui affiche toujours le même message
générique), seulement journalisée côté serveur pour que l'admin sache
qu'il doit finir de configurer son SMTP."""
def send_email(to_email, subject, body):
host = os.environ.get("SMTP_HOST")
port = int(os.environ.get("SMTP_PORT", "587"))
user = os.environ.get("SMTP_USER")
password = os.environ.get("SMTP_PASSWORD")
sender = os.environ.get("SMTP_FROM") or user
if not host or not user or not password or not sender:
raise EmailNotConfiguredError(
"SMTP_HOST / SMTP_USER / SMTP_PASSWORD / SMTP_FROM doivent être "
"définis en variables d'environnement pour envoyer des emails."
)
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = sender
msg["To"] = to_email
with smtplib.SMTP(host, port, timeout=10) as server:
server.starttls()
server.login(user, password)
server.sendmail(sender, [to_email], msg.as_string())
def send_password_reset_email(to_email, reset_url):
body = (
"Une réinitialisation de mot de passe a été demandée pour ce compte "
"Forge Engine.\n\n"
f"Clique sur ce lien pour choisir un nouveau mot de passe "
f"(valable {TOKEN_TTL_MINUTES} minutes) :\n{reset_url}\n\n"
"Si tu n'es pas à l'origine de cette demande, ignore cet email : "
"ton mot de passe actuel reste inchangé."
)
send_email(to_email, "Réinitialisation de ton mot de passe — Forge Engine", body)