feat(auth): harden sessions + HA Ingress support
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled

- backend: settings SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH,
  require_session uses secrets.compare_digest and respects trusted-network
  opt-in, main.py adds require_family_session middleware gating all /api/
  routes except auth/admin/email-vote-token paths
- docker-compose: pass SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH
  through to backend + scheduler (fixes env-file changes not reaching runtime)
- frontend: Ingress path-prefix support (APP_BASE_PATH, BrowserRouter basename,
  vite base './'), Login redirect honors APP_BASE_PATH
- nginx: no-cache headers on root + /assets/
- docs: Home Assistant Ingress install/troubleshooting + plan file
- tests: test_auth expects 401 on no-session GET

Defaults: SESSION_COOKIE_SECURE=false, TRUSTED_NETWORK_AUTO_AUTH=true
(HA is the auth boundary; MealPlanner must not be port-forwarded directly).
This commit is contained in:
2026-06-30 16:11:33 -07:00
parent 7f5757094e
commit 7838c49721
14 changed files with 148 additions and 38 deletions
+28 -1
View File
@@ -1,9 +1,11 @@
from fastapi import FastAPI, Depends
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from sqlalchemy import text
from app.database import get_db
from app.config import settings
from app.security import require_session
import logging
logging.basicConfig(level=settings.LOG_LEVEL)
@@ -19,6 +21,31 @@ app = FastAPI(
app.mount("/static", StaticFiles(directory="static"), name="static")
def _requires_session(path: str, method: str) -> bool:
if method == "OPTIONS" or not path.startswith("/api/"):
return False
if path.startswith("/api/auth/") or path.startswith("/api/admin/"):
return False
# Email approval links carry their own signed, single-use token.
if path.startswith("/api/meals/vote/"):
return False
return True
@app.middleware("http")
async def require_family_session(request: Request, call_next):
if _requires_session(request.url.path, request.method):
try:
require_session(request)
except HTTPException as exc:
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=getattr(exc, "headers", None),
)
return await call_next(request)
@app.get("/health")
def health_check(db: Session = Depends(get_db)):
return {"status": "ok"}