diff --git a/controllers/BaseApiController.php b/controllers/BaseApiController.php index e0f2595e..215d184b 100644 --- a/controllers/BaseApiController.php +++ b/controllers/BaseApiController.php @@ -4,8 +4,25 @@ namespace Grocy\Controllers; class BaseApiController extends BaseController { - protected function ApiResponse($response) + + public function __construct(\Slim\Container $container) { - return json_encode($response); + parent::__construct($container); + $this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json')); + } + + protected $OpenApiSpec; + + protected function ApiResponse($data) + { + return json_encode($data); + } + + protected function VoidApiActionResponse($response, $success = true, $status = 200, $errorMessage = '') + { + return $response->withStatus($status)->withJson(array( + 'success' => $success, + 'error_message' => $errorMessage + )); } } diff --git a/controllers/BatteriesApiController.php b/controllers/BatteriesApiController.php index e504e01c..f8430b40 100644 --- a/controllers/BatteriesApiController.php +++ b/controllers/BatteriesApiController.php @@ -22,11 +22,26 @@ class BatteriesApiController extends BaseApiController $trackedTime = $request->getQueryParams()['tracked_time']; } - return $this->ApiResponse(array('success' => $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime))); + try + { + $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId'])); + try + { + return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId'])); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } } diff --git a/controllers/GenericEntityApiController.php b/controllers/GenericEntityApiController.php index c72fa370..b740b303 100644 --- a/controllers/GenericEntityApiController.php +++ b/controllers/GenericEntityApiController.php @@ -6,35 +6,75 @@ class GenericEntityApiController extends BaseApiController { public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->Database->{$args['entity']}()); + if ($this->IsValidEntity($args['entity'])) + { + return $this->ApiResponse($this->Database->{$args['entity']}()); + } + else + { + return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); + } } public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId'])); + if ($this->IsValidEntity($args['entity'])) + { + return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId'])); + } + else + { + return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); + } } public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody()); - $newRow->save(); - $success = $newRow->isClean(); - return $this->ApiResponse(array('success' => $success)); + if ($this->IsValidEntity($args['entity'])) + { + $newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody()); + $newRow->save(); + $success = $newRow->isClean(); + return $this->ApiResponse(array('success' => $success)); + } + else + { + return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); + } } public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $row = $this->Database->{$args['entity']}($args['objectId']); - $row->update($request->getParsedBody()); - $success = $row->isClean(); - return $this->ApiResponse(array('success' => $success)); + if ($this->IsValidEntity($args['entity'])) + { + $row = $this->Database->{$args['entity']}($args['objectId']); + $row->update($request->getParsedBody()); + $success = $row->isClean(); + return $this->ApiResponse(array('success' => $success)); + } + else + { + return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); + } } public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $row = $this->Database->{$args['entity']}($args['objectId']); - $row->delete(); - $success = $row->isClean(); - return $this->ApiResponse(array('success' => $success)); + if ($this->IsValidEntity($args['entity'])) + { + $row = $this->Database->{$args['entity']}($args['objectId']); + $row->delete(); + $success = $row->isClean(); + return $this->ApiResponse(array('success' => $success)); + } + else + { + return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); + } + } + + private function IsValidEntity($entity) + { + return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum); } } diff --git a/controllers/HabitsApiController.php b/controllers/HabitsApiController.php index 7fcb1718..6755fb3b 100644 --- a/controllers/HabitsApiController.php +++ b/controllers/HabitsApiController.php @@ -22,11 +22,26 @@ class HabitsApiController extends BaseApiController $trackedTime = $request->getQueryParams()['tracked_time']; } - return $this->ApiResponse(array('success' => $this->HabitsService->TrackHabit($args['habitId'], $trackedTime))); + try + { + $this->HabitsService->TrackHabit($args['habitId'], $trackedTime); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId'])); + try + { + return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId'])); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } } diff --git a/controllers/OpenApiController.php b/controllers/OpenApiController.php index 843fdc5e..2559f9fe 100644 --- a/controllers/OpenApiController.php +++ b/controllers/OpenApiController.php @@ -24,12 +24,11 @@ class OpenApiController extends BaseApiController { $applicationService = new ApplicationService(); - $specJson = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json')); - $specJson->info->version = $applicationService->GetInstalledVersion(); - $specJson->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $specJson->info->description); - $specJson->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api'); + $this->OpenApiSpec->info->version = $applicationService->GetInstalledVersion(); + $this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description); + $this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api'); - return $this->ApiResponse($specJson); + return $this->ApiResponse($this->OpenApiSpec); } public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index ecd8f14f..c8c9b3cb 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -16,7 +16,14 @@ class StockApiController extends BaseApiController public function ProductDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - return $this->ApiResponse($this->StockService->GetProductDetails($args['productId'])); + try + { + return $this->ApiResponse($this->StockService->GetProductDetails($args['productId'])); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) @@ -33,7 +40,15 @@ class StockApiController extends BaseApiController $transactionType = $request->getQueryParams()['transactiontype']; } - return $this->ApiResponse(array('success' => $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType))); + try + { + $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) @@ -50,7 +65,15 @@ class StockApiController extends BaseApiController $transactionType = $request->getQueryParams()['transactiontype']; } - return $this->ApiResponse(array('success' => $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType))); + try + { + $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function InventoryProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) @@ -61,7 +84,15 @@ class StockApiController extends BaseApiController $bestBeforeDate = $request->getQueryParams()['bestbeforedate']; } - return $this->ApiResponse(array('success' => $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate))); + try + { + $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } } public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) @@ -69,9 +100,9 @@ class StockApiController extends BaseApiController return $this->ApiResponse($this->StockService->GetCurrentStock()); } - public function AddmissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $this->StockService->AddMissingProductsToShoppingList(); - return $this->ApiResponse(array('success' => true)); + return $this->VoidApiActionResponse($response); } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 37b3e7b2..3ba7da56 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -76,6 +76,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -139,6 +149,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -203,6 +223,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -276,6 +306,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -316,6 +356,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -374,6 +424,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product, invalid transaction type)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -433,6 +493,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product, invalid transaction type)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -482,6 +552,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -513,6 +593,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -596,6 +686,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing habit)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -627,6 +727,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing habit)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -646,6 +756,15 @@ "schema": { "type": "integer" } + }, + { + "in": "query", + "name": "tracked_time", + "required": false, + "description": "The time of when the battery was charged, when omitted, the current time is used", + "schema": { + "type": "date-time" + } } ], "responses": { @@ -658,6 +777,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing battery)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -689,6 +818,16 @@ } } } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing battery)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } } } } @@ -1095,13 +1234,34 @@ } } }, + "ErrorExampleVoidApiActionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "error_message": { + "type": "string" + } + }, + "example": { + "success": false, + "error_message": "The error message..." + } + }, "VoidApiActionResponse": { "type": "object", "properties": { "success": { - "type": "boolean", - "default": true + "type": "boolean" + }, + "error_message": { + "type": "string" } + }, + "example": { + "success": true, + "error_message": "" } }, "CurrentStockResponse": { @@ -1120,6 +1280,14 @@ } } }, + "examples": { + "ErrorVoidApiActionResponseExample": { + "value": { + "success": false, + "error_message": "The error message..." + } + } + }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", diff --git a/services/BatteriesService.php b/services/BatteriesService.php index 7645a1b2..e8b4b3c0 100644 --- a/services/BatteriesService.php +++ b/services/BatteriesService.php @@ -12,6 +12,11 @@ class BatteriesService extends BaseService public function GetNextChargeTime(int $batteryId) { + if (!$this->BatteryExists($batteryId)) + { + throw new \Exception('Battery does not exist'); + } + $battery = $this->Database->batteries($batteryId); $batteryLastLogRow = $this->DatabaseService->ExecuteDbQuery("SELECT * from batteries_current WHERE battery_id = $batteryId LIMIT 1")->fetch(\PDO::FETCH_OBJ); @@ -29,6 +34,11 @@ class BatteriesService extends BaseService public function GetBatteryDetails(int $batteryId) { + if (!$this->BatteryExists($batteryId)) + { + throw new \Exception('Battery does not exist'); + } + $battery = $this->Database->batteries($batteryId); $batteryChargeCylcesCount = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->count(); $batteryLastChargedTime = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time'); @@ -42,6 +52,11 @@ class BatteriesService extends BaseService public function TrackChargeCycle(int $batteryId, string $trackedTime) { + if (!$this->BatteryExists($batteryId)) + { + throw new \Exception('Battery does not exist'); + } + $logRow = $this->Database->battery_charge_cycles()->createRow(array( 'battery_id' => $batteryId, 'tracked_time' => $trackedTime @@ -50,4 +65,10 @@ class BatteriesService extends BaseService return true; } + + private function BatteryExists($batteryId) + { + $batteryRow = $this->Database->batteries()->where('id = :1', $batteryId)->fetch(); + return $batteryRow !== null; + } } diff --git a/services/HabitsService.php b/services/HabitsService.php index 8d11dfc5..dff00615 100644 --- a/services/HabitsService.php +++ b/services/HabitsService.php @@ -15,6 +15,11 @@ class HabitsService extends BaseService public function GetNextHabitTime(int $habitId) { + if (!$this->HabitExists($habitId)) + { + throw new \Exception('Habit does not exist'); + } + $habit = $this->Database->habits($habitId); $habitLastLogRow = $this->DatabaseService->ExecuteDbQuery("SELECT * from habits_current WHERE habit_id = $habitId LIMIT 1")->fetch(\PDO::FETCH_OBJ); @@ -31,6 +36,11 @@ class HabitsService extends BaseService public function GetHabitDetails(int $habitId) { + if (!$this->HabitExists($habitId)) + { + throw new \Exception('Habit does not exist'); + } + $habit = $this->Database->habits($habitId); $habitTrackedCount = $this->Database->habits_log()->where('habit_id', $habitId)->count(); $habitLastTrackedTime = $this->Database->habits_log()->where('habit_id', $habitId)->max('tracked_time'); @@ -44,6 +54,11 @@ class HabitsService extends BaseService public function TrackHabit(int $habitId, string $trackedTime) { + if (!$this->HabitExists($habitId)) + { + throw new \Exception('Habit does not exist'); + } + $logRow = $this->Database->habits_log()->createRow(array( 'habit_id' => $habitId, 'tracked_time' => $trackedTime @@ -52,4 +67,10 @@ class HabitsService extends BaseService return true; } + + private function HabitExists($habitId) + { + $habitRow = $this->Database->habits()->where('id = :1', $habitId)->fetch(); + return $habitRow !== null; + } } diff --git a/services/StockService.php b/services/StockService.php index 1e727254..2a697f3b 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -22,6 +22,11 @@ class StockService extends BaseService public function GetProductDetails(int $productId) { + if (!$this->ProductExists($productId)) + { + throw new \Exception('Product does not exist'); + } + $product = $this->Database->products($productId); $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); $productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date'); @@ -41,6 +46,11 @@ class StockService extends BaseService public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType) { + if (!$this->ProductExists($productId)) + { + throw new \Exception('Product does not exist'); + } + if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) { $stockId = uniqid(); @@ -68,12 +78,17 @@ class StockService extends BaseService } else { - throw new Exception("Transaction type $transactionType is not valid (StockService.AddProduct)"); + throw new \Exception("Transaction type $transactionType is not valid (StockService.AddProduct)"); } } public function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType) { + if (!$this->ProductExists($productId)) + { + throw new \Exception('Product does not exist'); + } + if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) { $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); @@ -141,6 +156,11 @@ class StockService extends BaseService public function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate) { + if (!$this->ProductExists($productId)) + { + throw new \Exception('Product does not exist'); + } + $productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount'); if ($newAmount > $productStockAmount) @@ -182,4 +202,10 @@ class StockService extends BaseService } } } + + private function ProductExists($productId) + { + $productRow = $this->Database->products()->where('id = :1', $productId)->fetch(); + return $productRow !== null; + } } diff --git a/version.txt b/version.txt index f8e233b2..9ab8337f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.0 +1.9.1 diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index c3e55478..09e76168 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -261,9 +261,9 @@ - @stack('pageScripts') @stack('componentScripts') + @if(file_exists(__DIR__ . '/../../data/add_before_end_body.html')) @php include __DIR__ . '/../../data/add_before_end_body.html' @endphp