You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
9.2 KiB
Python
312 lines
9.2 KiB
Python
from fastapi import FastAPI, Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional, Dict, Any
|
|
import sys
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../../../shared"))
|
|
from database.connection import get_db
|
|
from database.models import Question, Theme, Player, GameSession, GameAnswer
|
|
from auth.oauth_handler import oauth_handler
|
|
|
|
app = FastAPI(title="Admin Service", version="1.0.0")
|
|
|
|
# Add CORS middleware
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:3000", "http://localhost:5173"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
security = HTTPBearer()
|
|
|
|
|
|
def verify_admin(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
"""Verify admin authentication and permissions"""
|
|
token = credentials.credentials
|
|
payload = oauth_handler.verify_admin_token(token)
|
|
|
|
if not payload:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid or expired authentication token",
|
|
headers={"WWW-Authenticate": "Bearer"}
|
|
)
|
|
|
|
return payload
|
|
|
|
|
|
def require_permission(permission: str):
|
|
"""Decorator to require specific permission"""
|
|
def permission_checker(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
payload = verify_admin(credentials)
|
|
|
|
if not oauth_handler.has_permission(credentials.credentials, permission):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Missing required permission: {permission}"
|
|
)
|
|
|
|
return payload
|
|
|
|
return permission_checker
|
|
|
|
|
|
class QuestionAdmin(BaseModel):
|
|
id: Optional[int] = None
|
|
theme_id: int
|
|
question_text: str
|
|
correct_answer: str
|
|
hint: Optional[str] = None
|
|
difficulty_level: int = 1
|
|
|
|
|
|
class ThemeAdmin(BaseModel):
|
|
id: Optional[int] = None
|
|
name: str
|
|
description: Optional[str] = None
|
|
|
|
|
|
class GameStats(BaseModel):
|
|
total_players: int
|
|
total_games: int
|
|
total_questions: int
|
|
average_score: float
|
|
most_played_theme: str
|
|
total_answers: int
|
|
correct_answers: int
|
|
accuracy_rate: float
|
|
|
|
|
|
class AuthRequest(BaseModel):
|
|
auth0_token: str
|
|
|
|
|
|
class BulkImportRequest(BaseModel):
|
|
questions: List[Dict[str, Any]]
|
|
theme_id: int
|
|
|
|
|
|
class AnalyticsResponse(BaseModel):
|
|
question_stats: List[Dict[str, Any]]
|
|
theme_stats: List[Dict[str, Any]]
|
|
player_stats: List[Dict[str, Any]]
|
|
daily_stats: List[Dict[str, Any]]
|
|
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
return {"status": "healthy", "service": "admin-service"}
|
|
|
|
|
|
@app.post("/auth")
|
|
def authenticate_admin(request: AuthRequest):
|
|
"""Authenticate admin user with Auth0 token"""
|
|
try:
|
|
admin_token = oauth_handler.authenticate_admin(request.auth0_token)
|
|
|
|
if not admin_token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Authentication failed"
|
|
)
|
|
|
|
# Get user info from token
|
|
user_info = oauth_handler.verify_admin_token(admin_token)
|
|
|
|
return {
|
|
"access_token": admin_token,
|
|
"token_type": "bearer",
|
|
"expires_in": 28800, # 8 hours
|
|
"user": {
|
|
"email": user_info.get("email"),
|
|
"name": user_info.get("name"),
|
|
"permissions": user_info.get("permissions", [])
|
|
}
|
|
}
|
|
|
|
except PermissionError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e)
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Authentication service error"
|
|
)
|
|
|
|
|
|
@app.get("/profile")
|
|
def get_admin_profile(admin_user=Depends(verify_admin)):
|
|
"""Get current admin user profile"""
|
|
return {
|
|
"email": admin_user.get("email"),
|
|
"name": admin_user.get("name"),
|
|
"role": admin_user.get("role"),
|
|
"permissions": admin_user.get("permissions", []),
|
|
"login_time": admin_user.get("iat")
|
|
}
|
|
|
|
|
|
@app.get("/stats", response_model=GameStats)
|
|
def get_game_statistics(
|
|
admin_user=Depends(require_permission("read:analytics")),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
# Basic stats
|
|
total_players = db.query(Player).count()
|
|
total_games = db.query(GameSession).filter(GameSession.ended_at.isnot(None)).count()
|
|
total_questions = db.query(Question).count()
|
|
|
|
# Answer statistics
|
|
total_answers = db.query(GameAnswer).count()
|
|
correct_answers = db.query(GameAnswer).filter(GameAnswer.is_correct == True).count()
|
|
accuracy_rate = (correct_answers / max(total_answers, 1)) * 100
|
|
|
|
# Score statistics
|
|
completed_sessions = db.query(GameSession).filter(GameSession.ended_at.isnot(None)).all()
|
|
average_score = sum(session.total_score for session in completed_sessions) / max(len(completed_sessions), 1)
|
|
|
|
# Most played theme (by question answers)
|
|
theme_query = db.query(Theme.name, db.func.count(GameAnswer.id).label('answer_count'))\
|
|
.join(Question, Theme.id == Question.theme_id)\
|
|
.join(GameAnswer, Question.id == GameAnswer.question_id)\
|
|
.group_by(Theme.id, Theme.name)\
|
|
.order_by(db.func.count(GameAnswer.id).desc())\
|
|
.first()
|
|
|
|
most_played_theme = theme_query[0] if theme_query else "None"
|
|
|
|
return GameStats(
|
|
total_players=total_players,
|
|
total_games=total_games,
|
|
total_questions=total_questions,
|
|
average_score=round(average_score, 2),
|
|
most_played_theme=most_played_theme,
|
|
total_answers=total_answers,
|
|
correct_answers=correct_answers,
|
|
accuracy_rate=round(accuracy_rate, 2)
|
|
)
|
|
|
|
|
|
@app.get("/questions", response_model=List[dict])
|
|
def list_questions(
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
questions = db.query(Question).join(Theme).all()
|
|
return [
|
|
{
|
|
"id": q.id,
|
|
"theme_id": q.theme_id,
|
|
"theme_name": q.theme.name,
|
|
"question_text": q.question_text,
|
|
"correct_answer": q.correct_answer,
|
|
"hint": q.hint,
|
|
"difficulty_level": q.difficulty_level
|
|
}
|
|
for q in questions
|
|
]
|
|
|
|
|
|
@app.post("/questions")
|
|
def create_question(
|
|
question_data: QuestionAdmin,
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
theme = db.query(Theme).filter(Theme.id == question_data.theme_id).first()
|
|
if not theme:
|
|
raise HTTPException(status_code=404, detail="Theme not found")
|
|
|
|
question = Question(
|
|
theme_id=question_data.theme_id,
|
|
question_text=question_data.question_text,
|
|
correct_answer=question_data.correct_answer,
|
|
hint=question_data.hint,
|
|
difficulty_level=question_data.difficulty_level
|
|
)
|
|
|
|
db.add(question)
|
|
db.commit()
|
|
db.refresh(question)
|
|
|
|
return {"id": question.id, "message": "Question created successfully"}
|
|
|
|
|
|
@app.put("/questions/{question_id}")
|
|
def update_question(
|
|
question_id: int,
|
|
question_data: QuestionAdmin,
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
question = db.query(Question).filter(Question.id == question_id).first()
|
|
if not question:
|
|
raise HTTPException(status_code=404, detail="Question not found")
|
|
|
|
question.theme_id = question_data.theme_id
|
|
question.question_text = question_data.question_text
|
|
question.correct_answer = question_data.correct_answer
|
|
question.hint = question_data.hint
|
|
question.difficulty_level = question_data.difficulty_level
|
|
|
|
db.commit()
|
|
|
|
return {"message": "Question updated successfully"}
|
|
|
|
|
|
@app.delete("/questions/{question_id}")
|
|
def delete_question(
|
|
question_id: int,
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
question = db.query(Question).filter(Question.id == question_id).first()
|
|
if not question:
|
|
raise HTTPException(status_code=404, detail="Question not found")
|
|
|
|
db.delete(question)
|
|
db.commit()
|
|
|
|
return {"message": "Question deleted successfully"}
|
|
|
|
|
|
@app.get("/themes", response_model=List[dict])
|
|
def list_themes(
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
themes = db.query(Theme).all()
|
|
return [{"id": theme.id, "name": theme.name, "description": theme.description} for theme in themes]
|
|
|
|
|
|
@app.post("/themes")
|
|
def create_theme(
|
|
theme_data: ThemeAdmin,
|
|
admin_user = Depends(verify_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
existing_theme = db.query(Theme).filter(Theme.name == theme_data.name).first()
|
|
if existing_theme:
|
|
raise HTTPException(status_code=400, detail="Theme already exists")
|
|
|
|
theme = Theme(name=theme_data.name, description=theme_data.description)
|
|
db.add(theme)
|
|
db.commit()
|
|
db.refresh(theme)
|
|
|
|
return {"id": theme.id, "message": "Theme created successfully"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8004) |