# R1-B+D — Auth + path canonicalization + DATABASE_URL fail-fast ## Files added - `backend/app/security.py` — `require_admin` (bearer token vs `settings.ADMIN_TOKEN`), `require_session` (signed-cookie via itsdangerous), `issue_session`, `SESSION_COOKIE`, `SESSION_MAX_AGE`. - `backend/app/api/auth.py` — `POST /api/auth/login` (shared `SESSION_PASSWORD`, sets httponly+secure+samesite=lax cookie), `POST /api/auth/logout` (clears cookie). Both return 204. - `backend/tests/test_auth.py` — admin bearer required, session cookie required for mutation, login/logout round-trip. - `backend/tests/test_config.py` — `Settings(_env_file=None, DATABASE_URL="")` raises `RuntimeError("DATABASE_URL is required")`. ## Files modified - `backend/app/config.py` — added `ADMIN_TOKEN`, `SESSION_PASSWORD`; `DATABASE_URL` no longer typed as required (default `""`) but a `model_validator(mode="after")` raises `RuntimeError` if blank — gives a clear error instead of pydantic's confusing ValidationError. - `backend/app/main.py` — wired `auth.router` at `/api/auth`. - `backend/app/api/admin.py` — `APIRouter(dependencies=[Depends(require_admin)])` so EVERY admin route is bearer-gated. - `backend/app/api/profile.py` — session-gated: `PUT /`, `POST /members`, `DELETE /members/{id}`. GETs open. Trailing slashes dropped. - `backend/app/api/recipes.py` — rewritten so `/ingredients` GET/POST come before `/{recipe_id}`. Session-gated: `POST /`, `DELETE /{id}`, `POST /ingredients`. GETs open. `GET /ingredients/list` renamed → `GET /ingredients`. Trailing slashes dropped. - `backend/app/api/meals.py` — `GET /planned` renamed → `GET ""`. Session-gated: `POST /`, `POST /{id}/lock`, `POST /items/{id}/swap`. Per-voter approval token routes (`GET/POST /items/{id}/vote/{token}`) UNCHANGED. - `backend/app/api/pantry.py` — session-gated: `POST /`, `PUT /{id}`, `DELETE /{id}`. GETs open. Trailing slashes dropped. - `backend/app/api/shopping_list.py` — trailing slash dropped on root GET. No mutations exist. - `backend/tests/test_smoke.py` — canonical paths enforced; 307 (slash redirect) is now a failure. - `backend/requirements.txt` — added `itsdangerous==2.1.2`. - `frontend/src/api/index.ts` — `withCredentials: true`, added `auth.login/logout`, `recipes.listIngredients` → `/recipes/ingredients`, `meals.getPlanned` → `/meals`. - `.env.example` — added `ADMIN_TOKEN`, `SESSION_PASSWORD`. ## Routes admin-gated (bearer token) All `/api/admin/*`: `POST /scrape`, `GET /logs`, `GET /logs/{id}`, `GET /email-logs`, `GET /meal-plans`, `POST /test-email`, `GET /stats`. ## Routes session-gated (cookie) - `PUT /api/profile`, `POST /api/profile/members`, `DELETE /api/profile/members/{id}` - `POST /api/recipes`, `DELETE /api/recipes/{id}`, `POST /api/recipes/ingredients` - `POST /api/meals`, `POST /api/meals/{id}/lock`, `POST /api/meals/items/{id}/swap` - `POST /api/pantry`, `PUT /api/pantry/{id}`, `DELETE /api/pantry/{id}` ## Paths renamed - `GET /api/recipes/ingredients/list` → `GET /api/recipes/ingredients` - `GET /api/meals/planned` → `GET /api/meals` - All routers: `@router.get("/")` → `@router.get("")` (no trailing slash on resource roots) ## Blockers - Could not execute `pytest` or `python -c "from app.main import app"` locally — Bash python execution denied (same as R1-A). Verification deferred to CI. Logical review of imports/wiring done. - TestClient runs http; the `secure=True` cookie won't auto-roundtrip. `test_session_login_logout` manually re-sets the cookie to validate the signing path. Production (https via nginx) is unaffected.