Envoie les emails via un relais SMTP configurable plutôt qu'en direct-to-MX

Ajoute le bloc "smtp" (host, port, identifiants, TLS) dans config.json,
plus fiable et plus simple à opérer qu'une livraison directe au MX du
destinataire. data/config.json n'est plus versionné (secrets), remplacé
par data/config.example.json.
This commit is contained in:
Antoine
2026-07-17 21:46:52 +02:00
parent 1d9380736e
commit e6a523f76f
10 changed files with 297 additions and 64 deletions

View File

@ -1,7 +1,6 @@
"""Flat-file storage: JSON files per week for pointages, one file for congés."""
import json
import secrets
import shutil
from pathlib import Path
DATA_DIR = Path("/data")
@ -9,17 +8,20 @@ CONFIG_FILE = DATA_DIR / "config.json"
DEFAULT_CONFIG = {
"heures_jour": 7.8, # 7h48 = 39h/semaine
"allowed_email_domain": "", # ex: "sdv.fr" — seules ces adresses peuvent se connecter
"mail_from": "", # ex: "no-reply@sdv.fr" — expéditeur des emails de connexion
"smtp": {
"host": "", # ex: "smtp.sdv.fr"
"port": 587,
"user": "",
"password": "",
"use_tls": True,
},
}
# ── User path helpers ──────────────────────────────────────────────────────────
def _user_root(user_id: str) -> Path:
"""Return data directory for a user.
Legacy fallback: if no users/ directory exists yet but /data/pointages/ does,
the first login migrates the data automatically (see migrate_legacy_to_user).
After migration, each user gets /data/users/{user_id}/.
"""
return DATA_DIR / "users" / user_id
@ -35,29 +37,6 @@ def _ensure_user_dirs(user_id: str):
_pointages_dir(user_id).mkdir(parents=True, exist_ok=True)
# ── Legacy migration (called once on first login) ─────────────────────────────
def needs_migration() -> bool:
"""True when legacy flat data exists and no users/ directory has been created yet."""
return (DATA_DIR / "pointages").exists() and not (DATA_DIR / "users").exists()
def migrate_legacy_to_user(user_id: str):
"""Move /data/pointages/ and /data/conges.json into /data/users/{user_id}/."""
user_root = _user_root(user_id)
user_root.mkdir(parents=True, exist_ok=True)
legacy_pt = DATA_DIR / "pointages"
dest_pt = user_root / "pointages"
if legacy_pt.exists() and not dest_pt.exists():
shutil.copytree(legacy_pt, dest_pt)
legacy_cg = DATA_DIR / "conges.json"
dest_cg = user_root / "conges.json"
if legacy_cg.exists() and not dest_cg.exists():
shutil.copy2(legacy_cg, dest_cg)
# ── User list ─────────────────────────────────────────────────────────────────
def list_users() -> list[str]:
@ -126,6 +105,33 @@ def save_conges(conges: list[dict], user_id: str = ""):
)
# ── Auth (email + password) ────────────────────────────────────────────────────
def _auth_file(user_id: str) -> Path:
return _user_root(user_id) / "auth.json"
def load_auth(user_id: str) -> dict:
f = _auth_file(user_id)
if f.exists():
return json.loads(f.read_text())
return {"email": None, "password_hash": None, "token": None, "token_expires": None}
def save_auth(user_id: str, **fields):
auth = load_auth(user_id)
auth.update(fields)
_ensure_user_dirs(user_id)
_auth_file(user_id).write_text(json.dumps(auth, indent=2, ensure_ascii=False))
def find_user_by_reset_token(token: str) -> str | None:
for user_id in list_users():
if load_auth(user_id).get("token") == token:
return user_id
return None
# ── Notifications (ntfy topic + presence tracking) ────────────────────────────
def _notif_file(user_id: str) -> Path: