- création de compte par admin envoie un email avec lien de définition (7j), bouton "Reset mdp" pour renvoyer un lien (24h). Plus aucun admin ne peut modifier directement le mdp d'un user (tout passe par les liens email). - nouvelle page /password-set publique (validation token, formulaire, hash bcrypt) au style aligné sur /login, avec emails multipart texte+HTML. - nouvelle page /profile (changement mdp avec ancien, reset 2FA, avatar, infos), accessible via dropdown sur le widget user en bas de sidebar. - restriction d'accès par utilisateur : champ allowed_classes dans auth.yaml, multi-select dans la page Users, filtrage cross-page (KPIs, sanctions, classes, apprentis, navigations cross-page, génération PDF avis). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
128 lines
4 KiB
Python
128 lines
4 KiB
Python
"""Envoi des emails de définition / réinitialisation de mot de passe."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
from src.email_sender import send_email
|
|
|
|
_ROOT = Path(__file__).resolve().parent.parent
|
|
_DATA_DIR = Path(os.getenv("DATA_DIR", str(_ROOT / "data")))
|
|
_SETTINGS_PATH = _DATA_DIR / "settings.json"
|
|
_TEMPLATES_DIR = _DATA_DIR / "email_templates"
|
|
|
|
|
|
def _load_settings() -> dict:
|
|
if _SETTINGS_PATH.exists():
|
|
try:
|
|
return json.loads(_SETTINGS_PATH.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def _read_template(name: str) -> str:
|
|
path = _TEMPLATES_DIR / name
|
|
if not path.exists():
|
|
raise FileNotFoundError(
|
|
f"Template manquant : {path}. Créez data/email_templates/{name}."
|
|
)
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _read_optional_template(name: str) -> str | None:
|
|
path = _TEMPLATES_DIR / name
|
|
if not path.exists():
|
|
return None
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _format_expiry(dt: datetime) -> str:
|
|
return dt.strftime("%d.%m.%Y à %H:%M")
|
|
|
|
|
|
def _format_ttl_human(dt: datetime) -> str:
|
|
"""Renvoie 'dans X h' ou 'dans X jours'."""
|
|
delta = dt - datetime.now()
|
|
total_seconds = max(0, int(delta.total_seconds()))
|
|
if total_seconds < 3600:
|
|
mins = total_seconds // 60
|
|
return f"valable {mins} minute{'s' if mins > 1 else ''}"
|
|
hours = total_seconds // 3600
|
|
if hours < 48:
|
|
return f"valable {hours} heure{'s' if hours > 1 else ''}"
|
|
days = hours // 24
|
|
return f"valable {days} jour{'s' if days > 1 else ''}"
|
|
|
|
|
|
def send_password_email(
|
|
*,
|
|
kind: str, # "welcome" | "reset"
|
|
username: str,
|
|
name: str,
|
|
email: str,
|
|
token: str,
|
|
expires_at: datetime,
|
|
) -> None:
|
|
"""Envoie un email de définition (kind='welcome') ou de reset (kind='reset').
|
|
|
|
Lève une exception en cas d'erreur (SMTP, template manquant, base_url manquante).
|
|
"""
|
|
if kind not in ("welcome", "reset"):
|
|
raise ValueError(f"kind must be 'welcome' or 'reset', got {kind!r}")
|
|
if not email or "@" not in email:
|
|
raise ValueError(f"Email invalide pour {username!r}")
|
|
|
|
settings = _load_settings()
|
|
base_url = (settings.get("app_base_url") or "").rstrip("/")
|
|
if not base_url:
|
|
raise ValueError(
|
|
"URL de base manquante. Configurez `app_base_url` dans Paramètres."
|
|
)
|
|
|
|
smtp_host = settings.get("smtp_host")
|
|
smtp_port = int(settings.get("smtp_port") or 587)
|
|
smtp_login = settings.get("smtp_login")
|
|
smtp_password = settings.get("smtp_password")
|
|
smtp_sender = settings.get("smtp_sender")
|
|
if not (smtp_host and smtp_login and smtp_password and smtp_sender):
|
|
raise ValueError(
|
|
"Configuration SMTP incomplète. Vérifiez Paramètres → SMTP."
|
|
)
|
|
|
|
link = f"{base_url}/password-set?token={quote(token)}"
|
|
vars_ = {
|
|
"name": name,
|
|
"username": username,
|
|
"link": link,
|
|
"expiry": _format_expiry(expires_at),
|
|
"ttl_human": _format_ttl_human(expires_at),
|
|
}
|
|
template_text_name = "welcome.txt" if kind == "welcome" else "reset.txt"
|
|
template_html_name = "welcome.html" if kind == "welcome" else "reset.html"
|
|
body_text = _read_template(template_text_name).format_map(vars_)
|
|
html_raw = _read_optional_template(template_html_name)
|
|
body_html = html_raw.format_map(vars_) if html_raw else None
|
|
|
|
subject = (
|
|
"EPTM Dashboard — Définissez votre mot de passe"
|
|
if kind == "welcome"
|
|
else "EPTM Dashboard — Réinitialisation de votre mot de passe"
|
|
)
|
|
|
|
send_email(
|
|
smtp_host=smtp_host,
|
|
smtp_port=smtp_port,
|
|
smtp_login=smtp_login,
|
|
smtp_password=smtp_password,
|
|
smtp_sender=smtp_sender,
|
|
to_email=email,
|
|
subject=subject,
|
|
body=body_text,
|
|
body_html=body_html,
|
|
attachments=None,
|
|
)
|