Public Access
feat: phase r1+r2 recovery + r3-0 swiftly api ingestion
R1 stabilization: pytest harness with transactional db fixture, smoke + alembic + auth + scrape + approval + swiftly tests, github actions ci yaml. Bearer-token admin auth + signed-cookie session for family ui mutations. Async POST /api/admin/scrape (BackgroundTasks, returns 202). Path canonicalization (no /list, /planned suffixes). DATABASE_URL fail-fast on empty. R2 deferred-risk spikes: live lucky california fetch (R2-A), full email+per-voter approval click round trip with single-use enforcement (R2-B, console email backend, sendgrid stub). R3-0 phase 3 redesign: replaced playwright html scraper with requests based swiftly json api client. 17 categories, ~10k products per scrape, upsert by (source, external_id). 401 surfaces actionable token-refresh message via ScrapeLog.error_message. Pre-existing defects fixed: shopping_list.py syntax error blocking app import, MealPlan.votes orphan relationship, JSONB(astext=True) invalid kwarg, missing requests dep, calorie_target schema drift, every SQLEnum needed values_callable, 0001 had empty downgrade(), seed had duplicate ingredient rows. Migrations added: 0003 grocery_item.description, 0004 family_profile. calorie_target, 0005 grocery_item.external_id + source + composite index. Verified: 31/31 pytest green, alembic upgrade->downgrade->upgrade clean, frontend npm run build clean, live scrape 9,960 grocery_item rows in 36s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user