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>
76 lines
3.5 KiB
Python
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
|