Deployments reachable outside a trusted LAN need some form of access control. Set FUELISH_API_KEY and the server requires a matching X-API-Key header on /jobs; unset, behavior is unchanged (no auth). The Android app gets a matching Settings field. This only checks the key - TLS termination is left to a reverse proxy in front of the server, not handled here.
This commit is contained in:
@@ -10,8 +10,13 @@ Jobs are processed asynchronously so the app can show real per-stage
|
||||
progress: POST /jobs returns immediately with a job id; GET /jobs/{id}
|
||||
reports which of preprocessing/ocr/extracting is currently running, and
|
||||
the final result once done. Job state lives in memory only - this is a
|
||||
personal, LAN-only, single-instance server with no need for persistence
|
||||
here (the Android app is the system of record for confirmed receipts).
|
||||
personal, single-instance server with no need for persistence here (the
|
||||
Android app is the system of record for confirmed receipts).
|
||||
|
||||
Auth: set FUELISH_API_KEY to require a matching X-API-Key header on
|
||||
/jobs (GET /health stays open for reverse-proxy health checks). Leave it
|
||||
unset for the original LAN-only/no-auth setup. This server does not
|
||||
terminate TLS itself - see require_api_key()'s docstring.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
@@ -21,17 +26,34 @@ import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI, HTTPException, UploadFile
|
||||
from fastapi import Depends, FastAPI, HTTPException, Security, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
|
||||
from adaptive import preprocess_and_ocr_adaptive
|
||||
from extract import structure
|
||||
|
||||
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434")
|
||||
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5:3b")
|
||||
API_KEY = os.environ.get("FUELISH_API_KEY", "")
|
||||
|
||||
app = FastAPI(title="fuelish")
|
||||
|
||||
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
|
||||
def require_api_key(provided: str | None = Security(_api_key_header)) -> None:
|
||||
"""No-op when FUELISH_API_KEY isn't set - the unchanged default for a
|
||||
LAN-only deployment with no key configured. When it is set, every
|
||||
request to a protected route must carry a matching X-API-Key header.
|
||||
This only checks the key; it doesn't terminate TLS - a reverse proxy
|
||||
in front of this server (e.g. Traefik) is expected to do that for any
|
||||
deployment reachable outside a trusted LAN."""
|
||||
if not API_KEY:
|
||||
return
|
||||
if provided != API_KEY:
|
||||
raise HTTPException(status_code=401, detail="invalid or missing API key")
|
||||
|
||||
_jobs: dict[str, dict] = {}
|
||||
_jobs_lock = threading.Lock()
|
||||
|
||||
@@ -96,7 +118,7 @@ def _process_job(job_id: str, image_path: str, scratch_dir: str) -> None:
|
||||
shutil.rmtree(scratch_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@app.post("/jobs", status_code=201)
|
||||
@app.post("/jobs", status_code=201, dependencies=[Depends(require_api_key)])
|
||||
async def create_job(image: UploadFile) -> JSONResponse:
|
||||
job_id = uuid.uuid4().hex
|
||||
scratch_dir = tempfile.mkdtemp(prefix=f"fuelish-{job_id}-")
|
||||
@@ -113,7 +135,7 @@ async def create_job(image: UploadFile) -> JSONResponse:
|
||||
return JSONResponse({"job_id": job_id}, status_code=201)
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}")
|
||||
@app.get("/jobs/{job_id}", dependencies=[Depends(require_api_key)])
|
||||
def get_job(job_id: str) -> dict:
|
||||
with _jobs_lock:
|
||||
job = _jobs.get(job_id)
|
||||
|
||||
Reference in New Issue
Block a user