From 746a327d7de921f3e80b9817095e3b342971100d Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 15:03:27 +1100 Subject: [PATCH] fix(openapi): set explicit response_model for meals v2 get-by-id; plus security hardening and v2 endpoints --- api/meals_v2.py | 1 + openapi.json | 1204 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 1058 insertions(+), 147 deletions(-) diff --git a/api/meals_v2.py b/api/meals_v2.py index 27296ad..beee660 100644 --- a/api/meals_v2.py +++ b/api/meals_v2.py @@ -67,6 +67,7 @@ async def get_upcoming_meals_scoped( "/{meal_id}", operation_id="getMealV2", summary="Get a meal by id (scoped)", + response_model=meals.Meal, responses={404: {"model": ProblemDetails}}, ) async def get_meal_scoped( diff --git a/openapi.json b/openapi.json index ac6f711..3dc63dd 100644 --- a/openapi.json +++ b/openapi.json @@ -82,7 +82,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecipeOut" + "$ref": "#/components/schemas/api__recipes__RecipeOut" } } } @@ -224,7 +224,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_RecipeOut_" + "$ref": "#/components/schemas/common__Page_RecipeOut___1" }, "example": { "items": [ @@ -290,7 +290,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecipeOut" + "$ref": "#/components/schemas/api__recipes__RecipeOut" } } } @@ -341,7 +341,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RecipeOut" + "$ref": "#/components/schemas/api__recipes__RecipeOut" } } } @@ -1324,42 +1324,24 @@ "/api/v1/auth/refresh": { "post": { "tags": [ - "v1", - "auth" - ], - "summary": "Refresh current user from cookie", - "operationId": "refresh", - "parameters": [ - { - "name": "user_id", - "in": "cookie", - "required": true, - "schema": { - "type": "integer", - "title": "User Id" - } - } + "v2", + "auth-v2" ], + "summary": "Refresh", + "operationId": "refreshV2", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Person" + "$ref": "#/components/schemas/RefreshResponse" } } } }, "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } + "$ref": "#/components/responses/Problem422" } }, "security": [ @@ -1411,6 +1393,29 @@ } } }, + "/api/v1/auth/logout": { + "post": { + "tags": [ + "v2", + "auth-v2" + ], + "summary": "Logout", + "operationId": "logoutV2", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "$ref": "#/components/responses/Problem422" + } + } + } + }, "/api/v1/users/me/households": { "get": { "tags": [ @@ -1436,8 +1441,16 @@ }, "422": { "$ref": "#/components/responses/Problem422" + }, + "403": { + "$ref": "#/components/responses/Problem403" } - } + }, + "security": [ + { + "bearerAuth": [] + } + ] } }, "/api/v1/households": { @@ -1482,6 +1495,687 @@ } } }, + "/api/v1/invitations/accept": { + "post": { + "tags": [ + "v2", + "households" + ], + "summary": "Accept Invitation", + "operationId": "accept_invitation_api_v1_invitations_accept_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "Body" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/households/{householdSlug}/whoami": { + "get": { + "tags": [ + "v2" + ], + "summary": "Whoami", + "operationId": "whoami_api_v1_households__householdSlug__whoami_get", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WhoAmI" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/invitations": { + "post": { + "tags": [ + "v2" + ], + "summary": "Create Invitation", + "operationId": "create_invitation_api_v1_households__householdSlug__invitations_post", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInvitationBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/recipes": { + "get": { + "tags": [ + "v2", + "recipes-v2" + ], + "summary": "List Recipes", + "operationId": "list_recipes_api_v1_households__householdSlug__recipes_get", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + }, + { + "name": "q", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Q" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/common__Page_RecipeOut___2" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "v2", + "recipes-v2" + ], + "summary": "Create Recipe", + "operationId": "create_recipe_api_v1_households__householdSlug__recipes_post", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecipeCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api__recipes_v2__RecipeOut" + } + } + } + }, + "400": { + "$ref": "#/components/responses/Problem400" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/recipes/{recipe_id}": { + "get": { + "tags": [ + "v2", + "recipes-v2" + ], + "summary": "Get Recipe", + "operationId": "get_recipe_api_v1_households__householdSlug__recipes__recipe_id__get", + "parameters": [ + { + "name": "recipe_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Recipe Id" + } + }, + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api__recipes_v2__RecipeOut" + } + } + } + }, + "404": { + "$ref": "#/components/responses/Problem404" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/meals/upcoming": { + "get": { + "tags": [ + "v2", + "meals-v2" + ], + "summary": "List upcoming meals in a date range (scoped)", + "operationId": "getUpcomingMealsV2", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + }, + { + "name": "from", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time", + "title": "From" + } + }, + { + "name": "to", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time", + "title": "To" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Meal-Output" + }, + "title": "Response Getupcomingmealsv2" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/meals/{meal_id}": { + "get": { + "tags": [ + "v2", + "meals-v2" + ], + "summary": "Get a meal by id (scoped)", + "operationId": "getMealV2", + "parameters": [ + { + "name": "meal_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Meal Id" + } + }, + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Meal-Output" + } + } + } + }, + "404": { + "$ref": "#/components/responses/Problem404" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/shopping/current": { + "get": { + "tags": [ + "v2", + "shopping-v2" + ], + "summary": "Get the current aggregated shopping list (scoped)", + "operationId": "getCurrentShoppingListV2", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentShoppingList" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/shopping/{list_id}": { + "get": { + "tags": [ + "v2", + "shopping-v2" + ], + "summary": "Get a purchased shopping list by id (scoped)", + "operationId": "getShoppingListV2", + "parameters": [ + { + "name": "list_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "List Id" + } + }, + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PurchasedShoppingList" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/api/v1/households/{householdSlug}/shopping": { + "post": { + "tags": [ + "v2", + "shopping-v2" + ], + "summary": "Purchase ingredients for a shopping list (scoped)", + "operationId": "purchaseIngredientsV2", + "parameters": [ + { + "name": "householdSlug", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Householdslug" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PurchaseListIn" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PurchasedShoppingList" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "403": { + "$ref": "#/components/responses/Problem403" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/healthz": { "get": { "summary": "Healthz", @@ -1516,6 +2210,19 @@ ], "title": "CreateHouseholdBody" }, + "CreateInvitationBody": { + "properties": { + "email": { + "type": "string", + "title": "Email" + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "CreateInvitationBody" + }, "CurrentShoppingList": { "properties": { "outstandingItems": { @@ -1781,6 +2488,24 @@ ], "title": "IngredientPurchaseItemIn" }, + "InvitationResponse": { + "properties": { + "token": { + "type": "string", + "title": "Token" + }, + "status": { + "type": "string", + "title": "Status", + "default": "pending" + } + }, + "type": "object", + "required": [ + "token" + ], + "title": "InvitationResponse" + }, "ListIngredientItem": { "properties": { "kind": { @@ -2231,51 +2956,6 @@ ], "title": "Page[Person]" }, - "Page_RecipeOut_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/RecipeOut" - }, - "type": "array", - "minItems": 0, - "title": "Items" - }, - "nextCursor": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Nextcursor" - }, - "prevCursor": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Prevcursor" - }, - "total": { - "type": "integer", - "title": "Total", - "description": "Total count", - "default": 0 - } - }, - "type": "object", - "required": [ - "items" - ], - "title": "Page[RecipeOut]" - }, "Person": { "properties": { "id": { @@ -2691,13 +3371,8 @@ ], "title": "Recipe" }, - "RecipeOut": { + "RecipeCreate": { "properties": { - "id": { - "type": "integer", - "title": "Id", - "default": -1 - }, "name": { "type": "string", "title": "Name" @@ -2725,69 +3400,6 @@ "type": "array", "minItems": 0, "title": "Ingredients" - }, - "basedOnRecipe": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Basedonrecipe" - }, - "dateCreated": { - "type": "string", - "format": "date-time", - "title": "Datecreated" - }, - "createdById": { - "type": "integer", - "title": "Createdbyid" - }, - "createdBy": { - "anyOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ] - }, - "dateHidden": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Datehidden" - }, - "hiddenById": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Hiddenbyid" - }, - "hiddenBy": { - "anyOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ] } }, "type": "object", @@ -2796,11 +3408,27 @@ "link", "serves", "imageUrls", - "ingredients", - "dateCreated", - "createdById" + "ingredients" ], - "title": "RecipeOut" + "title": "RecipeCreate" + }, + "RefreshResponse": { + "properties": { + "accessToken": { + "type": "string", + "title": "Accesstoken" + }, + "tokenType": { + "type": "string", + "title": "Tokentype", + "default": "bearer" + } + }, + "type": "object", + "required": [ + "accessToken" + ], + "title": "RefreshResponse" }, "RegisterBody": { "properties": { @@ -3014,6 +3642,24 @@ ], "title": "ValidationError" }, + "WhoAmI": { + "properties": { + "householdId": { + "type": "integer", + "title": "Householdid" + }, + "householdSlug": { + "type": "string", + "title": "Householdslug" + } + }, + "type": "object", + "required": [ + "householdId", + "householdSlug" + ], + "title": "WhoAmI" + }, "api__auth__LoginBody": { "properties": { "username": { @@ -3045,6 +3691,253 @@ ], "title": "LoginBody" }, + "api__recipes__RecipeOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "default": -1 + }, + "name": { + "type": "string", + "title": "Name" + }, + "link": { + "type": "string", + "title": "Link" + }, + "serves": { + "type": "integer", + "title": "Serves" + }, + "imageUrls": { + "items": { + "type": "string" + }, + "type": "array", + "minItems": 0, + "title": "Imageurls" + }, + "ingredients": { + "items": { + "$ref": "#/components/schemas/Ingredient" + }, + "type": "array", + "minItems": 0, + "title": "Ingredients" + }, + "basedOnRecipe": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Basedonrecipe" + }, + "dateCreated": { + "type": "string", + "format": "date-time", + "title": "Datecreated" + }, + "createdById": { + "type": "integer", + "title": "Createdbyid" + }, + "createdBy": { + "anyOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "type": "null" + } + ] + }, + "dateHidden": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Datehidden" + }, + "hiddenById": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Hiddenbyid" + }, + "hiddenBy": { + "anyOf": [ + { + "$ref": "#/components/schemas/Person" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "name", + "link", + "serves", + "imageUrls", + "ingredients", + "dateCreated", + "createdById" + ], + "title": "RecipeOut" + }, + "api__recipes_v2__RecipeOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "default": -1 + }, + "name": { + "type": "string", + "title": "Name" + }, + "link": { + "type": "string", + "title": "Link" + }, + "serves": { + "type": "integer", + "title": "Serves" + }, + "imageUrls": { + "items": { + "type": "string" + }, + "type": "array", + "minItems": 0, + "title": "Imageurls" + }, + "ingredients": { + "items": { + "$ref": "#/components/schemas/Ingredient" + }, + "type": "array", + "minItems": 0, + "title": "Ingredients" + } + }, + "type": "object", + "required": [ + "name", + "link", + "serves", + "imageUrls", + "ingredients" + ], + "title": "RecipeOut" + }, + "common__Page_RecipeOut___1": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/api__recipes__RecipeOut" + }, + "type": "array", + "minItems": 0, + "title": "Items" + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Nextcursor" + }, + "prevCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prevcursor" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total count", + "default": 0 + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "Page[RecipeOut]" + }, + "common__Page_RecipeOut___2": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/api__recipes_v2__RecipeOut" + }, + "type": "array", + "minItems": 0, + "title": "Items" + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Nextcursor" + }, + "prevCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prevcursor" + }, + "total": { + "type": "integer", + "title": "Total", + "description": "Total count", + "default": 0 + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "Page[RecipeOut]" + }, "StoreNameOut": { "type": "string", "enum": [ @@ -3092,6 +3985,17 @@ } } } + }, + "Problem403": { + "description": "Forbidden", + "content": { + "application/problem+json": {}, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } } }, "securitySchemes": { @@ -3100,6 +4004,12 @@ "in": "cookie", "name": "user_id", "description": "Authentication via user_id cookie (session-style)." + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT access token in Authorization header" } } }