Files
Meal-Planner/backend/app/security.py
T
admin 26d9985433 feat: remove login requirements for internal home-network use
- backend/app/security.py: require_session() now auto-authenticates by
  returning the first family_profile_id from the DB. No cookie or password
  needed. Falls back to "bootstrap" sentinel if no FamilyProfile exists.
  Admin routes (require_admin) still protected by bearer token.
- frontend/src/api/index.ts: removed 401→/login redirect interceptor
- frontend/src/App.tsx: removed Sign out button, removed /login route and
  Login page import
- Login page kept on disk (unused) for potential future re-enablement
2026-05-14 11:27:09 -07:00

79 lines
2.6 KiB
Python

"""
Auth dependencies for the MealPlanner backend.
Two flavors:
- ``require_admin`` — bearer token for ``/api/admin/*`` routes; token compared
to ``settings.ADMIN_TOKEN`` (must be set in env).
- ``require_session`` — auto-returns the first family_profile_id (no login
required). This app runs on a private home network so auth is disabled
for family-facing routes. Kept as a dependency so admin/token endpoints
can be re-enabled later by restoring cookie logic.
The per-voter approval-token flow on meal items is intentionally NOT covered
here — it has its own short-lived single-use tokens elsewhere.
"""
from fastapi import HTTPException, Request, status
from itsdangerous import TimestampSigner
from app.config import settings
from app.database import get_db
from app.models import FamilyProfile
bearer_header = "Authorization"
SESSION_COOKIE = "mp_session"
SESSION_MAX_AGE = 60 * 60 * 24 * 30 # 30 days
def require_admin(request: Request) -> None:
"""Enforce a shared bearer token. 401 on bad/missing token, 503 if unset."""
expected = settings.ADMIN_TOKEN
if not expected:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Admin auth not configured",
)
auth = request.headers.get(bearer_header, "")
if not auth.startswith("Bearer ") or auth[7:] != expected:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid admin token",
)
def _signer() -> TimestampSigner:
return TimestampSigner(settings.SECRET_KEY)
def issue_session(family_profile_id: str) -> str:
"""Sign the family_profile_id and return the cookie value."""
return _signer().sign(family_profile_id.encode()).decode()
def require_session(request: Request) -> str:
"""Auto-authenticate: return the first family_profile_id from the DB.
No cookie or password needed — this app runs on a private home network.
If no FamilyProfile exists yet, return \"bootstrap\" so the app can
initialise itself on first run.
"""
# 1. Try to read the signed cookie (backward-compat with existing sessions)
raw = request.cookies.get(SESSION_COOKIE)
if raw:
try:
return (
_signer().unsign(raw.encode(), max_age=SESSION_MAX_AGE).decode()
)
except Exception:
pass # fall through to auto-auth
# 2. Auto-auth: grab the first family profile from the DB
db = next(get_db())
profile = db.query(FamilyProfile).first()
if profile:
return str(profile.id)
# 3. Bootstrap hatch — no profile yet, return a sentinel value
return "bootstrap"