feat(ui): require session_token for guess submit (#39)
All checks were successful
CI / test-and-quality (push) Successful in 1m34s
CI / test-and-quality (pull_request) Successful in 1m36s

This commit is contained in:
2026-02-27 23:32:47 +01:00
parent 59f2b0b29e
commit 0a028bb499
3 changed files with 40 additions and 6 deletions

View File

@@ -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 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 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 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});}
</script> </script>
</body></html> </body></html>

View File

@@ -442,7 +442,7 @@ class GuessSubmissionTests(TestCase):
"lobby:submit_guess", "lobby:submit_guess",
kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, 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", content_type="application/json",
) )
@@ -461,7 +461,7 @@ class GuessSubmissionTests(TestCase):
"lobby:submit_guess", "lobby:submit_guess",
kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, 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", content_type="application/json",
) )
@@ -474,7 +474,7 @@ class GuessSubmissionTests(TestCase):
"lobby:submit_guess", "lobby:submit_guess",
kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, 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", content_type="application/json",
) )
@@ -489,7 +489,7 @@ class GuessSubmissionTests(TestCase):
"lobby:submit_guess", "lobby:submit_guess",
kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, 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", content_type="application/json",
) )
@@ -505,7 +505,7 @@ class GuessSubmissionTests(TestCase):
"lobby:submit_guess", "lobby:submit_guess",
kwargs={"code": self.session.code, "round_question_id": self.round_question.id}, 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", 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): class ScoreCalculationTests(TestCase):
def setUp(self): def setUp(self):
self.host = User.objects.create_user(username="host_score", password="secret123") self.host = User.objects.create_user(username="host_score", password="secret123")

View File

@@ -440,11 +440,15 @@ def submit_guess(request: HttpRequest, code: str, round_question_id: int) -> Jso
session_code = code.strip().upper() session_code = code.strip().upper()
player_id = payload.get("player_id") player_id = payload.get("player_id")
session_token = str(payload.get("session_token", "")).strip()
selected_text = str(payload.get("selected_text", "")).strip() selected_text = str(payload.get("selected_text", "")).strip()
if not player_id: if not player_id:
return JsonResponse({"error": "player_id is required"}, status=400) 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: if not selected_text or len(selected_text) > 255:
return JsonResponse({"error": "selected_text must be between 1 and 255 characters"}, status=400) 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: except Player.DoesNotExist:
return JsonResponse({"error": "Player not found in session"}, status=404) 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: try:
round_question = RoundQuestion.objects.get( round_question = RoundQuestion.objects.get(
pk=round_question_id, pk=round_question_id,