Files
kite/backend/api/topics.py
T
curo1305 1882edfff6 feat(02-02): auth API endpoints + security hardening + Python 3.9 compat
- backend/api/auth.py: register, login (TOTP+backup), refresh, logout,
  me, change-password; per-account Redis rate limit; HIBP check
- backend/main.py: Origin validation middleware, CSP headers middleware,
  CORS locked to settings.cors_origins, Redis lifespan (app.state.redis),
  admin bootstrap, auth router included, slowapi SlowAPIMiddleware
- backend/services/email.py: already created in Plan 01 (verified exists)
- Python 3.9 compat: fixed match statement in ai/__init__.py,
  str|None union syntax in openai_provider.py, api/documents.py,
  api/topics.py, api/settings.py, services/classifier.py

All 17 tests in test_auth_api.py pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:35:38 +02:00

83 lines
2.3 KiB
Python

from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from deps.db import get_db
from services import classifier, storage
router = APIRouter(prefix="/api/topics", tags=["topics"])
class TopicCreate(BaseModel):
name: str
description: str = ""
color: str = "#6366f1"
class TopicUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
class SuggestRequest(BaseModel):
document_id: str
@router.get("")
async def list_topics(session: AsyncSession = Depends(get_db)):
topics = await storage.load_topics(session)
counts = await storage.topic_doc_counts(session)
for t in topics:
t["doc_count"] = counts.get(t["name"], 0)
return {"topics": topics}
@router.post("")
async def create_topic(body: TopicCreate, session: AsyncSession = Depends(get_db)):
topic = await storage.create_topic(session, body.name, body.description, body.color)
topic["doc_count"] = 0
return topic
@router.patch("/{topic_id}")
async def update_topic(
topic_id: str,
body: TopicUpdate,
session: AsyncSession = Depends(get_db),
):
topic = await storage.update_topic(
session,
topic_id,
name=body.name,
description=body.description,
color=body.color,
)
if topic is None:
raise HTTPException(404, "Topic not found")
counts = await storage.topic_doc_counts(session)
topic["doc_count"] = counts.get(topic["name"], 0)
return topic
@router.delete("/{topic_id}")
async def delete_topic(topic_id: str, session: AsyncSession = Depends(get_db)):
name = await storage.delete_topic(session, topic_id)
if name is None:
raise HTTPException(404, "Topic not found")
return {"success": True, "removed_from_documents": True}
@router.post("/suggest")
async def suggest_topics(body: SuggestRequest, session: AsyncSession = Depends(get_db)):
meta = await storage.get_metadata(session, body.document_id)
if meta is None:
raise HTTPException(404, "Document not found")
try:
suggestions = await classifier.suggest_topics_for_document(session, body.document_id)
except Exception as e:
raise HTTPException(500, f"Suggestion failed: {e}")
return {"suggested": suggestions}