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