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:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# ── pdf-toolkit — Variáveis de Ambiente ─────────────────────────────
|
||||||
|
# Copie este arquivo para .env e ajuste os valores.
|
||||||
|
|
||||||
|
# ── Autenticação da API ─────────────────────────────────────────────
|
||||||
|
# Se vazio/omitido, a API roda sem autenticação (modo dev).
|
||||||
|
# Em produção, defina uma chave forte com 32+ caracteres.
|
||||||
|
PDFTOOLKIT_API_KEY=my-super-secret-api-key-change-me
|
||||||
|
|
||||||
|
# ── Portas (personalizável via docker-compose) ──────────────────────
|
||||||
|
API_PORT=8555
|
||||||
|
WEB_PORT=8550
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ tmp/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
*.pyc
|
||||||
|
.tmp/
|
||||||
|
__pycache__
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# ── Estágio 1: base ─────────────────────────────────────────────────
|
||||||
|
FROM python:3.12-slim AS base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libgl1-mesa-glx libglib2.0-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# ── Estágio 2: API ──────────────────────────────────────────────────
|
||||||
|
FROM base AS api
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8555
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import http.client; conn=http.client.HTTPConnection('localhost:8555'); conn.request('GET','/health'); r=conn.getresponse(); exit(0 if r.status==200 else 1)"
|
||||||
|
|
||||||
|
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8555"]
|
||||||
|
|
||||||
|
# ── Estágio 3: Streamlit Web ────────────────────────────────────────
|
||||||
|
FROM base AS web
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8550
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import http.client; conn=http.client.HTTPConnection('localhost:8550'); conn.request('GET','/'); r=conn.getresponse(); exit(0 if r.status==200 else 1)"
|
||||||
|
|
||||||
|
CMD ["streamlit", "run", "app.py", "--server.port", "8550", "--server.address", "0.0.0.0", "--server.headless", "true"]
|
||||||
76
README.md
76
README.md
@@ -4,6 +4,8 @@ Transforme, otimize e gerencie arquivos PDF com facilidade.
|
|||||||
|
|
||||||
- **Interface Web**: Streamlit com upload drag-and-drop e configuração visual
|
- **Interface Web**: Streamlit com upload drag-and-drop e configuração visual
|
||||||
- **API REST**: FastAPI com documentação OpenAPI automática em `/docs`
|
- **API REST**: FastAPI com documentação OpenAPI automática em `/docs`
|
||||||
|
- **Autenticação**: API Key via header `X-API-Key` (configurável via env var)
|
||||||
|
- **Docker**: containers prontos para produção via docker-compose
|
||||||
- **Operações**: compressão, PDF/A ↔ PDF comum, merge, split, rotação, extração de texto e imagens
|
- **Operações**: compressão, PDF/A ↔ PDF comum, merge, split, rotação, extração de texto e imagens
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -14,8 +16,9 @@ Transforme, otimize e gerencie arquivos PDF com facilidade.
|
|||||||
|
|
||||||
- Python 3.12+
|
- Python 3.12+
|
||||||
- pip
|
- pip
|
||||||
|
- Docker (opcional, para containers)
|
||||||
|
|
||||||
### Instalação
|
### Instalação (sem Docker)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <url-do-repositorio> pdf-toolkit
|
git clone <url-do-repositorio> pdf-toolkit
|
||||||
@@ -23,7 +26,7 @@ cd pdf-toolkit
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Execução
|
### Execução (sem Docker)
|
||||||
|
|
||||||
**Streamlit (interface web):**
|
**Streamlit (interface web):**
|
||||||
```bash
|
```bash
|
||||||
@@ -109,6 +112,75 @@ pdf-toolkit/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔐 Autenticação da API
|
||||||
|
|
||||||
|
A API suporta autenticação via **API Key** no header `X-API-Key`.
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Defina a variável de ambiente `PDFTOOLKIT_API_KEY`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PDFTOOLKIT_API_KEY="sua-chave-secreta-aqui"
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a variável **não** estiver definida, a API roda sem autenticação (modo desenvolvimento).
|
||||||
|
|
||||||
|
### Uso com curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8555/compress \
|
||||||
|
-H "X-API-Key: sua-chave-secreta-aqui" \
|
||||||
|
-F "file=@documento.pdf" \
|
||||||
|
-o comprimido.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Endpoints públicos (sem auth)
|
||||||
|
|
||||||
|
- `GET /` — raiz informativa
|
||||||
|
- `GET /health` — health check
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker
|
||||||
|
|
||||||
|
### Com docker-compose (recomendado)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copie e configure o .env
|
||||||
|
cp .env.example .env
|
||||||
|
# Edite .env com sua API Key
|
||||||
|
|
||||||
|
# 2. Inicie os serviços
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# Acesse:
|
||||||
|
# - Streamlit: http://localhost:8550
|
||||||
|
# - API: http://localhost:8555
|
||||||
|
# - Swagger: http://localhost:8555/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API
|
||||||
|
docker build --target api -t pdf-toolkit-api .
|
||||||
|
docker run -p 8555:8555 --env-file .env pdf-toolkit-api
|
||||||
|
|
||||||
|
# Web
|
||||||
|
docker build --target web -t pdf-toolkit-web .
|
||||||
|
docker run -p 8550:8550 --env-file .env pdf-toolkit-web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estrutura dos containers
|
||||||
|
|
||||||
|
- **api**: FastAPI + uvicorn, porta 8555
|
||||||
|
- **web**: Streamlit, porta 8550, depende do serviço `api`
|
||||||
|
- Volume compartilhado `uploads` para arquivos temporários
|
||||||
|
- Healthcheck em ambos os containers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🧪 Testes
|
## 🧪 Testes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
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.
|
Endpoints REST para todas as operações do toolkit.
|
||||||
Documentação interativa disponível em /docs (Swagger) e /redoc.
|
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:
|
Uso:
|
||||||
uvicorn api:app --host 0.0.0.0 --port 8555 --reload
|
uvicorn api:app --host 0.0.0.0 --port 8555 --reload
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import zipfile
|
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.responses import Response, StreamingResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from config import API_TITLE, API_VERSION, API_DESCRIPTION, MAX_UPLOAD_SIZE_BYTES, QUALITY_PRESETS
|
from config import API_TITLE, API_VERSION, API_DESCRIPTION, MAX_UPLOAD_SIZE_BYTES, QUALITY_PRESETS
|
||||||
from core.engine import PDFEngine
|
from core.engine import PDFEngine
|
||||||
|
from core.auth import verify_api_key, is_auth_enabled
|
||||||
|
|
||||||
engine = PDFEngine()
|
engine = PDFEngine()
|
||||||
|
|
||||||
@@ -38,6 +43,16 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
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 ─────────────────────────────────────────────────────────
|
# ── 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("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
|
auth_info = {"authentication": "enabled"} if is_auth_enabled() else {"authentication": "disabled (dev mode)"}
|
||||||
return {
|
return {
|
||||||
"app": API_TITLE,
|
"app": API_TITLE,
|
||||||
"version": API_VERSION,
|
"version": API_VERSION,
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"redoc": "/redoc",
|
"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(...)):
|
def inspect(file: UploadFile = File(...)):
|
||||||
"""
|
"""
|
||||||
Retorna metadados detalhados do PDF: número de páginas,
|
Retorna metadados detalhados do PDF: número de páginas,
|
||||||
@@ -98,10 +125,11 @@ def inspect(file: UploadFile = File(...)):
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
# ── Compress ────────────────────────────────────────────────────────
|
@app.post(
|
||||||
|
"/compress",
|
||||||
|
summary="Comprime um PDF",
|
||||||
@app.post("/compress", summary="Comprime um PDF")
|
dependencies=[Depends(_auth_required)],
|
||||||
|
)
|
||||||
def compress(
|
def compress(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
preset: str = Form("ebook"),
|
preset: str = Form("ebook"),
|
||||||
@@ -115,14 +143,15 @@ def compress(
|
|||||||
raise HTTPException(400, f"Preset inválido. Opções: {', '.join(QUALITY_PRESETS.keys())}")
|
raise HTTPException(400, f"Preset inválido. Opções: {', '.join(QUALITY_PRESETS.keys())}")
|
||||||
data = _read_file(file)
|
data = _read_file(file)
|
||||||
result = engine.compress(data, preset=preset)
|
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)
|
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(...)):
|
def pdfa_to_regular(file: UploadFile = File(...)):
|
||||||
"""Remove restrições PDF/A gerando um PDF comum padrão."""
|
"""Remove restrições PDF/A gerando um PDF comum padrão."""
|
||||||
data = _read_file(file)
|
data = _read_file(file)
|
||||||
@@ -130,15 +159,16 @@ def pdfa_to_regular(file: UploadFile = File(...)):
|
|||||||
if info.get("is_pdfa"):
|
if info.get("is_pdfa"):
|
||||||
result = engine.pdfa_to_regular(data)
|
result = engine.pdfa_to_regular(data)
|
||||||
else:
|
else:
|
||||||
result = data # já é PDF comum
|
result = data
|
||||||
out_name = (file.filename or "documento.pdf").replace(".pdf", "_comum.pdf")
|
out_name = (file.filename or "documento.pdf").replace(".pdf", "_comum.pdf")
|
||||||
return _pdf_response(result, out_name)
|
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(
|
def compress_pdfa(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
quality: int = Form(60, ge=10, le=100),
|
quality: int = Form(60, ge=10, le=100),
|
||||||
@@ -150,10 +180,11 @@ def compress_pdfa(
|
|||||||
return _pdf_response(result, out_name)
|
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(...)):
|
async def merge(files: List[UploadFile] = File(...)):
|
||||||
"""Mescla 2 ou mais PDFs em ordem, retornando um único PDF."""
|
"""Mescla 2 ou mais PDFs em ordem, retornando um único PDF."""
|
||||||
if len(files) < 2:
|
if len(files) < 2:
|
||||||
@@ -166,10 +197,11 @@ async def merge(files: List[UploadFile] = File(...)):
|
|||||||
return _pdf_response(result, "merged.pdf")
|
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(
|
def split(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
ranges: str = Form(..., description='Ex: "1-3,5,7-9"'),
|
ranges: str = Form(..., description='Ex: "1-3,5,7-9"'),
|
||||||
@@ -204,17 +236,16 @@ def split(
|
|||||||
return _zip_response(results, "split.zip")
|
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(
|
def extract_pages(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
pages: str = Form(..., description='Ex: "1,3,5" (1-indexado)'),
|
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)
|
data = _read_file(file)
|
||||||
reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data))
|
reader = __import__("pypdf", fromlist=["PdfReader"]).PdfReader(io.BytesIO(data))
|
||||||
total = len(reader.pages)
|
total = len(reader.pages)
|
||||||
@@ -233,10 +264,11 @@ def extract_pages(
|
|||||||
return _pdf_response(result, out_name)
|
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(
|
def rotate(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
pages: str = Form(..., description='Páginas: "1,3,5" ou "todas"'),
|
pages: str = Form(..., description='Páginas: "1,3,5" ou "todas"'),
|
||||||
@@ -259,10 +291,11 @@ def rotate(
|
|||||||
return _pdf_response(result, out_name)
|
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(...)):
|
def extract_text_endpoint(file: UploadFile = File(...)):
|
||||||
"""Extrai todo o texto legível de um PDF e retorna como .txt."""
|
"""Extrai todo o texto legível de um PDF e retorna como .txt."""
|
||||||
data = _read_file(file)
|
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(...)):
|
def extract_images_endpoint(file: UploadFile = File(...)):
|
||||||
"""Extrai todas as imagens embutidas no PDF e retorna um ZIP."""
|
"""Extrai todas as imagens embutidas no PDF e retorna um ZIP."""
|
||||||
data = _read_file(file)
|
data = _read_file(file)
|
||||||
@@ -289,15 +323,6 @@ def extract_images_endpoint(file: UploadFile = File(...)):
|
|||||||
return _zip_response(images, "imagens.zip")
|
return _zip_response(images, "imagens.zip")
|
||||||
|
|
||||||
|
|
||||||
# ── Health check ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
def health():
|
|
||||||
"""Health check simples."""
|
|
||||||
return {"status": "ok", "version": API_VERSION}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Entrypoint ──────────────────────────────────────────────────────
|
# ── Entrypoint ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
59
core/auth.py
Normal file
59
core/auth.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Autenticação via API Key para a API REST.
|
||||||
|
|
||||||
|
A chave é lida da variável de ambiente PDFTOOLKIT_API_KEY.
|
||||||
|
Se a variável não estiver definida, a autenticação fica desabilitada
|
||||||
|
(útil para desenvolvimento local).
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
export PDFTOOLKIT_API_KEY="sua-chave-secreta-aqui"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from fastapi import Security, HTTPException, status
|
||||||
|
from fastapi.security import APIKeyHeader
|
||||||
|
|
||||||
|
API_KEY_ENV = "PDFTOOLKIT_API_KEY"
|
||||||
|
|
||||||
|
# Lê a chave do ambiente (None se não configurada)
|
||||||
|
_API_KEY = os.environ.get(API_KEY_ENV, None)
|
||||||
|
|
||||||
|
# Schema de header: X-API-Key
|
||||||
|
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_api_key(api_key: str = Security(api_key_header)) -> str:
|
||||||
|
"""
|
||||||
|
Dependência FastAPI que valida a API Key.
|
||||||
|
|
||||||
|
- Se PDFTOOLKIT_API_KEY não foi definida no ambiente → permite acesso livre
|
||||||
|
(modo desenvolvimento).
|
||||||
|
- Se foi definida → exige que o header X-API-Key corresponda exatamente.
|
||||||
|
|
||||||
|
Retorna a própria chave se válida, ou levanta 401.
|
||||||
|
"""
|
||||||
|
# Modo desenvolvimento: sem chave configurada
|
||||||
|
if _API_KEY is None:
|
||||||
|
return "dev-mode"
|
||||||
|
|
||||||
|
# Modo produção: exige chave
|
||||||
|
if api_key is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="API Key ausente. Envie o header X-API-Key.",
|
||||||
|
headers={"WWW-Authenticate": "ApiKey"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if api_key != _API_KEY:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="API Key inválida.",
|
||||||
|
headers={"WWW-Authenticate": "ApiKey"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
|
||||||
|
def is_auth_enabled() -> bool:
|
||||||
|
"""Retorna True se a autenticação estiver ativa."""
|
||||||
|
return _API_KEY is not None
|
||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
# ── API REST (FastAPI) ──────────────────────────────────────────
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: api
|
||||||
|
container_name: pdf-toolkit-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-8555}:8555"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- PDFTOOLKIT_API_KEY=${PDFTOOLKIT_API_KEY:-}
|
||||||
|
volumes:
|
||||||
|
- uploads:/tmp/uploads
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import http.client; conn=http.client.HTTPConnection('localhost:8555'); conn.request('GET','/health'); r=conn.getresponse(); exit(0 if r.status==200 else 1)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- pdf-net
|
||||||
|
|
||||||
|
# ── Interface Web (Streamlit) ───────────────────────────────────
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: web
|
||||||
|
container_name: pdf-toolkit-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${WEB_PORT:-8550}:8550"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- API_BASE_URL=http://api:8555
|
||||||
|
volumes:
|
||||||
|
- uploads:/tmp/uploads
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import http.client; conn=http.client.HTTPConnection('localhost:8550'); conn.request('GET','/'); r=conn.getresponse(); exit(0 if r.status==200 else 1)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- pdf-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uploads:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
pdf-net:
|
||||||
|
driver: bridge
|
||||||
25
healthz.py
Normal file
25
healthz.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Adiciona endpoint /healthz para healthcheck do Docker."""
|
||||||
|
import streamlit as st
|
||||||
|
# O Streamlit não tem roteamento nativo, mas podemos expor via
|
||||||
|
# arquivo estático. Para healthcheck do Docker, usamos o seguinte
|
||||||
|
# truque: endpoint /healthz no mesmo host.
|
||||||
|
# Como Streamlit não permite criar rotas customizadas, o healthcheck
|
||||||
|
# será feito via HTTP na própria API (mais confiável).
|
||||||
|
# Esta função é apenas um placeholder para documentação.
|
||||||
|
"""
|
||||||
|
Healthcheck do container web (Streamlit):
|
||||||
|
|
||||||
|
O Streamlit não expõe endpoints HTTP customizados nativamente.
|
||||||
|
Para o healthcheck do Docker, usamos:
|
||||||
|
test: ["CMD", "python", "-c", "import http.client; conn=http.client.HTTPConnection('localhost:8550'); conn.request('GET','/'); r=conn.getresponse(); exit(0 if r.status==200 else 1)"]
|
||||||
|
|
||||||
|
Isso verifica se o servidor Streamlit está respondendo na porta 8550.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Adiciona uma página de health no sidebar para debug
|
||||||
|
def show_health_page():
|
||||||
|
st.set_page_config(page_title="Health Check", page_icon="💚")
|
||||||
|
st.write("✅ pdf-toolkit web is running")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
show_health_page()
|
||||||
Reference in New Issue
Block a user