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
@@ -19,9 +19,18 @@ 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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Identité d'un visiteur anonyme de la route publique /jouer/<slug> (voir
|
||||
routes/public_play/) — état par joueur, Phase 1. Un cookie NON SIGNÉ (pas
|
||||
besoin de itsdangerous/app.secret_key ici) : c'est une simple clé de
|
||||
PARTITION des données (quelles lignes/valeurs appartiennent à ce visiteur),
|
||||
jamais un jeton d'autorisation — la contrefaçon la plus grave possible
|
||||
serait de se faire passer pour un AUTRE joueur anonyme du même jeu, pas de
|
||||
franchir une frontière de sécurité (l'aperçu créateur, lui, reste protégé
|
||||
par core/auth_guard.py, complètement indépendant de ce mécanisme)."""
|
||||
import secrets
|
||||
|
||||
from flask import request
|
||||
|
||||
PLAYER_ID_COOKIE = "forge_player_id"
|
||||
_PLAYER_ID_MAX_AGE = 60 * 60 * 24 * 365 # 1 an
|
||||
|
||||
|
||||
def get_or_create_player_id():
|
||||
"""Renvoie (player_id, is_new) — is_new indique s'il faut poser le
|
||||
cookie sur la réponse avant de la renvoyer (voir
|
||||
set_player_id_cookie ci-dessous) : à utiliser aussi bien depuis une
|
||||
route qui rend une page (GET /jouer/<slug>) qu'une route API JSON
|
||||
(runtime-payload, run-data, run-variable), dont la réponse n'est
|
||||
construite qu'APRÈS avoir déjà besoin du player_id pour agir."""
|
||||
existing = request.cookies.get(PLAYER_ID_COOKIE)
|
||||
if existing:
|
||||
return existing, False
|
||||
return secrets.token_urlsafe(16), True
|
||||
|
||||
|
||||
def set_player_id_cookie(response, player_id):
|
||||
response.set_cookie(
|
||||
PLAYER_ID_COOKIE, player_id,
|
||||
max_age=_PLAYER_ID_MAX_AGE, httponly=True, samesite="Lax",
|
||||
)
|
||||
return response
|
||||
Reference in New Issue
Block a user