Merge branch 'master' into order
This commit is contained in:
@@ -28,6 +28,7 @@ from classquiz.routers import (
|
||||
login,
|
||||
sitemap,
|
||||
remote,
|
||||
community,
|
||||
)
|
||||
from classquiz.socket_server import sio
|
||||
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
|
||||
@@ -98,5 +99,6 @@ app.include_router(
|
||||
app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True)
|
||||
app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True)
|
||||
app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True)
|
||||
app.include_router(community.router, tags=["community"], prefix="/api/v1/community", include_in_schema=True)
|
||||
|
||||
app.mount("/", ASGIApp(sio))
|
||||
|
||||
+6
-6
@@ -43,12 +43,12 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
|
||||
if authorization is None:
|
||||
try:
|
||||
authorization = request.state.access_token
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
authorization = request.state.access_token
|
||||
except AttributeError:
|
||||
authorization: str = request.cookies.get(
|
||||
"access_token"
|
||||
) # changed to accept access token from httpOnly Cookie
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
|
||||
@@ -133,6 +133,7 @@ class QuizQuestion(BaseModel):
|
||||
|
||||
@validator("answers")
|
||||
def answers_not_none_if_abcd_type(cls, v, values):
|
||||
# print(values)
|
||||
if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer:
|
||||
raise ValueError("Answers can't be none if type is ABCD")
|
||||
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
|
||||
@@ -272,3 +273,12 @@ class GameInLobby(BaseModel):
|
||||
game_pin: str
|
||||
quiz_title: str
|
||||
game_id: uuid.UUID
|
||||
|
||||
|
||||
#
|
||||
# class UserProfileLinks(ormar.Model):
|
||||
# id: int = ormar.Integer(primary_key=True, autoincrement=True)
|
||||
# user: Optional[User] = ormar.ForeignKey(User)
|
||||
# github_username: str | None = ormar.Text(nullable=True)
|
||||
# reddit_username: str | None = ormar.Text(nullable=True)
|
||||
# kahoot_user_id: str | None = ormar.Text(nullable=True)
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from jose import jws, jwt, JWTError, JWSError
|
||||
|
||||
from classquiz.auth import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token
|
||||
from classquiz.db.models import UserSession
|
||||
from classquiz.oauth import google, github
|
||||
from classquiz.config import settings
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(google.router, prefix="/google")
|
||||
@@ -18,7 +22,45 @@ router.include_router(github.router, prefix="/github")
|
||||
async def rememberme_middleware(request: Request, call_next):
|
||||
rememberme_cookie = request.cookies.get("rememberme_token")
|
||||
bearer_token = request.cookies.get("access_token")
|
||||
if rememberme_cookie is not None and bearer_token is None:
|
||||
conditions_to_handle_met = True
|
||||
# print(bearer_token)
|
||||
# if bearer_token is not None:
|
||||
# bearer_token = bearer_token.replace("Bearer ", "")
|
||||
# test = jws.verify(bearer_token, settings.secret_key, algorithms=["HS256"])
|
||||
# try:
|
||||
# jwt.decode(bearer_token, settings.secret_key, algorithms=["HS256"])
|
||||
# print("jwt ok")
|
||||
# except JWTError as e:
|
||||
# print("jwt failed")
|
||||
# print(test)
|
||||
|
||||
scheme, param = get_authorization_scheme_param(bearer_token)
|
||||
|
||||
# if bearer token is none, we can just do the request, since you can't be signed in
|
||||
if scheme is None or param is None:
|
||||
conditions_to_handle_met = False
|
||||
# if rememberme token is none, we can just do the request, since you can't be signed in
|
||||
if rememberme_cookie is None:
|
||||
conditions_to_handle_met = False
|
||||
|
||||
if scheme.lower() != "bearer":
|
||||
conditions_to_handle_met = False
|
||||
|
||||
# Verifying the bearer
|
||||
try:
|
||||
jwt.decode(
|
||||
param, settings.secret_key, algorithms=["HS256"]
|
||||
) # checking if the token is valid, throws error if not
|
||||
conditions_to_handle_met = False
|
||||
except JWTError:
|
||||
try:
|
||||
jws.verify(
|
||||
param, settings.secret_key, algorithms=["HS256"]
|
||||
) # Verifying only the signature of the jwt, throws error if signature is invalid
|
||||
except JWSError:
|
||||
conditions_to_handle_met = False
|
||||
|
||||
if conditions_to_handle_met:
|
||||
user_session: UserSession | None = (
|
||||
await UserSession.objects.filter(session_key=rememberme_cookie)
|
||||
.select_related(UserSession.user)
|
||||
@@ -27,17 +69,19 @@ async def rememberme_middleware(request: Request, call_next):
|
||||
if (user_session is None) or (user_session.user is None):
|
||||
response: Response = await call_next(request)
|
||||
return response
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES * 60)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
# access_token_expires = timedelta(seconds=1)
|
||||
access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires)
|
||||
await user_session.update(last_seen=datetime.now())
|
||||
request.state.access_token = f"Bearer {access_token}"
|
||||
request.cookies.pop("access_token")
|
||||
response: Response = await call_next(request)
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
)
|
||||
else:
|
||||
response: Response = await call_next(request)
|
||||
|
||||
@@ -43,7 +43,7 @@ async def log_user_in(user: User, request: Request, response: Response):
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=settings.access_token_expire_minutes * 60,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
)
|
||||
response.set_cookie(
|
||||
key="rememberme_token", value=session_key, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 365
|
||||
@@ -64,7 +64,7 @@ async def rememberme_check(rememberme_token: str, response: Response):
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=settings.access_token_expire_minutes * 60,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
)
|
||||
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
|
||||
response.status_code = 200
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from uuid import UUID
|
||||
|
||||
from classquiz.db.models import User, Quiz
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
#
|
||||
@router.get("/user/{user_id}", response_model_include={"username", "created_at", "id"}, response_model=User)
|
||||
async def get_user_by_user_id(user_id: UUID):
|
||||
user = await User.objects.get_or_none(id=user_id)
|
||||
# .select_related("quizs")
|
||||
# print(user)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="user not found")
|
||||
else:
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/quizzes/{user_id}", response_model_exclude={"questions", "user_id"}, response_model=list[Quiz])
|
||||
async def get_quizzes_from_user(user_id: UUID, imported: bool | None = None):
|
||||
if imported is None:
|
||||
quizzes = await Quiz.objects.all(user_id=user_id, public=True)
|
||||
else:
|
||||
quizzes = await Quiz.objects.all(user_id=user_id, public=True, imported_from_kahoot=imported)
|
||||
if len(quizzes) == 0:
|
||||
raise HTTPException(status_code=404, detail="no quizzes found")
|
||||
else:
|
||||
return quizzes
|
||||
@@ -18,7 +18,7 @@ import bleach
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
from classquiz.config import redis, settings, storage, meilisearch
|
||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby
|
||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby, QuizQuestion
|
||||
from classquiz.kahoot_importer.import_quiz import import_quiz
|
||||
import html
|
||||
import urllib.parse
|
||||
@@ -75,17 +75,28 @@ async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current
|
||||
return quiz
|
||||
|
||||
|
||||
@router.get("/get/public/{quiz_id}", response_model=Quiz)
|
||||
class PublicQuizResponseUser(BaseModel):
|
||||
username: str
|
||||
id: uuid.UUID
|
||||
|
||||
|
||||
class PublicQuizResponse(Quiz.get_pydantic()):
|
||||
user_id: PublicQuizResponseUser
|
||||
questions: list[QuizQuestion]
|
||||
|
||||
|
||||
@router.get("/get/public/{quiz_id}")
|
||||
async def get_public_quiz(quiz_id: str):
|
||||
try:
|
||||
quiz_id = uuid.UUID(quiz_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="badly formed quiz id")
|
||||
quiz = await Quiz.objects.get_or_none(id=quiz_id)
|
||||
quiz = await Quiz.objects.select_related("user_id").get_or_none(id=quiz_id)
|
||||
print(quiz.dict(exclude={"user_id": {"avatar"}}))
|
||||
if quiz is None:
|
||||
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
|
||||
else:
|
||||
return quiz
|
||||
return PublicQuizResponse(**quiz.dict())
|
||||
|
||||
|
||||
@router.post("/start/{quiz_id}")
|
||||
|
||||
Reference in New Issue
Block a user