feat(v2): finalize scoped meals consumed + fix shopping v2 invariants; refresh JWT route; tests green
This commit is contained in:
parent
ec3199fb0c
commit
0502902ffe
5 changed files with 71 additions and 13 deletions
|
|
@ -44,7 +44,7 @@ async def login(
|
|||
|
||||
|
||||
@router.post(
|
||||
"/refresh",
|
||||
"/refresh-cookie",
|
||||
response_model=persons.Person,
|
||||
operation_id="refresh",
|
||||
summary="Refresh current user from cookie",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import aiosqlite
|
|||
from fastapi import APIRouter, Depends, Request, Response
|
||||
|
||||
import shopping
|
||||
from api.deps import error_response, get_db, get_household_from_slug
|
||||
from api.deps import error_response, get_db, get_household_from_slug, get_current_user
|
||||
from api.shopping import (
|
||||
CurrentShoppingList,
|
||||
PurchasedShoppingList,
|
||||
|
|
@ -105,6 +105,7 @@ async def purchase_ingredients_scoped(
|
|||
shopping_list: PurchaseListIn,
|
||||
request: Request,
|
||||
household=Depends(get_household_from_slug),
|
||||
user=Depends(get_current_user),
|
||||
conn: aiosqlite.Connection = Depends(get_db),
|
||||
) -> PurchasedShoppingList | Response:
|
||||
hid = household["id"]
|
||||
|
|
@ -123,7 +124,9 @@ async def purchase_ingredients_scoped(
|
|||
)
|
||||
)
|
||||
|
||||
domain_list = shopping.ShoppingList(items=domain_items, store_name=shopping_list.store_name)
|
||||
domain_list = shopping.ShoppingList(
|
||||
items=domain_items, store_name=shopping_list.store_name, purchased_by_id=user.id
|
||||
)
|
||||
|
||||
try:
|
||||
# Use household-scoped purchase which ensures requests belong to the same household
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
- 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.
|
||||
- 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 (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).
|
||||
# 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.
|
||||
|
|
@ -222,7 +222,7 @@ Impact on existing routes (exact files to refactor):
|
|||
- ✅ Refactor `main.py`:
|
||||
- Create a new `APIRouter` for household-scoped routes, e.g., `household_router = APIRouter(prefix="/api/v1/households/{householdSlug}")`.
|
||||
- Mounted a scoped helper endpoint and a new recipes v2 router under this prefix.
|
||||
- ✅ Recipes scoping:
|
||||
- ✅ Recipes scoping:
|
||||
- Added `api/recipes_v2.py` providing `/api/v1/households/{householdSlug}/recipes` with list/get/create.
|
||||
- Added scoped repo helpers in `recipes/repository.py` and exported via `recipes/__init__.py`.
|
||||
- Tests in `tests/test_recipes_household_v2.py` validate isolation across households (PASS).
|
||||
|
|
@ -237,7 +237,8 @@ Impact on existing routes (exact files to refactor):
|
|||
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
||||
- POST `/api/v1/households/{householdSlug}/shopping` to purchase list items scoped to household; validates invariants and updates outstanding requests.
|
||||
- Scoped helpers in `shopping/repository.py` and `shopping/__init__.py` filter by `household_id` (find items, load list, purchased ingredients, and purchase_scoped).
|
||||
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase). PASS.
|
||||
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase). PASS.
|
||||
- Notes: Normalized Ingredient.preparation to allow NULLs from DB (treated as empty string) to avoid 422 in v2 responses; set purchased_by_id in scoped purchase from JWT user.
|
||||
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/consumed/delete and shopping write flows (purchase, requests) with scoping.
|
||||
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
||||
- **Acceptance**: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403.
|
||||
|
|
|
|||
|
|
@ -46,3 +46,9 @@ class Ingredient(ApiModel):
|
|||
except ValueError:
|
||||
return v
|
||||
return v
|
||||
|
||||
# Backward compatibility: DB may contain NULL preparation; normalize to empty string
|
||||
@field_validator("preparation", mode="before")
|
||||
@classmethod
|
||||
def _normalize_preparation(cls, v: Any) -> Any:
|
||||
return "" if v is None else v
|
||||
|
|
|
|||
62
openapi.json
62
openapi.json
|
|
@ -1321,27 +1321,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/refresh": {
|
||||
"/api/v1/auth/refresh-cookie": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"v2",
|
||||
"auth-v2"
|
||||
"v1",
|
||||
"auth"
|
||||
],
|
||||
"summary": "Refresh current user from cookie",
|
||||
"operationId": "refresh",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"in": "cookie",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"title": "User Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "Refresh",
|
||||
"operationId": "refreshV2",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshResponse"
|
||||
"$ref": "#/components/schemas/Person"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/Problem422"
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
|
|
@ -1393,6 +1411,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/refresh": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"v2",
|
||||
"auth-v2"
|
||||
],
|
||||
"summary": "Refresh",
|
||||
"operationId": "refreshV2",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/components/responses/Problem422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/logout": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue