feat: add API Key authentication, Dockerfile, and docker-compose
- core/auth.py: API Key auth via header X-API-Key (env PDFTOOLKIT_API_KEY) - Modo dev: sem chave configurada → acesso livre - Modo produção: exige chave, retorna 401/403 - api.py: endpoints protegidos com Depends(_auth_required) - /health e / permanecem públicos - Dockerfile: multi-stage (base → api → web) - docker-compose.yml: api + web com healthcheck, volumes e rede - .env.example: template de variáveis de ambiente - README.md: seções de autenticação e Docker
This commit is contained in:
137
api.py
137
api.py
@@ -1,23 +1,28 @@
|
||||
"""
|
||||
pdf-toolkit API — FastAPI com documentação OpenAPI automática.
|
||||
pdf-toolkit API — FastAPI com documentação OpenAPI automática e autenticação.
|
||||
|
||||
Endpoints REST para todas as operações do toolkit.
|
||||
Documentação interativa disponível em /docs (Swagger) e /redoc.
|
||||
|
||||
Autenticação via header X-API-Key.
|
||||
Variável de ambiente: PDFTOOLKIT_API_KEY
|
||||
Se não definida, a API roda sem autenticação (modo dev).
|
||||
|
||||
Uso:
|
||||
uvicorn api:app --host 0.0.0.0 --port 8555 --reload
|
||||
"""
|
||||
|
||||
import io
|
||||
import zipfile
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Query
|
||||
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Depends
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from config import API_TITLE, API_VERSION, API_DESCRIPTION, MAX_UPLOAD_SIZE_BYTES, QUALITY_PRESETS
|
||||
from core.engine import PDFEngine
|
||||
from core.auth import verify_api_key, is_auth_enabled
|
||||
|
||||
engine = PDFEngine()
|
||||
|
||||
@@ -38,6 +43,16 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ── Dependência de autenticação ─────────────────────────────────────
|
||||
# Endpoints públicos (health, root) usam auth opcional
|
||||
# Endpoints de operação usam auth obrigatório (se configurado)
|
||||
|
||||
|
||||
def _auth_required(api_key: str = Depends(verify_api_key)):
|
||||
"""Dependência que aplica autenticação nos endpoints operacionais."""
|
||||
return api_key
|
||||
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -69,23 +84,35 @@ def _zip_response(files: List[tuple], zip_name: str = "resultados.zip") -> Strea
|
||||
)
|
||||
|
||||
|
||||
# ── Endpoint Raiz ───────────────────────────────────────────────────
|
||||
# ── Endpoints Públicos (sem auth) ───────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
auth_info = {"authentication": "enabled"} if is_auth_enabled() else {"authentication": "disabled (dev mode)"}
|
||||
return {
|
||||
"app": API_TITLE,
|
||||
"version": API_VERSION,
|
||||
"docs": "/docs",
|
||||
"redoc": "/redoc",
|
||||
**auth_info,
|
||||
}
|
||||
|
||||
|
||||
# ── Inspect ─────────────────────────────────────────────────────────
|
||||
@app.get("/health")
|
||||
def health():
|
||||
"""Health check — sem autenticação."""
|
||||
return {"status": "ok", "version": API_VERSION, "auth": is_auth_enabled()}
|
||||
|
||||
|
||||
@app.post("/inspect", summary="Inspeciona metadados de um PDF")
|
||||
# ── Endpoints Protegidos ────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post(
|
||||
"/inspect",
|
||||
summary="Inspeciona metadados de um PDF",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def inspect(file: UploadFile = File(...)):
|
||||
"""
|
||||
Retorna metadados detalhados do PDF: número de páginas,
|
||||
@@ -98,10 +125,11 @@ def inspect(file: UploadFile = File(...)):
|
||||
return info
|
||||
|
||||
|
||||
# ── Compress ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/compress", summary="Comprime um PDF")
|
||||
@app.post(
|
||||
"/compress",
|
||||
summary="Comprime um PDF",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def compress(
|
||||
file: UploadFile = File(...),
|
||||
preset: str = Form("ebook"),
|
||||
@@ -115,14 +143,15 @@ def compress(
|
||||
raise HTTPException(400, f"Preset inválido. Opções: {', '.join(QUALITY_PRESETS.keys())}")
|
||||
data = _read_file(file)
|
||||
result = engine.compress(data, preset=preset)
|
||||
out_name = (file.filename or "documento.pdf").replace(".pdf", f"_comprimido.pdf")
|
||||
out_name = (file.filename or "documento.pdf").replace(".pdf", "_comprimido.pdf")
|
||||
return _pdf_response(result, out_name)
|
||||
|
||||
|
||||
# ── PDF/A → PDF comum ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/pdfa-to-regular", summary="Converte PDF/A para PDF comum")
|
||||
@app.post(
|
||||
"/pdfa-to-regular",
|
||||
summary="Converte PDF/A para PDF comum",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def pdfa_to_regular(file: UploadFile = File(...)):
|
||||
"""Remove restrições PDF/A gerando um PDF comum padrão."""
|
||||
data = _read_file(file)
|
||||
@@ -130,15 +159,16 @@ def pdfa_to_regular(file: UploadFile = File(...)):
|
||||
if info.get("is_pdfa"):
|
||||
result = engine.pdfa_to_regular(data)
|
||||
else:
|
||||
result = data # já é PDF comum
|
||||
result = data
|
||||
out_name = (file.filename or "documento.pdf").replace(".pdf", "_comum.pdf")
|
||||
return _pdf_response(result, out_name)
|
||||
|
||||
|
||||
# ── Compress PDF/A ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/compress-pdfa", summary="Comprime mantendo PDF/A")
|
||||
@app.post(
|
||||
"/compress-pdfa",
|
||||
summary="Comprime mantendo PDF/A",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def compress_pdfa(
|
||||
file: UploadFile = File(...),
|
||||
quality: int = Form(60, ge=10, le=100),
|
||||
@@ -150,10 +180,11 @@ def compress_pdfa(
|
||||
return _pdf_response(result, out_name)
|
||||
|
||||
|
||||
# ── Merge ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/merge", summary="Junta múltiplos PDFs em um")
|
||||
@app.post(
|
||||
"/merge",
|
||||
summary="Junta múltiplos PDFs em um",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
async def merge(files: List[UploadFile] = File(...)):
|
||||
"""Mescla 2 ou mais PDFs em ordem, retornando um único PDF."""
|
||||
if len(files) < 2:
|
||||
@@ -166,10 +197,11 @@ async def merge(files: List[UploadFile] = File(...)):
|
||||
return _pdf_response(result, "merged.pdf")
|
||||
|
||||
|
||||
# ── Split ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/split", summary="Extrai intervalos de páginas de um PDF")
|
||||
@app.post(
|
||||
"/split",
|
||||
summary="Extrai intervalos de páginas de um PDF",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def split(
|
||||
file: UploadFile = File(...),
|
||||
ranges: str = Form(..., description='Ex: "1-3,5,7-9"'),
|
||||
@@ -204,17 +236,16 @@ def split(
|
||||
return _zip_response(results, "split.zip")
|
||||
|
||||
|
||||
# ── Extract pages ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/extract-pages", summary="Extrai páginas específicas")
|
||||
@app.post(
|
||||
"/extract-pages",
|
||||
summary="Extrai páginas específicas",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def extract_pages(
|
||||
file: UploadFile = File(...),
|
||||
pages: str = Form(..., description='Ex: "1,3,5" (1-indexado)'),
|
||||
):
|
||||
"""
|
||||
Extrai páginas específicas de um PDF em ordem.
|
||||
"""
|
||||
"""Extrai páginas específicas de um PDF em ordem."""
|
||||
data = _read_file(file)
|
||||
reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data))
|
||||
total = len(reader.pages)
|
||||
@@ -233,10 +264,11 @@ def extract_pages(
|
||||
return _pdf_response(result, out_name)
|
||||
|
||||
|
||||
# ── Rotate ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/rotate", summary="Rotaciona páginas de um PDF")
|
||||
@app.post(
|
||||
"/rotate",
|
||||
summary="Rotaciona páginas de um PDF",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def rotate(
|
||||
file: UploadFile = File(...),
|
||||
pages: str = Form(..., description='Páginas: "1,3,5" ou "todas"'),
|
||||
@@ -259,10 +291,11 @@ def rotate(
|
||||
return _pdf_response(result, out_name)
|
||||
|
||||
|
||||
# ── Extract text ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/extract-text", summary="Extrai texto de um PDF")
|
||||
@app.post(
|
||||
"/extract-text",
|
||||
summary="Extrai texto de um PDF",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def extract_text_endpoint(file: UploadFile = File(...)):
|
||||
"""Extrai todo o texto legível de um PDF e retorna como .txt."""
|
||||
data = _read_file(file)
|
||||
@@ -276,10 +309,11 @@ def extract_text_endpoint(file: UploadFile = File(...)):
|
||||
)
|
||||
|
||||
|
||||
# ── Extract images ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/extract-images", summary="Extrai imagens de um PDF como ZIP")
|
||||
@app.post(
|
||||
"/extract-images",
|
||||
summary="Extrai imagens de um PDF como ZIP",
|
||||
dependencies=[Depends(_auth_required)],
|
||||
)
|
||||
def extract_images_endpoint(file: UploadFile = File(...)):
|
||||
"""Extrai todas as imagens embutidas no PDF e retorna um ZIP."""
|
||||
data = _read_file(file)
|
||||
@@ -289,15 +323,6 @@ def extract_images_endpoint(file: UploadFile = File(...)):
|
||||
return _zip_response(images, "imagens.zip")
|
||||
|
||||
|
||||
# ── Health check ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
"""Health check simples."""
|
||||
return {"status": "ok", "version": API_VERSION}
|
||||
|
||||
|
||||
# ── Entrypoint ──────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user