Files
Forge-Engine/core/auth_guard.py
T
williamandClaude Sonnet 5 d90abc827b Ajoute l'authentification : inscription, mot de passe fort, 2FA obligatoire, isolation par utilisateur
Première des deux grandes fonctionnalités demandées (authentification
d'abord, export HTML/CSS/JS autonome ensuite) :

- Inscription (nom, prénom, email UNIQUE, mot de passe) avec schéma
  visuel du mot de passe (jauge + liste de critères qui passent au vert
  en direct — auth/password_strength.py, mêmes règles vérifiées côté
  serveur qu'affichées côté client).
- Double authentification (TOTP, compatible Google Authenticator/Authy)
  OBLIGATOIRE dès l'inscription : QR code (SVG, sans dépendance Pillow)
  à scanner puis code à confirmer avant que le compte soit utilisable —
  voir auth/create_user.py (totp_confirmed) et routes/auth/register_2fa.py.
- Connexion en 2 temps (mot de passe puis code TOTP), déconnexion.
- Isolation par utilisateur : un compte "user" est limité à un SEUL
  projet, dont le dossier est nommé d'après son adresse email (slugifiée)
  et créé automatiquement dès la 2FA confirmée — aucune page de gestion
  multi-jeux pour lui (redirigé directement vers son propre tableau de
  bord). Le rôle "admin" reste illimité, comme le moteur l'a toujours été
  (le TOUT PREMIER compte jamais créé sur une base de comptes vide devient
  automatiquement admin — voir auth/is_first_user.py — pas de mot de
  passe par défaut à faire circuler : s'inscrire en premier suffit).
  Un compte "user" ne peut pas non plus supprimer son unique projet
  (aucune façon d'en recréer un ensuite).
- Garde d'accès globale (core/auth_guard.py, un seul before_request) :
  toute page exige une connexion, sans avoir touché individuellement aux
  ~80 routes déjà existantes du moteur.

tests/conftest.py isole complètement les tests de la vraie base de
comptes (FORGE_USERS_DB_PATH/FORGE_SECRET_KEY_PATH vers un dossier
temporaire propre à la session de tests) et authentifie automatiquement
la fixture `client` partagée en tant que compte admin de test — les 155
tests déjà existants continuent de passer SANS AUCUNE modification de
leur côté, exactement comme avant l'authentification. 10 nouveaux tests
dédiés (tests/test_auth.py) : inscription/mots de passe/2FA/connexion/
isolation par projet/blocage de suppression, vérifiés en conditions
réelles (vrai client de test Flask, vraie base SQLite, vrais codes TOTP
calculés avec pyotp). 165 tests au total, tous au vert.

Nouvelles dépendances : pyotp, qrcode (requirements.txt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 19:30:43 +02:00

65 lines
2.9 KiB
Python

"""Garde d'accès globale — connexion obligatoire pour tout le moteur, et
isolation par utilisateur d'un SEUL projet (sauf le rôle "admin",
illimité comme avant l'authentification). Un seul before_request plutôt
qu'un décorateur à poser sur chacune des ~80 routes existantes : moins de
risque d'en oublier une, et aucun fichier de routes existant n'a besoin
d'être modifié.
Cet import doit avoir lieu APRÈS `import routes` (voir app.py/conftest.py)
pour que `app.url_map` connaisse déjà toutes les routes au moment où ce
module tente de résoudre request.endpoint — en pratique sans importance
ici (la résolution se fait à la requête, pas à l'import), mais gardé pour
rester cohérent avec l'ordre d'import du reste du moteur."""
from flask import g, redirect, request, session, url_for, abort
import auth
from .flask_app import app
# Endpoints accessibles SANS être connecté — tout le reste exige une
# session valide. "static" (CSS/JS/images) doit rester public : la page
# de connexion elle-même en a besoin pour s'afficher.
_PUBLIC_ENDPOINTS = {
"static", "login", "login_2fa", "register", "register_2fa", "logout",
}
@app.before_request
def _require_login_and_enforce_project_isolation():
endpoint = request.endpoint
if endpoint is None or endpoint in _PUBLIC_ENDPOINTS:
return None
user_id = session.get("user_id")
if not user_id:
return redirect(url_for("login", next=request.path))
user = auth.get_user_by_id(user_id)
if not user:
# Compte supprimé/introuvable depuis la dernière requête : la
# session ne doit jamais rester "connectée" dans le vide.
session.clear()
return redirect(url_for("login"))
g.current_user = user
# Un compte "user" n'a accès qu'à SON SEUL projet (project_slug) — un
# "admin" reste illimité, exactement comme avant l'authentification.
# Les routes de gestion multi-jeux (page d'accueil, "+ Nouveau jeu")
# n'ont pas leur place pour un compte à projet unique : redirigées
# directement vers son propre tableau de bord plutôt qu'un 403 sec.
if user["role"] != "admin":
if endpoint in ("index", "games_new") and user.get("project_slug"):
return redirect(url_for("game_dashboard", slug=user["project_slug"]))
slug = request.view_args.get("slug") if request.view_args else None
if slug is not None and slug != user.get("project_slug"):
abort(403)
if endpoint == "game_delete":
# Un compte "user" n'a qu'UN SEUL projet, et aucun moyen d'en
# recréer un une fois supprimé (games_new le renvoie toujours
# vers project_slug, même si le dossier n'existe plus) —
# bloqué plutôt que de risquer de le laisser sans projet du
# tout. Un admin, lui, peut toujours supprimer ses jeux comme
# avant (aucune restriction ci-dessus pour ce rôle).
abort(403)
return None