Added API-keys for live game data

This commit is contained in:
Mawoka
2022-09-19 21:29:04 +02:00
parent fe3dde3f3a
commit e2814c7944
6 changed files with 114 additions and 21 deletions
+16 -3
View File
@@ -1,7 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public # This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
import uuid
from datetime import timedelta from datetime import timedelta
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
@@ -18,9 +18,9 @@ from jose import JWTError, jwt
from passlib.hash import argon2 from passlib.hash import argon2
from classquiz.cache import get_cache from classquiz.cache import get_cache
from classquiz.config import settings from classquiz.config import settings, redis
from datetime import datetime from datetime import datetime
from classquiz.db.models import User, TokenData from classquiz.db.models import User, TokenData, ApiKey
settings = settings() settings = settings()
@@ -148,3 +148,16 @@ async def check_token(token: str = Depends(oauth2_scheme)):
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
return token_data.email return token_data.email
async def check_api_key(key: str) -> uuid.UUID | None:
redis_res = await redis.get(f"apikey:{key}")
if redis_res is None:
key2 = await ApiKey.objects.get_or_none(key=key)
if key2 is None:
return None
else:
await redis.set(f"apikey:{key}", key2.user.id.hex, ex=3600)
return key2.user.id
else:
return uuid.UUID(redis_res)
+11
View File
@@ -43,6 +43,16 @@ class User(ormar.Model):
use_enum_values = True use_enum_values = True
class ApiKey(ormar.Model):
key: str = ormar.String(max_length=48, min_length=48, primary_key=True)
user: Optional[User] = ormar.ForeignKey(User)
class Meta:
tablename = "api_keys"
metadata = metadata
database = database
class UserSession(ormar.Model): class UserSession(ormar.Model):
""" """
The user session model for user-sessions The user session model for user-sessions
@@ -151,6 +161,7 @@ class TokenData(BaseModel):
class PlayGame(BaseModel): class PlayGame(BaseModel):
quiz_id: uuid.UUID | str quiz_id: uuid.UUID | str
description: str description: str
user_id: uuid.UUID
title: str title: str
questions: list[QuizQuestion] questions: list[QuizQuestion]
game_id: uuid.UUID game_id: uuid.UUID
+25 -16
View File
@@ -7,7 +7,6 @@ import re
import uuid import uuid
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from typing import Dict
import ormar.exceptions import ormar.exceptions
@@ -17,7 +16,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import ValidationError, BaseModel from pydantic import ValidationError, BaseModel
import bleach import bleach
from classquiz.auth import get_current_user from classquiz.auth import get_current_user, check_api_key
from classquiz.config import redis, settings, storage, meilisearch from classquiz.config import redis, settings, storage, meilisearch
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameSession, GameAnswer1, GameAnswer2 from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameSession, GameAnswer1, GameAnswer2
from classquiz.kahoot_importer.import_quiz import import_quiz from classquiz.kahoot_importer.import_quiz import import_quiz
@@ -113,6 +112,7 @@ async def start_quiz(
captcha_enabled=captcha_enabled, captcha_enabled=captcha_enabled,
cover_image=quiz.cover_image, cover_image=quiz.cover_image,
game_mode=game_mode, game_mode=game_mode,
user_id=user.id,
) )
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000) await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})} return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
@@ -260,12 +260,15 @@ class GetLiveDataResponse(BaseModel):
player_count: int player_count: int
@router.get("/live", response_model=GetLiveDataResponse) @router.get("/live", response_model=GetLiveDataResponse, tags=["live"])
async def get_live_game_data(game_pin: int): async def get_live_game_data(game_pin: int, api_key: str):
user_id = await check_api_key(api_key)
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game = PlayGame.parse_raw(redis_res) game = PlayGame.parse_raw(redis_res)
if game.user_id != user_id:
raise HTTPException(status_code=404, detail="Game not found or API key not found")
data = GameSession.parse_raw(await redis.get(f"game_session:{game_pin}")) data = GameSession.parse_raw(await redis.get(f"game_session:{game_pin}"))
for i in range(0, len(game.questions)): for i in range(0, len(game.questions)):
res = await redis.get(f"game_session:{game_pin}:{i}") res = await redis.get(f"game_session:{game_pin}:{i}")
@@ -279,7 +282,7 @@ async def get_live_game_data(game_pin: int):
return GetLiveDataResponse(quiz=game, data=data, player_count=player_count) return GetLiveDataResponse(quiz=game, data=data, player_count=player_count)
@router.get("/live/user_count") @router.get("/live/user_count", tags=["live"])
async def get_game_user_count(game_pin: int) -> dict[str, int]: async def get_game_user_count(game_pin: int) -> dict[str, int]:
# if redis_res is None: # if redis_res is None:
# raise HTTPException(status_code=404, detail="Game not found") # raise HTTPException(status_code=404, detail="Game not found")
@@ -287,13 +290,16 @@ async def get_game_user_count(game_pin: int) -> dict[str, int]:
return {"player_count": player_count} return {"player_count": player_count}
@router.get("/live/players", response_model=GameSession) @router.get("/live/players", response_model=GameSession, tags=["live"])
async def get_game_session(game_pin: int): async def get_game_session(game_pin: int, api_key: str):
user_id = await check_api_key(api_key)
redis_res = await redis.get(f"game_session:{game_pin}") redis_res = await redis.get(f"game_session:{game_pin}")
if redis_res is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
data = GameSession.parse_raw(redis_res) data = GameSession.parse_raw(redis_res)
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}")) game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}"))
if game.user_id != user_id:
raise HTTPException(status_code=404, detail="Game not found or API key not found")
for i in range(0, len(game.questions)): for i in range(0, len(game.questions)):
res = await redis.get(f"game_session:{game_pin}:{i}") res = await redis.get(f"game_session:{game_pin}:{i}")
if res is None: if res is None:
@@ -305,12 +311,15 @@ async def get_game_session(game_pin: int):
return data return data
@router.post("/live/set_question") @router.post("/live/set_question", tags=["live"])
async def set_next_question(game_pin: int, question_number: int): async def set_next_question(game_pin: int, question_number: int, api_key: str):
user_id = await check_api_key(api_key)
redis_res = await redis.get(f"game:{game_pin}") redis_res = await redis.get(f"game:{game_pin}")
if redis_res is None: if redis_res is None or user_id is None:
raise HTTPException(status_code=404, detail="Game not found") raise HTTPException(status_code=404, detail="Game not found or API key not found")
game_data = PlayGame.parse_raw(await redis.get(f"game:{game_pin}")) game_data = PlayGame.parse_raw(redis_res)
if game_data.user_id != user_id:
raise HTTPException(status_code=404, detail="Game not found or API key not found")
game_data.current_question = question_number game_data.current_question = question_number
await redis.set(f"game:{game_pin}", game_data.json()) await redis.set(f"game:{game_pin}", game_data.json())
await sio.emit( await sio.emit(
+23 -1
View File
@@ -31,7 +31,7 @@ from classquiz.config import redis, settings, meilisearch
import uuid import uuid
import bleach import bleach
from pydantic import BaseModel from pydantic import BaseModel
from classquiz.db.models import User, UserSession, UpdatePassword, Token, Quiz from classquiz.db.models import User, UserSession, UpdatePassword, Token, Quiz, ApiKey
from classquiz.emails import send_register_email, send_forgotten_password_email from classquiz.emails import send_register_email, send_forgotten_password_email
settings = settings() settings = settings()
@@ -312,3 +312,25 @@ async def get_email_from_jwt(data: GetEmailFromJWT):
except JWTError as e: except JWTError as e:
print(e) print(e)
raise HTTPException(status_code=401) raise HTTPException(status_code=401)
@router.post("/api_keys", response_model=ApiKey, response_model_include={"key"})
async def generate_api_key(user: User = Depends(get_current_user)):
key = ApiKey(key=os.urandom(24).hex(), user=user)
await key.save()
return key.dict(include={"key"})
@router.get("/api_keys", response_model=list[ApiKey], response_model_include={"key"})
async def list_api_keys(user: User = Depends(get_current_user)):
keys = await ApiKey.objects.filter(user=user).all()
return keys
@router.delete("/api_keys")
async def delete_api_key(api_key: str, user: User = Depends(get_current_user)):
key = await ApiKey.objects.get_or_none(key=api_key)
if key is None:
raise HTTPException(status_code=404, detail="Key not found")
await redis.delete(f"apikey:{key.key}")
await key.delete()
+4 -1
View File
@@ -77,7 +77,10 @@ async def join_game(sid: str, data: dict):
await sio.save_session(sid, session) await sio.save_session(sid, session)
await sio.emit( await sio.emit(
"joined_game", "joined_game",
{**json.loads(game_data.json(exclude={"quiz_id", "questions"})), "question_count": len(game_data.questions)}, {
**json.loads(game_data.json(exclude={"quiz_id", "questions", "user_id"})),
"question_count": len(game_data.questions),
},
room=sid, room=sid,
) )
redis_res = await redis.get(f"game_session:{data.game_pin}") redis_res = await redis.get(f"game_session:{data.game_pin}")
@@ -0,0 +1,35 @@
"""added apikey
Revision ID: ec6cf07ff68a
Revises: cda6903dfc0c
Create Date: 2022-09-19 21:04:37.967466
"""
from alembic import op
import sqlalchemy as sa
import ormar
# revision identifiers, used by Alembic.
revision = "ec6cf07ff68a"
down_revision = "cda6903dfc0c"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"api_keys",
sa.Column("key", sa.String(length=48), nullable=False),
sa.Column("user", ormar.fields.sqlalchemy_uuid.CHAR(32), nullable=True),
sa.ForeignKeyConstraint(["user"], ["users.id"], name="fk_api_keys_users_id_user"),
sa.PrimaryKeyConstraint("key"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("api_keys")
# ### end Alembic commands ###