Files
admin 7838c49721
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled
feat(auth): harden sessions + HA Ingress support
- 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).
2026-06-30 16:11:33 -07:00

97 lines
3.8 KiB
Python

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)
logger = logging.getLogger(__name__)
app = FastAPI(
title="MealPlanner",
description="Self-hosted meal planning system",
version="0.1.0",
)
# Serve generated recipe images from local filesystem.
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"}
@app.get("/health/db")
def health_check_db(db: Session = Depends(get_db)):
try:
db.execute(text("SELECT 1"))
return {"status": "ok", "database": "connected"}
except Exception as e:
return {"status": "error", "database": "disconnected", "error": str(e)}
from app.api import profile, meals, shopping_list, pantry, admin, auth
from app.api import ingredients as ingredients_api
from app.api import recipe_search as recipe_search_api
from app.api import recipes as recipes_api
from app.api import never_suggest as never_suggest_api
from app.api import meal_plans as meal_plans_api
from app.api import feedback as feedback_api
from app.api import orchestrate as orchestrate_api
from app.api import llm_plan as llm_plan_api
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
app.include_router(meals.router, prefix="/api/meals", tags=["meals"])
app.include_router(shopping_list.router, prefix="/api/shopping-list", tags=["shopping-list"])
app.include_router(pantry.router, prefix="/api/pantry", tags=["pantry"])
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
app.include_router(orchestrate_api.router, prefix="/api/orchestrate", tags=["orchestrate"])
# Sprint 12: external recipe search (Spoonacular) + import.
# Mounted BEFORE recipes_api.public_router so /search and /import are
# not shadowed by the WIP's GET /{recipe_id} (which would 422 on a
# non-UUID "search" path segment). See sprint15-verification.md.
app.include_router(recipe_search_api.router, prefix="/api/recipes", tags=["recipes"])
app.include_router(ingredients_api.public_router)
app.include_router(ingredients_api.admin_router)
app.include_router(ingredients_api._match_admin_router)
app.include_router(recipes_api.public_router)
app.include_router(recipes_api.admin_router)
app.include_router(never_suggest_api.public_router)
app.include_router(never_suggest_api.admin_router)
app.include_router(meal_plans_api.admin_router)
app.include_router(meal_plans_api.public_router)
app.include_router(feedback_api.router, prefix="/api/feedback", tags=["feedback"])
# Sprint 13: F9-lite — free-text meal-plan synthesis via Ollama Cloud.
app.include_router(llm_plan_api.router, prefix="/api/llm", tags=["llm"])