basic auth

This commit is contained in:
Matteo Rosati
2026-02-18 18:02:22 +01:00
parent 39eb4f4f01
commit a9e23d94b1
5 changed files with 139 additions and 2 deletions

37
.dockerignore Normal file
View File

@@ -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

3
.env.dist Normal file
View File

@@ -0,0 +1,3 @@
GOOGLE_APPLICATION_CREDENTIALS=credentials.json
AUTH_USER=
PASSWORD=

55
Dockerfile Normal file
View File

@@ -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}"]

30
app.py
View File

@@ -1,7 +1,11 @@
import json import json
import os
import secrets
from contextlib import asynccontextmanager 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.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.ext.asyncio import create_async_engine 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 chain import RagChain
from db import DB from db import DB
load_dotenv()
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -19,13 +25,27 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
security = HTTPBasic()
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static") 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("/") @app.get("/")
async def home(request: Request): async def home(request: Request, _: None = Depends(verify_credentials)):
db: DB = request.app.state.db db: DB = request.app.state.db
prompts = await db.get_prompts() prompts = await db.get_prompts()
@@ -86,3 +106,9 @@ async def websocket_endpoint(websocket: WebSocket):
await websocket.send_json({"type": "end"}) await websocket.send_json({"type": "end"})
except Exception: except Exception:
pass pass
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 8080)))

16
entrypoint.sh Normal file
View File

@@ -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 "$@"