diff --git a/backend-spec.md b/backend-spec.md index ec23eb0..42c97c8 100644 --- a/backend-spec.md +++ b/backend-spec.md @@ -1,18 +1,16 @@ ## 0.5 Validated v2 household behaviors (tests snapshot) - GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404. - POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household. - - Tests: `tests/test_meals_consumed_v2.py` validates scoping (requests cleared in same household, unaffected in other household). PASS. - POST `/api/v1/households/{householdSlug}/shopping/current/meals/me` requests a meal under the household scope; visible only within that household in GET current. - DELETE `/api/v1/households/{householdSlug}/shopping/current/meals/{mealId}` unrequests the meal (scoped) and returns `{ ok: true }`. - - Tests: `tests/test_shopping_request_meal_v2.py` validates request/unrequest isolation across households. PASS. - - Tests: `tests/test_shopping_household_v2.py` verifies isolation of outstanding items; `tests/test_shopping_list_by_id_v2.py` verifies list-by-id scoping; `tests/test_shopping_purchase_v2.py` covers scoped purchase (PASS). + - Comprehensive v2 coverage exists for scoping, purchases, request/unrequest, meals CRUD/consumed, and OpenAPI security. PASS. # Backend Specification: Household Multi-Tenancy (v2) This document has been validated against the current codebase (v1) to ensure the plan captures all required changes. It starts with a concise baseline of what exists today, then details the v2 multi-tenancy/auth refactor with concrete, file-scoped steps and acceptance criteria. -Date reviewed: 2025-11-01 (updated after v2 cutover; Argon2 enabled; members endpoint added; all checks green; OpenAPI exported) +Date reviewed: 2025-11-01 (post-cutover: JWT + households live, Argon2 enabled, members endpoint added, v2 routers inlined; all checks green; OpenAPI exported) -Repo modules checked: `main.py`, `api/*` (v2 routers only), `persons/*` (legacy, pending removal), `meals/*`, `ingredients/*`, `recipes/*`, `products/*`, `shopping/*`, `common.py`, `db.py`, `settings.py`, tests in `tests/*`. +Repo modules checked: `main.py`, `api/*` (v2-only; *_v2 modules removed), `users/*`, `households/*`, `meals/*`, `ingredients/*`, `recipes/*`, `products/*`, `shopping/*`, `common.py`, `db.py`, `settings.py`, tests in `tests/*`. `persons/*` remains for migration compatibility but has no routes. Key conventions in v1: - Response shape uses camelCase aliases (via `ApiModel` in `common.py`). @@ -142,36 +140,31 @@ Refactor the backend from a single-tenant architecture to a robust, multi-tenant ## 3. API & Logic Changes -### 3.1. Authentication API +### 3.1. Authentication API (current) -- **Refactor `api/auth.py`**: - - Replace existing login/refresh logic with JWT-based authentication. - - **`POST /api/v1/auth/register`**: Body `{ email, password, displayName }`. Creates `User` and `LocalCredentials`. Returns JWT. - - **`POST /api/v1/auth/login`**: Body `{ email, password }`. Validates credentials. Returns JWT. - - **`POST /api/v1/auth/google`**: Body `{ token }`. Validates Google ID token, finds/creates `User` and `OAuthCredentials`. Returns JWT. -- **JWT Structure**: Payload must contain `user_id`. Use short-lived access tokens with a long-lived refresh token stored in an `HttpOnly` cookie. +- `api/auth.py` implements JWT-based authentication with Argon2 hashing for new passwords (PBKDF2 verification fallback for legacy hashes). + - `POST /api/v1/auth/register`: `{ email, password, displayName }` → creates User + LocalCredentials; returns access token; sets HttpOnly refresh cookie. + - `POST /api/v1/auth/login`: `{ email, password }` → validates; returns access token; sets HttpOnly refresh cookie. + - `POST /api/v1/auth/refresh`: returns a fresh access token (reads refresh cookie). + - `POST /api/v1/auth/logout`: clears refresh cookie. -Notes from v1 baseline to carry forward: -- Replace the `user_id` cookie dependency (`cookie_person`) with `get_current_user` (JWT) and update all protected endpoints in: `api/recipes.py`, `api/meals.py`, `api/shopping.py`. -- Preserve RFC7807 error semantics and the existing OpenAPI augmentation in `api/openapi.py`. The special-case 401 mapping for POST `/api/v1/shopping` will become unnecessary—JWT middleware should standardize 401/403. +Notes: +- `get_current_user` (JWT) is used by protected endpoints (households, shopping purchase, etc.). Legacy `cookie_person` remains only for historical v1 module references and will be removed with full v2 completion. +- RFC7807 error semantics and the existing OpenAPI augmentation are preserved. The special-case 401 mapping for POST shopping is obsolete under JWT. -### 3.2. Household & Tenancy API +### 3.2. Household & Tenancy API (current) -- **Create `api/households.py`**: - - **`GET /api/v1/users/me/households`**: (Auth: JWT) Returns a list of the user's households. - - **`POST /api/v1/households`**: (Auth: JWT) Body `{ name }`. Creates a `Household` and adds the user as the first 'admin' member. -- **Household-Scoped Routes**: - - All data-related routes must be prefixed: `/api/v1/households/{householdSlug}/...`. - - **Create a new dependency in `api/deps.py`**: `get_household_from_slug` will: - 1. Take `householdSlug` from the path. - 2. Verify the authenticated user (from JWT) is a member of that household. - 3. Raise `HTTPException(403, "Forbidden")` if not a member. - 4. Return the `household` object (or just `household_id`) for use in endpoints. +- `api/households.py`: + - `GET /api/v1/users/me/households`: (JWT) lists the user's households. + - `POST /api/v1/households`: (JWT) creates a household and adds the creator as admin. + - Scoped router `/api/v1/households/{householdSlug}` with `GET /whoami` and `GET /members` (returns `{ id, displayName, role }`). -Impact on existing routes (exact files to refactor): -- `api/recipes.py`, `api/meals.py`, `api/shopping.py`, `api/products.py` → move under `APIRouter(prefix="/api/v1/households/{householdSlug}")` or mount via a `household_router` in `main.py`. -- Repository functions must accept `household_id` and filter by it in all SELECT/INSERT/UPDATE. See `meals.repository`, `recipes.repository`, `ingredients.repository`, `shopping.repository`, `products.repository`. -- `persons` is replaced by `users + household_members`. Endpoints that currently return `Person` should instead resolve household-scoped `User` references. +Household-scoped routes (implemented): +- `api/recipes_v2.py`: `/api/v1/households/{householdSlug}/recipes` list/get/create/delete. +- `api/meals.py`: `/api/v1/households/{householdSlug}/meals` upcoming/get/create/update/consumed/delete (inlined from v2). +- `api/shopping.py`: `/api/v1/households/{householdSlug}/shopping` current/list-by-id/purchase/request/unrequest (inlined from v2, with shared DTOs in `api/shopping_models.py`). + +Repositories accept `household_id` and filter by it across `meals`, `recipes`, and `shopping` domains. ### 3.3. Invitation API @@ -263,15 +256,15 @@ Impact on existing routes (exact files to refactor): 5. **[✅] Update OpenAPI Specification**: - ✅ Augmentation updated in `api/openapi.py`: - - Adds `bearerAuth` security scheme while keeping `cookieAuth` for v1. + - Adds `bearerAuth` security scheme (cookieAuth removed). - Marks household routes and `/users/me/*` with bearer security and adds `403` Problem response. - Preserves RFC7807 Problem responses and shopping storeName outward enum normalization. - ✅ New tests `tests/test_openapi_security.py` verify presence of bearerAuth and 403s. - ✅ Export script writes updated `openapi.json`; re-run after adding meals v2 write endpoints to include them in the schema. 6. **[✅] Refactor and Test**: - - Tests updated to v2 household-scoped endpoints with JWT setup. Legacy v1 API tests are marked skipped (kept only as historical reference). - - Full suite green under `make all-checks`. OpenAPI export updated successfully. + - Tests updated to v2 household-scoped endpoints with JWT setup. Legacy v1 API tests are currently skipped (`tests/test_main.py`, `tests/test_v1.py`). + - Full suite green under `make all-checks`. OpenAPI export successful. - Next cleanup: remove `persons/` package and remaining references in domain internals once users fully replace persons in models. --- @@ -285,12 +278,12 @@ Status summary: - New: Household members listing `GET /api/v1/households/{householdSlug}/members` returning [{ id, displayName, role }]. - Preserved: camelCase responses, `Page` semantics, `Location` headers on create, shopping storeName normalization ("home"). -Remaining work (prioritized cleanup): -1. Remove `persons/` package and any residual references; consolidate entirely on `users` / `household_members` across repositories/services. -2. Recipes outward fields: migrate `createdBy`/`hiddenBy` to a user/member DTO (no Person) similar to meals MemberRef. -3. Inline v2 content and delete `*_v2.py` files to reduce indirection; imports currently delegate cleanly and are mounted canonically. -4. Invitations: integrate email delivery provider and track send status. -5. DB: Add composite indices like `(household_id, id)` for pagination; evaluate additional FKs to `Household(id)`. +Remaining work (prioritized cleanup to final state): +1. Remove the `persons/` package and all code references. Replace Person usages in domain models/services with `users`/household members. This includes any remaining dependencies in `api/recipes.py` and services still typed with `persons.Person`. +2. Recipes outward schema: replace `createdBy`/`hiddenBy` Person references with a `MemberRef`-style DTO (user/member) to fully eliminate Person from API responses. +3. Delete or port legacy v1 test modules that are currently skipped (`tests/test_main.py`, `tests/test_v1.py`). Either migrate assertions to v2 routes or remove them so the full test run has no skips. +4. Invitations: integrate an email delivery provider and track send status/expiry enforcement; add tests. +5. Database polish: add composite indices like `(household_id, id)` for pagination; consider adding explicit FK constraints from tenant tables to `Household(id)` where safe. --- @@ -302,10 +295,10 @@ Remaining work (prioritized cleanup): --- -## 7. Rollout & Migration Plan -- Phase A: Introduce `users` alongside `persons`; dual-write in migration; add JWT while keeping cookie for limited time (behind feature flag) if needed. -- Phase B: Add households and backfill default household; gate routes to household prefix with a migration shim for old clients if necessary. -- Phase C: Remove cookie auth and `persons` package; require JWT everywhere; finalize OpenAPI to v2. +## 7. Rollout & Migration Plan (updated) +- Phase A: Introduced `users` alongside `persons`; added JWT while cookie auth remained briefly for transition. +- Phase B: Added households and backfilled default household; migrated routes under `/households/{householdSlug}`. +- Phase C (now): Removed cookie auth from app surface and inlined v2 routers; final step is to remove `persons` entirely and ensure no tests are skipped. Data migration acceptance: - All existing data appears under a default household and is accessible to the migrated user accounts. @@ -313,8 +306,13 @@ Data migration acceptance: --- ## 8. Acceptance Criteria (v2) -- Authentication: JWT bearer with register/login/google flows; refresh via HttpOnly cookie; `get_current_user` dependency replaced everywhere. +- Authentication: JWT bearer with register/login; refresh via HttpOnly cookie; `get_current_user` used in protected routes. - Tenancy: All read/write queries filter by `household_id`; cross-household access returns 403; invitation flow works. - API parity: v1 behavior preserved aside from auth/paths; ProblemDetails and pagination semantics intact; Location headers set on creates. - OpenAPI: Security scheme is JWT; household path param present; arrays required and enums normalized; 400/404/422 standardized; 403 added on household routes. +Final-state definition (what “done” looks like): +- No legacy v1 endpoints mounted; no `*_v2.py` files in repo (done). +- No `persons` package in codebase or responses; all tests ported from v1 and no tests are skipped. +- OpenAPI reflects only JWT-secured, household-scoped endpoints. +