Complete Section 1 security/auth hardening
This commit is contained in:
@@ -7,11 +7,18 @@ Admin endpoints for AI question generation playground.
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy import and_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.auth import (
|
||||
AuthContext,
|
||||
ensure_website_scope_matches,
|
||||
get_auth_context,
|
||||
require_website_auth,
|
||||
)
|
||||
from app.core.rate_limit import enforce_rate_limit
|
||||
from app.database import get_db
|
||||
from app.models.item import Item
|
||||
from app.schemas.ai import (
|
||||
@@ -58,8 +65,10 @@ router = APIRouter(prefix="/admin/ai", tags=["admin", "ai-generation"])
|
||||
},
|
||||
)
|
||||
async def generate_preview(
|
||||
request_http: Request,
|
||||
request: AIGeneratePreviewRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
) -> AIGeneratePreviewResponse:
|
||||
"""
|
||||
Generate AI question preview (no database save).
|
||||
@@ -68,6 +77,14 @@ async def generate_preview(
|
||||
- **target_level**: Target difficulty (mudah/sulit)
|
||||
- **ai_model**: OpenRouter model to use (default: qwen/qwen2.5-32b-instruct)
|
||||
"""
|
||||
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
|
||||
enforce_rate_limit(
|
||||
request_http,
|
||||
scope="ai.generate_preview",
|
||||
max_requests=40,
|
||||
window_seconds=300,
|
||||
)
|
||||
|
||||
# Validate AI model
|
||||
if not validate_ai_model(request.ai_model):
|
||||
supported = ", ".join(SUPPORTED_MODELS.keys())
|
||||
@@ -88,6 +105,7 @@ async def generate_preview(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Basis item not found: {request.basis_item_id}",
|
||||
)
|
||||
ensure_website_scope_matches(website_id, basis_item.website_id)
|
||||
|
||||
# Validate basis item is sedang level
|
||||
if basis_item.level != "sedang":
|
||||
@@ -158,8 +176,10 @@ async def generate_preview(
|
||||
},
|
||||
)
|
||||
async def generate_save(
|
||||
request_http: Request,
|
||||
request: AISaveRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
) -> AISaveResponse:
|
||||
"""
|
||||
Save AI-generated question to database.
|
||||
@@ -175,6 +195,15 @@ async def generate_save(
|
||||
- **level**: Difficulty level
|
||||
- **ai_model**: AI model used for generation
|
||||
"""
|
||||
website_id = require_website_auth(auth, allowed_roles={"admin", "system_admin"})
|
||||
enforce_rate_limit(
|
||||
request_http,
|
||||
scope="ai.generate_save",
|
||||
max_requests=40,
|
||||
window_seconds=300,
|
||||
)
|
||||
ensure_website_scope_matches(website_id, request.website_id)
|
||||
|
||||
# Verify basis item exists
|
||||
basis_result = await db.execute(
|
||||
select(Item).where(Item.id == request.basis_item_id)
|
||||
@@ -186,6 +215,7 @@ async def generate_save(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Basis item not found: {request.basis_item_id}",
|
||||
)
|
||||
ensure_website_scope_matches(website_id, basis_item.website_id)
|
||||
|
||||
# Check for duplicate (same tryout, website, slot, level)
|
||||
existing_result = await db.execute(
|
||||
@@ -256,10 +286,12 @@ async def generate_save(
|
||||
)
|
||||
async def get_stats(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
) -> AIStatsResponse:
|
||||
"""
|
||||
Get AI generation statistics.
|
||||
"""
|
||||
require_website_auth(auth, allowed_roles={"admin", "system_admin"})
|
||||
stats = await get_ai_stats(db)
|
||||
|
||||
return AIStatsResponse(
|
||||
@@ -276,10 +308,11 @@ async def get_stats(
|
||||
summary="List supported AI models",
|
||||
description="Returns list of supported AI models for question generation.",
|
||||
)
|
||||
async def list_models() -> dict:
|
||||
async def list_models(auth: AuthContext = Depends(get_auth_context)) -> dict:
|
||||
"""
|
||||
List supported AI models.
|
||||
"""
|
||||
require_website_auth(auth, allowed_roles={"admin", "system_admin"})
|
||||
return {
|
||||
"models": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user