Validate all API request as the API is now open for third parties (references #5)

This commit is contained in:
Bernd Bestel 2018-04-22 14:25:08 +02:00
parent 538d789366
commit 4853174d03
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
12 changed files with 389 additions and 36 deletions

View File

@ -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
));
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1 +1 @@
1.9.0
1.9.1

View File

@ -261,9 +261,9 @@
<script src="{{ $U('/js/extensions.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/viewjs') }}/@yield('viewJsName').js?v={{ $version }}"></script>
@stack('pageScripts')
@stack('componentScripts')
<script src="{{ $U('/viewjs') }}/@yield('viewJsName').js?v={{ $version }}"></script>
@if(file_exists(__DIR__ . '/../../data/add_before_end_body.html'))
@php include __DIR__ . '/../../data/add_before_end_body.html' @endphp