Onboarding guidé systématique + tableau de bord simplifié
Comportement SYSTÉMATIQUE à chaque création de jeu (admin compris), pas une formalité réservée à l'inscription : /onboarding (routes/onboarding/) devient le point d'entrée unique — 4 cartes retournables (survol = explication au dos), défilement horizontal animé vers le nom du jeu. Un admin y repasse à volonté (pas de project_slug dédié, jamais bloqué/ redirigé vers un projet précédent) ; un compte "user" n'en a plus qu'un créé d'office (routes/auth/register_2fa.py), guidé ici à la place. - db/games/game_type_catalog.py : catalogue des 4 types (Quiz/ Embranchement-escape game/RPG/Créer mon jeu de A à Z), _meta['onboarding_type'] décide du "kind" du premier écran créé et si le tableau de bord complet reste accessible. - routes/games/game_dashboard.py, templates/game_dashboard_simple.html : un type restreint (quiz/embranchement/rpg) voit désormais SON tableau de bord (même route que "custom"), rendu en version simplifiée — juste ses écrans en cartes avec un aperçu RÉEL du contenu (scène mise à l'échelle par container query CSS, adaptée à la largeur réelle de la carte). "+ Ajouter un écran" n'y propose pas de choix de type : imposé par le projet (routes/screens/screens_new.py), verrouillé aussi côté serveur. - core/auth_guard.py : plus de blocage de game_dashboard par type — la restriction se fait au rendu, pas à l'accès à la route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0f23024882
commit
76a50fa87a
+45
-9
@@ -34,19 +34,41 @@ def _register(client, email, prenom="Alice", nom="Test", password="Sup3r$ecret!"
|
||||
def _confirm_2fa(client):
|
||||
"""Récupère la clé TOTP affichée sur la page de confirmation et
|
||||
soumet le code du moment — comme un humain qui vient de scanner le
|
||||
QR code avec son application."""
|
||||
QR code avec son application. Un compte "user" n'a plus de projet
|
||||
créé d'office ici (voir routes/onboarding/onboarding_new.py) — la
|
||||
redirection renvoie désormais vers /onboarding, pas /game/<slug> ;
|
||||
voir _complete_onboarding ci-dessous pour les tests qui ont besoin
|
||||
d'un vrai projet ensuite."""
|
||||
html = client.get("/register/2fa").get_data(as_text=True)
|
||||
secret = re.search(r"<code>([A-Z0-9]+)</code>", html).group(1)
|
||||
code = pyotp.TOTP(secret).now()
|
||||
return client.post("/register/2fa", data={"code": code}, follow_redirects=False), secret
|
||||
|
||||
|
||||
def _complete_onboarding(client, name="Mon jeu", onboarding_type="custom"):
|
||||
"""Termine l'onboarding guidé (voir routes/onboarding/onboarding_new.py)
|
||||
— la plupart des tests de ce fichier n'ont pas besoin de tester
|
||||
l'onboarding lui-même (voir test_onboarding.py), juste d'obtenir un
|
||||
project_slug réel pour la suite ; "custom" est le choix le plus proche
|
||||
du comportement historique (tableau de bord complet)."""
|
||||
return client.post(
|
||||
"/onboarding", data={"onboarding_type": onboarding_type, "name": name}, follow_redirects=False
|
||||
)
|
||||
|
||||
|
||||
def _cleanup_project(email):
|
||||
game_dir = db.game_dir(db.slugify(email))
|
||||
import os
|
||||
import shutil
|
||||
if os.path.isdir(game_dir):
|
||||
shutil.rmtree(game_dir)
|
||||
user = auth.get_user_by_email(email)
|
||||
if user and user.get("project_slug"):
|
||||
if os.path.isdir(db.game_dir(user["project_slug"])):
|
||||
# db.delete_game (voir db/games/project_slug.py — structure de
|
||||
# dossiers par utilisateur) nettoie aussi le dossier
|
||||
# propriétaire s'il devient vide, contrairement à un simple
|
||||
# shutil.rmtree(game_dir) — sans ça, des dossiers propriétaires
|
||||
# vides s'accumulent sous projects/ d'un run de tests à l'autre
|
||||
# (déjà observé : ça a fini par provoquer une collision de nom
|
||||
# dans db.move_game, un dossier vide comptant comme "occupé").
|
||||
db.delete_game(user["project_slug"])
|
||||
|
||||
|
||||
def test_anonymous_request_redirects_to_login(anon_client):
|
||||
@@ -100,13 +122,23 @@ def test_full_registration_flow_requires_totp_before_account_is_usable(anon_clie
|
||||
user = auth.get_user_by_email("flow@example.com")
|
||||
assert user["totp_confirmed"] == 1
|
||||
assert user["role"] == "user" # jamais le premier compte de la session de tests
|
||||
assert user["project_slug"] == db.slugify("flow@example.com")
|
||||
# Onboarding guidé (voir routes/onboarding/onboarding_new.py) : plus
|
||||
# de projet créé d'office ici — connecté mais sans project_slug tant
|
||||
# qu'il n'a pas choisi un type de jeu.
|
||||
assert user["project_slug"] is None
|
||||
try:
|
||||
assert resp.headers["Location"] == "/game/" + user["project_slug"]
|
||||
# Connecté pour de vrai maintenant.
|
||||
assert resp.headers["Location"] == "/onboarding"
|
||||
# Connecté pour de vrai maintenant, mais renvoyé vers l'onboarding
|
||||
# tant qu'aucun projet n'existe (voir core/auth_guard.py).
|
||||
resp2 = anon_client.get("/", follow_redirects=False)
|
||||
assert resp2.status_code == 302
|
||||
assert resp2.headers["Location"] == "/game/" + user["project_slug"]
|
||||
assert resp2.headers["Location"] == "/onboarding"
|
||||
|
||||
onboard_resp = _complete_onboarding(anon_client)
|
||||
assert onboard_resp.status_code == 302
|
||||
user = auth.get_user_by_email("flow@example.com")
|
||||
assert user["project_slug"] is not None
|
||||
assert onboard_resp.headers["Location"] == "/game/" + user["project_slug"]
|
||||
finally:
|
||||
_cleanup_project("flow@example.com")
|
||||
|
||||
@@ -123,6 +155,7 @@ def test_wrong_totp_code_is_rejected(anon_client):
|
||||
def test_login_requires_correct_password_then_totp(anon_client):
|
||||
_register(anon_client, "login@example.com")
|
||||
resp, secret = _confirm_2fa(anon_client)
|
||||
_complete_onboarding(anon_client)
|
||||
slug = auth.get_user_by_email("login@example.com")["project_slug"]
|
||||
try:
|
||||
anon_client.post("/logout")
|
||||
@@ -146,6 +179,7 @@ def test_login_requires_correct_password_then_totp(anon_client):
|
||||
def test_non_admin_user_is_isolated_to_their_own_project(anon_client):
|
||||
_register(anon_client, "isolated@example.com")
|
||||
_confirm_2fa(anon_client)
|
||||
_complete_onboarding(anon_client)
|
||||
user = auth.get_user_by_email("isolated@example.com")
|
||||
try:
|
||||
# Un autre projet (celui de l'admin partagé par conftest.py, ou
|
||||
@@ -173,6 +207,7 @@ def test_non_admin_user_cannot_delete_their_only_project(anon_client):
|
||||
retour — bloqué plutôt que risqué."""
|
||||
_register(anon_client, "nodelete@example.com")
|
||||
_confirm_2fa(anon_client)
|
||||
_complete_onboarding(anon_client)
|
||||
user = auth.get_user_by_email("nodelete@example.com")
|
||||
try:
|
||||
resp = anon_client.post(f"/game/{user['project_slug']}/delete")
|
||||
@@ -281,6 +316,7 @@ def test_recovery_codes_are_shown_once_after_confirming_2fa(anon_client):
|
||||
def test_login_with_a_recovery_code_instead_of_totp(anon_client):
|
||||
_register(anon_client, "recoverylogin@example.com")
|
||||
_confirm_2fa(anon_client)
|
||||
_complete_onboarding(anon_client)
|
||||
user = auth.get_user_by_email("recoverylogin@example.com")
|
||||
slug = user["project_slug"]
|
||||
codes = auth.generate_recovery_codes(user["id"])
|
||||
|
||||
Reference in New Issue
Block a user