✨ Added API-keys for live game data
This commit is contained in:
+16
-3
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 ###
|
||||||
Reference in New Issue
Block a user