"""Authenticated documentation APIs.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy import text from app.core.security import decode_token from app.db.session import async_session_factory from app.models.user import User from app.services.docs_gatekeeper import ( DOCS_BY_SLUG, VALID_DOCS_LANGS, can_read_doc, catalog_for_user, doc_path_for, title_for, ) router = APIRouter() optional_bearer = HTTPBearer(auto_error=False) async def get_optional_current_user( credentials: HTTPAuthorizationCredentials | None = Depends(optional_bearer), ) -> User | None: if credentials is None: return None payload = decode_token(credentials.credentials) if payload is None or payload.get("type") != "access" or payload.get("sub") is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token", ) async with async_session_factory() as db: result = await db.execute( text( "SELECT id, username, email, password_hash, role, is_active, gatekeeper_groups FROM users WHERE id = :id" ), {"id": int(payload["sub"])}, ) row = result.fetchone() if row is None or not row[5]: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive", ) user = User() user.id = row[0] user.username = row[1] user.email = row[2] user.password_hash = row[3] user.role = row[4] user.is_active = row[5] user.gatekeeper_groups = row[6] or [] return user @router.get("/catalog") async def get_docs_catalog(current_user: User | None = Depends(get_optional_current_user)): return { "items": catalog_for_user(current_user), "authenticated": current_user is not None, } @router.get("/{lang}/{slug}") async def get_doc_content( lang: str, slug: str, current_user: User | None = Depends(get_optional_current_user), ): if lang not in VALID_DOCS_LANGS: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found") entry = DOCS_BY_SLUG.get(slug) if entry is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found") path = doc_path_for(entry, lang) if not path.exists(): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found") if not can_read_doc(entry, current_user): if current_user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient Docs permissions") return { "slug": entry.slug, "filename": entry.filename, "lang": lang, "title": title_for(entry, lang), "group": entry.group, "order": entry.order, "access": entry.access, "markdown": path.read_text(encoding="utf-8"), }