Public Access
- 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
79 lines
2.6 KiB
Python
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"
|