From a9e23d94b1bf57794067a353fa3c9779beb5e221 Mon Sep 17 00:00:00 2001 From: Matteo Rosati Date: Wed, 18 Feb 2026 18:02:22 +0100 Subject: [PATCH] basic auth --- .dockerignore | 37 ++++++++++++++++++++++++++++++++++ .env.dist | 3 +++ Dockerfile | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ app.py | 30 ++++++++++++++++++++++++++-- entrypoint.sh | 16 +++++++++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.dist create mode 100644 Dockerfile create mode 100644 entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cca7a2b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# ── Python runtime artifacts ────────────────────────────────────────────────── +.venv/ +__pycache__/ +*.py[cod] +*.pyo +.ruff_cache/ +*.egg-info/ +dist/ +build/ + +# ── Secrets & credentials (mount as volumes or inject via env at runtime) ───── +.env +credentials.json + +# ── OS / editor noise ───────────────────────────────────────────────────────── +.DS_Store +.zed/ + +# ── VCS ─────────────────────────────────────────────────────────────────────── +.git/ +.gitignore + +# ── Analysis / data files (not needed at runtime) ───────────────────────────── +domande/ +domande.csv +domande.xlsx +risposte/ +risposte.md +parse_questions.py + +# ── Package-manager files (pip uses requirements.txt inside the image) ───────── +pyproject.toml +uv.lock + +# ── Docker files themselves ──────────────────────────────────────────────────── +Dockerfile +.dockerignore diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..615d153 --- /dev/null +++ b/.env.dist @@ -0,0 +1,3 @@ +GOOGLE_APPLICATION_CREDENTIALS=credentials.json +AUTH_USER= +PASSWORD= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..54e7f32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# ─── Stage 1: Builder ──────────────────────────────────────────────────────── +FROM python:3.13-slim AS builder + +# Native build tools required by packages with C extensions +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + libffi-dev \ + libssl-dev \ + make \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +COPY requirements.txt . + +# Install all dependencies into an isolated prefix so the runtime stage +# can copy them without pulling in the build toolchain. +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt + + +# ─── Stage 2: Runtime ──────────────────────────────────────────────────────── +FROM python:3.13-slim AS runtime + +# Copy compiled packages from the builder — no build tools in the final image +COPY --from=builder /install /usr/local + +WORKDIR /app + +# Entrypoint: materialises GOOGLE_APPLICATION_CREDENTIALS_JSON → temp file +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Application source +COPY app.py chain.py db.py ./ +COPY models/ models/ +COPY templates/ templates/ +COPY static/ static/ + +# Prompt templates read at runtime by chain.py +COPY prompt.md question_rewrite_prompt.md ./ + +# Initial database (mount a named volume here for persistence across restarts) +COPY example.db . + +# PORT is read by the __main__ entry-point and by the CMD below. +# Override at runtime with: docker run -e PORT=9090 ... +ENV PORT=8080 + +EXPOSE 8080 + +ENTRYPOINT ["/entrypoint.sh"] + +# Production command: `fastapi run` wraps uvicorn without --reload. +CMD ["sh", "-c", "exec fastapi run app.py --host 0.0.0.0 --port ${PORT}"] diff --git a/app.py b/app.py index 77d161a..7ad3d16 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,11 @@ import json +import os +import secrets from contextlib import asynccontextmanager -from fastapi import FastAPI, Request, WebSocket +from dotenv import load_dotenv +from fastapi import Depends, FastAPI, HTTPException, Request, WebSocket, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlalchemy.ext.asyncio import create_async_engine @@ -9,6 +13,8 @@ from sqlalchemy.ext.asyncio import create_async_engine from chain import RagChain from db import DB +load_dotenv() + @asynccontextmanager async def lifespan(app: FastAPI): @@ -19,13 +25,27 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +security = HTTPBasic() templates = Jinja2Templates(directory="templates") app.mount("/static", StaticFiles(directory="static"), name="static") +def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): + correct_username = secrets.compare_digest(credentials.username, os.environ["AUTH_USER"]) + correct_password = secrets.compare_digest( + credentials.password, os.environ["PASSWORD"] + ) + if not (correct_username and correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized", + headers={"WWW-Authenticate": "Basic"}, + ) + + @app.get("/") -async def home(request: Request): +async def home(request: Request, _: None = Depends(verify_credentials)): db: DB = request.app.state.db prompts = await db.get_prompts() @@ -86,3 +106,9 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.send_json({"type": "end"}) except Exception: pass + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 8080))) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..520f4a8 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +# If the credentials JSON is provided as an env var, materialise it as a +# temporary file and point GOOGLE_APPLICATION_CREDENTIALS at it. +# This avoids baking sensitive credentials into the image. +# +# Usage: +# docker run -e GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat credentials.json)" ... +if [ -n "$GOOGLE_APPLICATION_CREDENTIALS_JSON" ]; then + CREDS_FILE="$(mktemp /tmp/gcloud-credentials-XXXXXX.json)" + printf '%s' "$GOOGLE_APPLICATION_CREDENTIALS_JSON" > "$CREDS_FILE" + export GOOGLE_APPLICATION_CREDENTIALS="$CREDS_FILE" +fi + +exec "$@"