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>
53 lines
2.2 KiB
Python
53 lines
2.2 KiB
Python
"""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)
|