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:
41
app/main.py
41
app/main.py
@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user