103 lines
3.1 KiB
Python
103 lines
3.1 KiB
Python
"""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"),
|
|
}
|