Ajoute une journalisation détaillée (login, mail, présence, rappels)

Niveau configurable via LOG_LEVEL (défaut DEBUG). Les erreurs de config
JSON invalide remontent maintenant un message clair (fichier + position)
au lieu d'un JSONDecodeError brut.
This commit is contained in:
Antoine
2026-07-17 22:03:10 +02:00
parent e6a523f76f
commit 2d4179169b
4 changed files with 84 additions and 10 deletions

View File

@ -1,5 +1,7 @@
import asyncio
import json
import logging
import os
import re
import secrets
from datetime import datetime, timedelta
@ -23,9 +25,28 @@ from stats import compute_all_stats
from classement import compute_classement
from notifications import reminder_loop
logging.basicConfig(
level=getattr(logging, os.environ.get("LOG_LEVEL", "DEBUG").upper(), logging.DEBUG),
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger("pointeuse")
app = FastAPI(title="Décompte Horaire")
@app.on_event("startup")
async def _log_config_at_startup():
cfg = load_config()
smtp = cfg.get("smtp", {})
logger.info(
"config chargée: heures_jour=%r allowed_email_domain=%r mail_from=%r "
"smtp.host=%r smtp.port=%r smtp.user=%r smtp.password=%s smtp.use_tls=%r",
cfg.get("heures_jour"), cfg.get("allowed_email_domain"), cfg.get("mail_from"),
smtp.get("host"), smtp.get("port"), smtp.get("user"),
"***" if smtp.get("password") else "(vide)", smtp.get("use_tls"),
)
@app.on_event("startup")
async def _start_reminder_loop():
asyncio.create_task(reminder_loop())
@ -251,7 +272,9 @@ def login_submit(request: Request, email: str = Form("")):
email = email.strip().lower()
m = _EMAIL_RE.match(email)
domain = allowed_email_domain()
logger.info("POST /login: email=%r domaine_attendu=%r", email, domain)
if not m or not domain or m.group(2) != domain:
logger.warning("login refusé (domaine invalide): email=%r", email)
return templates.TemplateResponse("login.html", _login_ctx(
request, "email", email,
f"Seules les adresses @{domain or '?'} sont autorisées.",
@ -259,6 +282,7 @@ def login_submit(request: Request, email: str = Form("")):
user_id = m.group(1)
auth = load_auth(user_id)
logger.debug("login: user_id=%r compte_existant=%s", user_id, bool(auth.get("password_hash")))
if auth.get("password_hash"):
return templates.TemplateResponse("login.html", _login_ctx(request, "password", email))
@ -267,6 +291,7 @@ def login_submit(request: Request, email: str = Form("")):
token = secrets.token_urlsafe(32)
expires = (datetime.now() + timedelta(hours=RESET_TOKEN_VALIDITY_HOURS)).isoformat()
save_auth(user_id, email=email, token=token, token_expires=expires)
logger.info("nouveau token de création de compte pour %r (expire %s)", user_id, expires)
base_url = str(request.base_url).rstrip("/")
link = f"{base_url}/set-password/{token}"
@ -279,6 +304,7 @@ def login_submit(request: Request, email: str = Form("")):
smtp_config(),
)
except Exception:
logger.exception("échec envoi email d'invitation à %r", email)
return templates.TemplateResponse("login.html", _login_ctx(
request, "email", email, "Impossible d'envoyer l'email pour le moment, réessaie plus tard.",
))
@ -291,16 +317,20 @@ def login_password(request: Request, email: str = Form(""), password: str = Form
email = email.strip().lower()
m = _EMAIL_RE.match(email)
domain = allowed_email_domain()
logger.info("POST /login/password: email=%r", email)
if not m or not domain or m.group(2) != domain:
logger.warning("login/password refusé (domaine invalide): email=%r", email)
return RedirectResponse("/login", status_code=302)
user_id = m.group(1)
ph = load_auth(user_id).get("password_hash")
if not ph or not bcrypt.checkpw(password.encode(), ph.encode()):
logger.warning("mot de passe incorrect pour %r", user_id)
return templates.TemplateResponse("login.html", _login_ctx(
request, "password", email, "Mot de passe incorrect.",
))
logger.info("connexion réussie: user_id=%r", user_id)
resp = RedirectResponse("/", status_code=302)
resp.set_cookie("user_id", user_id, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 30)
return resp
@ -328,10 +358,12 @@ def set_password_page(request: Request, token: str):
def set_password_submit(request: Request, token: str, password: str = Form(""), password2: str = Form("")):
user_id = _valid_reset_token(token)
if not user_id:
logger.warning("POST /set-password: token invalide ou expiré")
return templates.TemplateResponse("set_password.html", {
"request": request, "token": token, "valid": False, "error": None,
})
if len(password) < 8 or password != password2:
logger.info("POST /set-password: mot de passe rejeté (règles) pour %r", user_id)
return templates.TemplateResponse("set_password.html", {
"request": request, "token": token, "valid": True,
"error": "Les mots de passe ne correspondent pas ou font moins de 8 caractères.",
@ -339,6 +371,7 @@ def set_password_submit(request: Request, token: str, password: str = Form(""),
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
save_auth(user_id, password_hash=password_hash, token=None, token_expires=None)
logger.info("mot de passe défini pour %r", user_id)
resp = RedirectResponse("/", status_code=302)
resp.set_cookie("user_id", user_id, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 30)
@ -346,7 +379,8 @@ def set_password_submit(request: Request, token: str, password: str = Form(""),
@app.get("/logout")
def logout():
def logout(request: Request):
logger.info("déconnexion: user_id=%r", _get_user(request))
resp = RedirectResponse("/login", status_code=302)
resp.delete_cookie("user_id")
return resp
@ -506,6 +540,7 @@ def settings_save(request: Request, ntfy_topic: str = Form("")):
if not user_id:
return RedirectResponse("/login", status_code=302)
logger.info("réglages notif mis à jour: user_id=%r ntfy_topic=%r", user_id, ntfy_topic)
save_notif_config(user_id, ntfy_topic)
return RedirectResponse("/settings?saved=1", status_code=303)
@ -514,7 +549,9 @@ def settings_save(request: Request, ntfy_topic: str = Form("")):
def presence_arrivee(token: str):
user_id = find_user_by_token(token)
if not user_id:
logger.warning("GET /presence/.../arrivee: token inconnu")
return JSONResponse({"error": "invalid token"}, status_code=404)
logger.info("présence: arrivée détectée pour %r", user_id)
save_presence(user_id, present=True, since=datetime.now().isoformat())
return {"ok": True}
@ -523,7 +560,9 @@ def presence_arrivee(token: str):
def presence_depart(token: str):
user_id = find_user_by_token(token)
if not user_id:
logger.warning("GET /presence/.../depart: token inconnu")
return JSONResponse({"error": "invalid token"}, status_code=404)
logger.info("présence: départ détecté pour %r", user_id)
save_presence(user_id, present=False, since=datetime.now().isoformat())
return {"ok": True}