from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.orm import Session import httpx from app.config import settings from app.database import get_db from app.models.user import User from app.models.ai_model_config import AIModelConfig from app.schemas.auth import UserResponse, UserUpdateRole, UserCreate from app.schemas.admin import AIModelConfigCreate, AIModelConfigResponse, AIModelConfigUpdate from app.utils.auth import require_admin, get_current_user, get_password_hash router = APIRouter() # --- User Management --- @router.get("/users", response_model=list[UserResponse]) def list_users( db: Session = Depends(get_db), admin: User = Depends(require_admin), ): return db.query(User).order_by(User.created_at.desc()).all() @router.put("/users/{user_id}/role", response_model=UserResponse) def update_user_role( user_id: int, role_data: UserUpdateRole, db: Session = Depends(get_db), admin: User = Depends(require_admin), ): if role_data.role not in ("admin", "moderator", "user"): raise HTTPException(status_code=400, detail="Role must be admin, moderator, or user") user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") if user.id == admin.id: raise HTTPException(status_code=400, detail="Cannot change your own role") user.role = role_data.role db.commit() db.refresh(user) return user @router.post("/users", response_model=UserResponse) def create_user( user_data: UserCreate, db: Session = Depends(get_db), admin: User = Depends(require_admin), ): """Admin creates a user directly — email is auto-verified.""" from app.models.email_verification import EmailVerification from datetime import datetime if db.query(User).filter(User.email == user_data.email).first(): raise HTTPException(status_code=400, detail="Email already registered") user = User( email=user_data.email, hashed_password=get_password_hash(user_data.password), name=user_data.name, role="user", ) db.add(user) db.flush() db.add(EmailVerification( user_id=user.id, token=f"admin_created_{user.id}", expires_at=datetime.utcnow(), verified_at=datetime.utcnow(), )) db.commit() db.refresh(user) return user # --- AI Model Configuration --- @router.get("/models/available") def list_available_models( task: str = Query("extraction"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Returns active models for a given task — for users to choose when taking/creating a quiz.""" models = db.query(AIModelConfig).filter( AIModelConfig.task == task, AIModelConfig.is_active == True, ).order_by(AIModelConfig.is_default.desc(), AIModelConfig.name).all() result = [{"id": m.id, "name": m.name, "model_id": m.model_id, "is_default": m.is_default} for m in models] # Always include env default as fallback if nothing configured if not result: result.append({"id": None, "name": "Default (from config)", "model_id": settings.LITELLM_MODEL, "is_default": True}) return result class LiteLLMModelQuery(BaseModel): api_key: str | None = None api_base: str | None = None @router.post("/litellm/models") def search_litellm_models( body: LiteLLMModelQuery = LiteLLMModelQuery(), admin: User = Depends(require_admin), ): """Query available models from LiteLLM proxy or OpenAI-compatible API.""" base = (body.api_base or settings.LITELLM_API_BASE or "").rstrip("/") key = body.api_key or settings.LITELLM_API_KEY if base: try: headers = {"Authorization": f"Bearer {key}"} if key else {} resp = httpx.get(f"{base}/v1/models", headers=headers, timeout=10) resp.raise_for_status() data = resp.json() models = sorted([m["id"] for m in data.get("data", [])]) return {"models": models, "source": base} except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to query models API: {e}") # Fall back to LiteLLM's built-in model list try: import litellm models = sorted(litellm.utils.get_valid_models()) return {"models": models, "source": "litellm-builtin"} except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get LiteLLM models: {e}") @router.get("/models", response_model=list[AIModelConfigResponse]) def list_models( db: Session = Depends(get_db), admin: User = Depends(require_admin), ): return db.query(AIModelConfig).order_by(AIModelConfig.task, AIModelConfig.name).all() @router.post("/models", response_model=AIModelConfigResponse) def create_model( data: AIModelConfigCreate, db: Session = Depends(get_db), admin: User = Depends(require_admin), ): if data.task not in ("extraction", "tts", "general"): raise HTTPException(status_code=400, detail="Task must be extraction, tts, or general") if data.is_default: db.query(AIModelConfig).filter( AIModelConfig.task == data.task, AIModelConfig.is_default == True, ).update({"is_default": False}) model = AIModelConfig(**data.model_dump()) db.add(model) db.commit() db.refresh(model) return model @router.put("/models/{model_id}", response_model=AIModelConfigResponse) def update_model( model_id: int, data: AIModelConfigUpdate, db: Session = Depends(get_db), admin: User = Depends(require_admin), ): model = db.query(AIModelConfig).filter(AIModelConfig.id == model_id).first() if not model: raise HTTPException(status_code=404, detail="Model config not found") update_data = data.model_dump(exclude_unset=True) task = update_data.get("task", model.task) if update_data.get("is_default"): db.query(AIModelConfig).filter( AIModelConfig.task == task, AIModelConfig.is_default == True, AIModelConfig.id != model_id, ).update({"is_default": False}) for key, value in update_data.items(): setattr(model, key, value) db.commit() db.refresh(model) return model @router.delete("/models/{model_id}", status_code=204) def delete_model( model_id: int, db: Session = Depends(get_db), admin: User = Depends(require_admin), ): model = db.query(AIModelConfig).filter(AIModelConfig.id == model_id).first() if not model: raise HTTPException(status_code=404, detail="Model config not found") db.delete(model) db.commit()