diff --git a/classquiz/db/models.py b/classquiz/db/models.py index 3da892b..56f71ea 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -99,10 +99,6 @@ class TokenData(BaseModel): class PlayGame(BaseModel): - """ - For JWT - """ - quiz_id: uuid.UUID | str description: str title: str @@ -110,6 +106,7 @@ class PlayGame(BaseModel): game_id: uuid.UUID game_pin: str started: bool = False + captcha_enabled: bool = False class GamePlayer(BaseModel): diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index 46b6f8a..cb01c77 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -6,7 +6,7 @@ from random import randint from classquiz.helpers import get_meili_data from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse -from pydantic import ValidationError +from pydantic import ValidationError, BaseModel import bleach from classquiz.auth import get_current_user @@ -76,7 +76,7 @@ async def get_public_quiz(quiz_id: str): @router.post("/start/{quiz_id}") -async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): +async def start_quiz(quiz_id: str, captcha_enabled: bool = True, user: User = Depends(get_current_user)): try: quiz_id = uuid.UUID(quiz_id) except ValueError: @@ -94,11 +94,24 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): game_id=uuid.uuid4(), title=quiz.title, description=quiz.description, + captcha_enabled=captcha_enabled, ) await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000) return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})} +class CheckIfCaptchaEnabledResponse(BaseModel): + enabled: bool + + +@router.get("/play/check_captcha/{game_pin}", response_model=CheckIfCaptchaEnabledResponse) +async def check_if_captcha_enabled(game_pin: str): + game = await redis.get(f"game:{game_pin}") + if game is None: + return JSONResponse(status_code=404, content={"detail": "game not found"}) + return CheckIfCaptchaEnabledResponse(**{"enabled": json.loads(game)["captcha_enabled"]}) + + @router.get("/join/{game_pin}") async def get_game_id(game_pin: str): redis_res = (await redis.get(f"game:{game_pin}")).decode() diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index ee9e50d..b5da3a5 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -13,20 +13,21 @@ settings = settings() @sio.event async def join_game(sid, data): async with aiohttp.ClientSession() as session: - try: - async with session.post( - "https://hcaptcha.com/siteverify", - data={"response": data["captcha"], "secret": settings.hcaptcha_key}, - ) as resp: - resp_data = await resp.json() - if not resp_data["success"]: - print("CAPTCHA FAILED") - return - except KeyError: - print("CAPTCHA FAILED") + redis_res = await redis.get(f"game:{data['game_pin']}") + if json.loads(redis_res)["captcha_enabled"]: + try: + async with session.post( + "https://hcaptcha.com/siteverify", + data={"response": data["captcha"], "secret": settings.hcaptcha_key}, + ) as resp: + resp_data = await resp.json() + if not resp_data["success"]: + print("CAPTCHA FAILED") + return + except KeyError: + print("CAPTCHA FAILED") - return - redis_res = await redis.get(f"game:{data['game_pin']}") + return if redis_res is None: await sio.emit("game_not_found", room=sid) else: diff --git a/frontend/src/lib/play/join.svelte b/frontend/src/lib/play/join.svelte index 1067a39..51b283d 100644 --- a/frontend/src/lib/play/join.svelte +++ b/frontend/src/lib/play/join.svelte @@ -44,17 +44,22 @@ return; } let captcha_resp: string; - try { - const { response } = await hcaptcha.execute(hcaptchaWidgetID, { - async: true - }); - captcha_resp = response; - } catch (e) { - if (import.meta.env.VITE_SENTRY !== null) { - Sentry.captureException(e); + const captcha_enabled = ( + await (await fetch(`/api/v1/quiz/play/check_captcha/${game_pin}`)).json() + ).enabled; + if (captcha_enabled) { + try { + const { response } = await hcaptcha.execute(hcaptchaWidgetID, { + async: true + }); + captcha_resp = response; + } catch (e) { + if (import.meta.env.VITE_SENTRY !== null) { + Sentry.captureException(e); + } + alert('Captcha failed, reloading the page probably helps!'); + window.location.reload(); } - alert('Captcha failed, reloading the page probably helps!'); - window.location.reload(); } socket.emit('join_game', { username: username, diff --git a/frontend/src/routes/overview.svelte b/frontend/src/routes/overview.svelte index ef207ee..3c40ed4 100644 --- a/frontend/src/routes/overview.svelte +++ b/frontend/src/routes/overview.svelte @@ -48,12 +48,17 @@ const startGame = async (id: string): Promise => { console.log('start game', id); - const res = await fetch(`/api/v1/quiz/start/${id}`, { - method: 'POST' - // headers: { - // 'Content-Type': 'application/json' - // } - }); + let res; + if (window.confirm('Do you want to enable the captcha for players?')) { + res = await fetch(`/api/v1/quiz/start/${id}?captcha_enabled=True`, { + method: 'POST' + }); + } else { + res = await fetch(`/api/v1/quiz/start/${id}?captcha_enabled=False`, { + method: 'POST' + }); + } + if (res.status !== 200) { alert('Failed to start game!'); console.error(`Failed to start game, ${await res.text()}`); diff --git a/frontend/src/routes/view/[quiz_id].svelte b/frontend/src/routes/view/[quiz_id].svelte index 0570d70..fc00cfa 100644 --- a/frontend/src/routes/view/[quiz_id].svelte +++ b/frontend/src/routes/view/[quiz_id].svelte @@ -64,12 +64,16 @@ const startGame = async (id: string): Promise => { console.log('start game', id); - const res = await fetch(`/api/v1/quiz/start/${id}`, { - method: 'POST' - // headers: { - // 'Content-Type': 'application/json' - // } - }); + let res; + if (window.confirm('Do you want to enable the captcha for players?')) { + res = await fetch(`/api/v1/quiz/start/${id}?captcha_enabled=True`, { + method: 'POST' + }); + } else { + res = await fetch(`/api/v1/quiz/start/${id}?captcha_enabled=False`, { + method: 'POST' + }); + } if (res.status !== 200) { throw new Error('Failed to start game'); }