From 0a028bb499c33b11ae4765b0e608a6ba91f3f925 Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 27 Feb 2026 23:32:47 +0100 Subject: [PATCH] feat(ui): require session_token for guess submit (#39) --- lobby/templates/lobby/player_screen.html | 2 +- lobby/tests.py | 37 ++++++++++++++++++++---- lobby/views.py | 7 +++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lobby/templates/lobby/player_screen.html b/lobby/templates/lobby/player_screen.html index e5e566f..9705e84 100644 --- a/lobby/templates/lobby/player_screen.html +++ b/lobby/templates/lobby/player_screen.html @@ -22,6 +22,6 @@ async function api(path,method,payload){var o={method:method||"GET",headers:{"Ac function joinSession(){return api("/lobby/sessions/join","POST",{code:code(),nickname:document.getElementById("nickname").value.trim()});} function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);} function submitLie(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/lies/submit","POST",{player_id:parseInt(pid(),10),session_token:document.getElementById("sessionToken").value,text:document.getElementById("lieText").value});} -function submitGuess(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/guesses/submit","POST",{player_id:parseInt(pid(),10),selected_text:document.getElementById("guessText").value});} +function submitGuess(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/guesses/submit","POST",{player_id:parseInt(pid(),10),session_token:document.getElementById("sessionToken").value,selected_text:document.getElementById("guessText").value});} diff --git a/lobby/tests.py b/lobby/tests.py index 07c73b7..157e3a1 100644 --- a/lobby/tests.py +++ b/lobby/tests.py @@ -442,7 +442,7 @@ class GuessSubmissionTests(TestCase): "lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ), - data={"player_id": self.player.id, "selected_text": "Mars"}, + data={"player_id": self.player.id, "session_token": self.player.session_token, "selected_text": "Mars"}, content_type="application/json", ) @@ -461,7 +461,7 @@ class GuessSubmissionTests(TestCase): "lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ), - data={"player_id": self.player.id, "selected_text": "Mars"}, + data={"player_id": self.player.id, "session_token": self.player.session_token, "selected_text": "Mars"}, content_type="application/json", ) @@ -474,7 +474,7 @@ class GuessSubmissionTests(TestCase): "lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ), - data={"player_id": self.player.id, "selected_text": "Venus"}, + data={"player_id": self.player.id, "session_token": self.player.session_token, "selected_text": "Venus"}, content_type="application/json", ) @@ -489,7 +489,7 @@ class GuessSubmissionTests(TestCase): "lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ), - data={"player_id": self.player.id, "selected_text": "Jupiter"}, + data={"player_id": self.player.id, "session_token": self.player.session_token, "selected_text": "Jupiter"}, content_type="application/json", ) @@ -505,7 +505,7 @@ class GuessSubmissionTests(TestCase): "lobby:submit_guess", kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, ), - data={"player_id": self.player.id, "selected_text": "Mars"}, + data={"player_id": self.player.id, "session_token": self.player.session_token, "selected_text": "Mars"}, content_type="application/json", ) @@ -514,6 +514,33 @@ class GuessSubmissionTests(TestCase): + def test_submit_guess_requires_session_token(self): + response = self.client.post( + reverse( + "lobby:submit_guess", + kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, + ), + data={"player_id": self.player.id, "selected_text": "Mars"}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"], "session_token is required") + + def test_submit_guess_rejects_invalid_session_token(self): + response = self.client.post( + reverse( + "lobby:submit_guess", + kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, + ), + data={"player_id": self.player.id, "session_token": "wrong-token", "selected_text": "Mars"}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json()["error"], "Invalid player session token") + + class ScoreCalculationTests(TestCase): def setUp(self): self.host = User.objects.create_user(username="host_score", password="secret123") diff --git a/lobby/views.py b/lobby/views.py index f9fd0da..8247508 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -440,11 +440,15 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso session_code = code.strip().upper() player_id = payload.get("player_id") + session_token = str(payload.get("session_token", "")).strip() selected_text = str(payload.get("selected_text", "")).strip() if not player_id: return JsonResponse({"error": "player_id is required"}, status=400) + if not session_token: + return JsonResponse({"error": "session_token is required"}, status=400) + if not selected_text or len(selected_text) > 255: return JsonResponse({"error": "selected_text must be between 1 and 255 characters"}, status=400) @@ -461,6 +465,9 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso except Player.DoesNotExist: return JsonResponse({"error": "Player not found in session"}, status=404) + if player.session_token != session_token: + return JsonResponse({"error": "Invalid player session token"}, status=403) + try: round_question = RoundQuestion.objects.get( pk=round_question_id,