Files
Forge-Engine/core/auth_guard.py
T
williamandClaude Sonnet 5 92fbfbc9dd
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 4 : ajout dynamique d'une ligne à l'exécution
Nouveau sentinel LAST_INSERTED_ROW_ID = -3 (screens/flow/constants.py),
aux côtés de CLICKED_ROW_ID = -1 — même principe : un id de ligne
n'existe qu'APRÈS l'insertion, jamais connu à la création du nœud.
Permet d'enchaîner un nœud "Ajouter une ligne" (crée une ligne VIDE) puis
un ou plusieurs nœuds "Modifier une donnée" déjà existants (ciblant
"➕ Dernière ligne ajoutée" dans le sélecteur "Ligne concernée", partagé
avec les conditions) pour renseigner ses champs — réutilise 100% du
mécanisme actuel, aucun nouveau format de payload multi-champs.

screens/data_actions/apply_add_row_action.py : db.get_definition +
db.insert_row(slug, definition, {}, player_id) — la ligne appartient au
joueur qui agit pour un objet per_player (Phase 1).

Nouvelles routes (créateur ET publique, comme prévu dès la Phase 1) :
POST /game/<slug>/flow/nodes/<id>/run-add-row et son miroir
/jouer/<slug>/.../run-add-row — renvoient {"ok", "row_id"}.
routes/flow/flow_node_run_data.py (+ son miroir public) : résout aussi
LAST_INSERTED_ROW_ID (en plus de CLICKED_ROW_ID déjà en place) via
last_inserted_row_id transmis par le client.

static/js/play/actions.js : runActionNode branche "ajouter_ligne" ->
fetch la nouvelle route, pose window.lastInsertedRowId, puis
refreshRuntimeData() (un Répéteur lié affiche la nouvelle ligne au
prochain rendu, confirmé par l'audit préalable — aucun ajustement du
mécanisme de rafraîchissement nécessaire). La branche "modifier_donnee"
transmet désormais aussi last_inserted_row_id, comme clicked_row_id.

templates/screen_edit.html + static/js/screen_edit/flow-editor.js :
nouveau type d'action "Ajouter une ligne à un objet" (juste un
sélecteur d'objet, aucun champ à remplir — le rappel du fonctionnement
enchaîné est affiché directement dans le formulaire) ; le sélecteur
"Ligne concernée" (partagé Condition/Modifier une donnée) gagne l'option
"➕ Dernière ligne ajoutée" à côté de "🖱️ Ligne cliquée".

Vérifié : 259 tests passent (4 nouveaux, dont un bout-en-bout via HTTP
qui enchaîne réellement les deux nœuds et vérifie le champ renseigné, et
un qui verrouille l'isolation par joueur de la ligne créée), 13 tests
node:test toujours au vert, syntaxe JS validée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:18:10 +02:00

76 lines
3.5 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.
#
# game_play_public/runtime_payload_public/flow_node_run_data_public/
# flow_node_run_variable_public (routes/public_play/) : la route PUBLIQUE
# /jouer/<slug>, pour un vrai visiteur anonyme (état par joueur, Phase 1)
# — chaque vue vérifie ELLE-MÊME db.is_public_played(slug) (404 sinon),
# cette garde générique ne fait qu'autoriser l'accès sans connexion, elle
# ne dispense d'aucune autre vérification.
_PUBLIC_ENDPOINTS = {
"static", "login", "login_2fa", "register", "register_2fa", "logout",
"forgot_password", "reset_password",
"game_play_public", "runtime_payload_public",
"flow_node_run_data_public", "flow_node_run_variable_public",
"flow_node_run_add_row_public",
}
@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