from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from datetime import datetime, timedelta from jose import JWTError, jwt from passlib.context import CryptContext from typing import List from app.models.base import get_db from app.models.admin import Admin from app.models.question import Question from app.models.category import Category from app.schemas.admin import AdminCreate, Token, TokenData from app.schemas.question import ( QuestionCreate, QuestionUpdate, QuestionResponse, AIGenerateRequest ) from app.services.ai_generator import ai_generator from app.config import get_settings router = APIRouter() settings = get_settings() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login") def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: return pwd_context.hash(password) def create_access_token(data: dict) -> str: to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=settings.jwt_expire_minutes) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm) async def get_current_admin( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db) ) -> Admin: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode( token, settings.jwt_secret, algorithms=[settings.jwt_algorithm] ) username: str = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception result = await db.execute(select(Admin).where(Admin.username == username)) admin = result.scalar_one_or_none() if admin is None: raise credentials_exception return admin @router.post("/login", response_model=Token) async def login( form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db) ): result = await db.execute( select(Admin).where(Admin.username == form_data.username) ) admin = result.scalar_one_or_none() if not admin or not verify_password(form_data.password, admin.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token = create_access_token(data={"sub": admin.username}) return {"access_token": access_token, "token_type": "bearer"} @router.post("/register", response_model=Token) async def register_admin( admin_data: AdminCreate, db: AsyncSession = Depends(get_db) ): # Check if admin exists result = await db.execute( select(Admin).where(Admin.username == admin_data.username) ) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered" ) # Create admin admin = Admin( username=admin_data.username, password_hash=get_password_hash(admin_data.password) ) db.add(admin) await db.commit() access_token = create_access_token(data={"sub": admin.username}) return {"access_token": access_token, "token_type": "bearer"} # Question Management @router.get("/questions", response_model=List[QuestionResponse]) async def get_questions( category_id: int = None, status: str = None, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): query = select(Question) if category_id: query = query.where(Question.category_id == category_id) if status: query = query.where(Question.status == status) result = await db.execute(query.order_by(Question.created_at.desc())) return result.scalars().all() @router.post("/questions", response_model=QuestionResponse) async def create_question( question_data: QuestionCreate, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): question = Question( **question_data.model_dump(), points=settings.default_points.get(question_data.difficulty, 300), time_seconds=settings.default_times.get(question_data.difficulty, 25) ) db.add(question) await db.commit() await db.refresh(question) return question @router.put("/questions/{question_id}", response_model=QuestionResponse) async def update_question( question_id: int, question_data: QuestionUpdate, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): result = await db.execute(select(Question).where(Question.id == question_id)) question = result.scalar_one_or_none() if not question: raise HTTPException(status_code=404, detail="Question not found") for key, value in question_data.model_dump(exclude_unset=True).items(): setattr(question, key, value) await db.commit() await db.refresh(question) return question @router.delete("/questions/{question_id}") async def delete_question( question_id: int, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): result = await db.execute(select(Question).where(Question.id == question_id)) question = result.scalar_one_or_none() if not question: raise HTTPException(status_code=404, detail="Question not found") await db.delete(question) await db.commit() return {"status": "deleted"} @router.post("/questions/generate") async def generate_questions( request: AIGenerateRequest, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): # Get category name result = await db.execute( select(Category).where(Category.id == request.category_id) ) category = result.scalar_one_or_none() if not category: raise HTTPException(status_code=404, detail="Category not found") # Generate questions with AI generated = await ai_generator.generate_questions( category_name=category.name, difficulty=request.difficulty, count=request.count ) # Save to database as pending questions = [] for q_data in generated: question = Question( category_id=request.category_id, question_text=q_data["question"], correct_answer=q_data["correct_answer"], alt_answers=q_data.get("alt_answers", []), difficulty=q_data["difficulty"], points=q_data["points"], time_seconds=q_data["time_seconds"], fun_fact=q_data.get("fun_fact"), status="pending" ) db.add(question) questions.append(question) await db.commit() return { "generated": len(questions), "questions": [q.id for q in questions] } @router.post("/questions/{question_id}/approve") async def approve_question( question_id: int, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): result = await db.execute(select(Question).where(Question.id == question_id)) question = result.scalar_one_or_none() if not question: raise HTTPException(status_code=404, detail="Question not found") question.status = "approved" await db.commit() return {"status": "approved"} @router.post("/questions/{question_id}/reject") async def reject_question( question_id: int, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): result = await db.execute(select(Question).where(Question.id == question_id)) question = result.scalar_one_or_none() if not question: raise HTTPException(status_code=404, detail="Question not found") await db.delete(question) await db.commit() return {"status": "rejected"} # Categories @router.get("/categories") async def get_categories( db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): result = await db.execute(select(Category)) return result.scalars().all() @router.post("/categories") async def create_category( name: str, icon: str = None, color: str = None, db: AsyncSession = Depends(get_db), admin: Admin = Depends(get_current_admin) ): category = Category(name=name, icon=icon, color=color) db.add(category) await db.commit() await db.refresh(category) return category