These dependencies check actor type (human user vs agent), not admin privilege. The old names were misleading and could cause authorization mistakes when wiring new endpoints. Renamed across all 10 consumer files along with their local ADMIN_AUTH_DEP / ADMIN_OR_AGENT_DEP aliases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""API routes for searching and fetching souls-directory markdown entries."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
from app.api.deps import ActorContext, require_user_or_agent
|
|
from app.schemas.souls_directory import (
|
|
SoulsDirectoryMarkdownResponse,
|
|
SoulsDirectorySearchResponse,
|
|
SoulsDirectorySoulRef,
|
|
)
|
|
from app.services import souls_directory
|
|
|
|
router = APIRouter(prefix="/souls-directory", tags=["souls-directory"])
|
|
USER_OR_AGENT_DEP = Depends(require_user_or_agent)
|
|
|
|
_SAFE_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
|
|
_SAFE_SLUG_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
|
|
|
|
|
|
def _validate_segment(value: str, *, field: str) -> str:
|
|
cleaned = value.strip().strip("/")
|
|
if not cleaned:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
detail=f"{field} is required",
|
|
)
|
|
if field == "handle":
|
|
ok = bool(_SAFE_SEGMENT_RE.match(cleaned))
|
|
else:
|
|
ok = bool(_SAFE_SLUG_RE.match(cleaned))
|
|
if not ok:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
detail=f"{field} contains unsupported characters",
|
|
)
|
|
return cleaned
|
|
|
|
|
|
@router.get("/search", response_model=SoulsDirectorySearchResponse)
|
|
async def search(
|
|
q: str = Query(default="", min_length=0),
|
|
limit: int = Query(default=20, ge=1, le=100),
|
|
_actor: ActorContext = USER_OR_AGENT_DEP,
|
|
) -> SoulsDirectorySearchResponse:
|
|
"""Search souls-directory entries by handle/slug query text."""
|
|
refs = await souls_directory.list_souls_directory_refs()
|
|
matches = souls_directory.search_souls(refs, query=q, limit=limit)
|
|
items = [
|
|
SoulsDirectorySoulRef(
|
|
handle=ref.handle,
|
|
slug=ref.slug,
|
|
page_url=ref.page_url,
|
|
raw_md_url=ref.raw_md_url,
|
|
)
|
|
for ref in matches
|
|
]
|
|
return SoulsDirectorySearchResponse(items=items)
|
|
|
|
|
|
@router.get("/{handle}/{slug}.md", response_model=SoulsDirectoryMarkdownResponse)
|
|
@router.get("/{handle}/{slug}", response_model=SoulsDirectoryMarkdownResponse)
|
|
async def get_markdown(
|
|
handle: str,
|
|
slug: str,
|
|
_actor: ActorContext = USER_OR_AGENT_DEP,
|
|
) -> SoulsDirectoryMarkdownResponse:
|
|
"""Fetch markdown content for a validated souls-directory handle and slug."""
|
|
safe_handle = _validate_segment(handle, field="handle")
|
|
safe_slug = _validate_segment(slug.removesuffix(".md"), field="slug")
|
|
try:
|
|
content = await souls_directory.fetch_soul_markdown(
|
|
handle=safe_handle,
|
|
slug=safe_slug,
|
|
)
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=str(exc),
|
|
) from exc
|
|
return SoulsDirectoryMarkdownResponse(
|
|
handle=safe_handle,
|
|
slug=safe_slug,
|
|
content=content,
|
|
)
|