Phase 1 (2/3) : route publique /jouer/<slug> + identité visiteur
Ajoute la vraie route hébergée multi-joueurs qui manquait totalement
(voir le constat d'exploration : /game/<slug>/play est réservé au
créateur connecté, "Publier" ne génère qu'un exécutable mono-joueur) —
un visiteur anonyme peut maintenant jouer un jeu explicitement publié en
ligne, avec sa propre partie (variables/objets per_player posés dans le
commit précédent).
core/player_identity.py : identité visiteur via un cookie NON SIGNÉ
(forge_player_id, secrets.token_urlsafe(16), 1 an) — une simple clé de
partition, jamais un jeton d'autorisation.
routes/public_play/ : 4 routes, miroirs des routes existantes de l'aperçu
créateur mais threadées avec le vrai player_id du cookie au lieu du
sentinel PLAYER_SHARED :
- GET /jouer/<slug> (game_play_public.py)
- GET /jouer/<slug>/runtime-payload
- POST /jouer/<slug>/flow/nodes/<id>/run-data
- POST /jouer/<slug>/flow/nodes/<id>/run-variable
Chacune vérifie elle-même db.is_public_played(slug) (404 sinon) — un jeu
n'est exposé publiquement que si le créateur l'a explicitement basculé
"Publier en ligne" (nouveau db/games/is_public_played.py, réutilise la
table générique _meta, comme game_meta.py pour 'name').
core/auth_guard.py : les 4 endpoints publics ajoutés à _PUBLIC_ENDPOINTS
— la garde générique de connexion les laisse passer sans session, mais
chaque vue vérifie quand même is_public_played elle-même (défense en
profondeur, pas seulement une liste d'exceptions). CSRF (core/csrf_guard.py)
n'a besoin d'AUCUN changement : le jeton est déjà lié à la session Flask,
qui existe pour n'importe quel visiteur (connecté ou non).
templates/play.html : FORGE_PLAY_URLS (posé en Phase -1) ne construit
plus ses URLs via des noms de endpoint fixes (url_for('runtime_payload',
...)) mais reçoit des URLs déjà résolues par la route elle-même
(runtime_payload_url/flow_node_run_data_url/flow_node_run_variable_url)
— nécessaire puisque ce même template sert maintenant DEUX familles de
routes (aperçu créateur ET partie publique), chacune avec ses propres
noms de endpoint. routes/play/game_play.py (aperçu créateur, INCHANGÉ
comportement) et game_play_public.py passent chacun ses propres URLs.
templates/base.html : bascule "🌐 Publier en ligne" dans la barre de
navigation du jeu, à côté de "📦 Publier" (export .zip) — deux
fonctionnalités distinctes. db/games/game_meta.py expose maintenant
is_public_played, disponible partout où `game` est dans le contexte.
Vérifié : 237 tests passent (5 nouveaux dans test_public_play.py, dont un
bout-en-bout via HTTP avec deux VRAIS clients de test anonymes — deux
cookies forge_player_id différents — qui obtiennent des valeurs de
variable indépendantes, et un qui verrouille que l'aperçu créateur reste
inchangé). Syntaxe JS validée sur les deux variantes de play.html rendu
(aperçu créateur et partie publique).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
236d6b4b46
commit
3c39f1a249
+1
-1
@@ -1 +1 @@
|
||||
from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks
|
||||
from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks, public_play
|
||||
|
||||
@@ -1 +1 @@
|
||||
from . import index, games_new, game_dashboard, game_rename, game_delete
|
||||
from . import index, games_new, game_dashboard, game_rename, game_delete, game_toggle_public_played
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from flask import request, redirect, url_for
|
||||
|
||||
import db
|
||||
|
||||
from core.flask_app import app
|
||||
|
||||
|
||||
@app.route("/game/<slug>/toggle-public-played", methods=["POST"])
|
||||
def game_toggle_public_played(slug):
|
||||
"""Bascule "Publier en ligne" (voir db/games/is_public_played.py) —
|
||||
active/désactive la route publique /jouer/<slug> (routes/public_play/),
|
||||
jouable par n'importe quel visiteur anonyme avec sa propre partie
|
||||
(état par joueur, Phase 1). Distinct de "Publier" (export .zip
|
||||
exécutable mono-joueur, voir routes/publish/publish_game.py)."""
|
||||
db.set_public_played(slug, request.form.get("value") == "1")
|
||||
return redirect(request.referrer or url_for("game_dashboard", slug=slug))
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
|
||||
from flask import render_template
|
||||
from flask import render_template, url_for
|
||||
|
||||
import db
|
||||
import screens
|
||||
@@ -10,8 +10,15 @@ from core.flask_app import app
|
||||
|
||||
@app.route("/game/<slug>/play")
|
||||
def game_play(slug):
|
||||
"""Aperçu créateur (connecté, voir core/auth_guard.py) — une seule
|
||||
partie partagée (PLAYER_SHARED, le défaut de full_game_payload), comme
|
||||
avant l'état par joueur (Phase 1). Distincte de /jouer/<slug>
|
||||
(routes/public_play/), la vraie route publique par joueur."""
|
||||
game = db.game_meta(slug)
|
||||
payload = screens.full_game_payload(slug)
|
||||
return render_template(
|
||||
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
||||
runtime_payload_url=url_for("runtime_payload", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable", slug=slug, node_id=999999),
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import game_play_public, runtime_payload_public, flow_node_run_data_public, flow_node_run_variable_public
|
||||
@@ -0,0 +1,29 @@
|
||||
from flask import jsonify, request, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/flow/nodes/<int:node_id>/run-data", methods=["POST"])
|
||||
def flow_node_run_data_public(slug, node_id):
|
||||
"""Miroir public de routes/flow/flow_node_run_data.py (aperçu créateur)
|
||||
— voir routes/public_play/game_play_public.py pour le principe
|
||||
général. `player_id` (le cookie du visiteur) est passé à
|
||||
apply_data_action pour qu'un objet per_player agisse sur SA ligne."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
|
||||
return jsonify({"ok": False}), 400
|
||||
if node.get("target_row_id") == screens.CLICKED_ROW_ID:
|
||||
body = request.get_json(silent=True) or {}
|
||||
node = {**node, "target_row_id": body.get("clicked_row_id")}
|
||||
ok = screens.apply_data_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": ok}))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask import jsonify, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/flow/nodes/<int:node_id>/run-variable", methods=["POST"])
|
||||
def flow_node_run_variable_public(slug, node_id):
|
||||
"""Miroir public de routes/flow/flow_node_run_variable.py (aperçu
|
||||
créateur) — voir routes/public_play/game_play_public.py pour le
|
||||
principe général."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
node = screens.get_flow_node(slug, node_id)
|
||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_variable":
|
||||
return jsonify({"ok": False}), 400
|
||||
ok = screens.apply_variable_action(slug, node, player_id)
|
||||
resp = app.make_response(jsonify({"ok": ok}))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
|
||||
from flask import render_template, url_for, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>")
|
||||
def game_play_public(slug):
|
||||
"""Route PUBLIQUE, non authentifiée (voir core/auth_guard.py) —
|
||||
distincte de /game/<slug>/play (aperçu créateur, connecté, INCHANGÉE) :
|
||||
un vrai visiteur anonyme, identifié par un cookie (voir
|
||||
core/player_identity.py), avec sa propre partie (variables/objets
|
||||
per_player, voir db/global_vars/, db/rows/). 404 si le créateur n'a
|
||||
pas explicitement basculé "Publier en ligne" dans son tableau de bord
|
||||
— jamais exposé par défaut."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
game = db.game_meta(slug)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
payload = screens.full_game_payload(slug, player_id)
|
||||
resp = app.make_response(
|
||||
render_template(
|
||||
"play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload),
|
||||
runtime_payload_url=url_for("runtime_payload_public", slug=slug),
|
||||
flow_node_run_data_url=url_for("flow_node_run_data_public", slug=slug, node_id=999999),
|
||||
flow_node_run_variable_url=url_for("flow_node_run_variable_public", slug=slug, node_id=999999),
|
||||
)
|
||||
)
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
@@ -0,0 +1,20 @@
|
||||
from flask import jsonify, abort
|
||||
|
||||
import db
|
||||
import screens
|
||||
|
||||
from core.flask_app import app
|
||||
from core.player_identity import get_or_create_player_id, set_player_id_cookie
|
||||
|
||||
|
||||
@app.route("/jouer/<slug>/runtime-payload")
|
||||
def runtime_payload_public(slug):
|
||||
"""Miroir public de routes/flow/runtime_payload.py (aperçu créateur) —
|
||||
voir routes/public_play/game_play_public.py pour le principe général."""
|
||||
if not db.is_public_played(slug):
|
||||
abort(404)
|
||||
player_id, is_new = get_or_create_player_id()
|
||||
resp = app.make_response(jsonify(screens.full_game_payload(slug, player_id)))
|
||||
if is_new:
|
||||
set_player_id_cookie(resp, player_id)
|
||||
return resp
|
||||
Reference in New Issue
Block a user