diff --git a/app.php b/app.php
index d2310bd0..5a3b4fb2 100644
--- a/app.php
+++ b/app.php
@@ -26,6 +26,10 @@ $appContainer = new \Slim\Container([
'UrlManager' => function($container)
{
return new UrlManager(BASE_URL);
+ },
+ 'ApiKeyHeaderName' => function($container)
+ {
+ return 'GROCY-API-KEY';
}
]);
$app = new \Slim\App($appContainer);
diff --git a/bower.json b/bower.json
index 4c0df17d..69fbacb7 100644
--- a/bower.json
+++ b/bower.json
@@ -18,6 +18,7 @@
"toastr": "^2.1.3",
"tagmanager": "^3.0.2",
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
- "swagger-ui": "^3.13.4"
+ "swagger-ui": "^3.13.4",
+ "jquery-ui": "^1.12.1"
}
}
diff --git a/controllers/OpenApiController.php b/controllers/OpenApiController.php
index 4e6942de..843fdc5e 100644
--- a/controllers/OpenApiController.php
+++ b/controllers/OpenApiController.php
@@ -3,21 +3,46 @@
namespace Grocy\Controllers;
use \Grocy\Services\ApplicationService;
+use \Grocy\Services\ApiKeyService;
class OpenApiController extends BaseApiController
{
+ public function __construct(\Slim\Container $container)
+ {
+ parent::__construct($container);
+ $this->ApiKeyService = new ApiKeyService();
+ }
+
+ protected $ApiKeyService;
+
public function DocumentationUi(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
- return $this->AppContainer->view->render($response, 'apidoc');
+ return $this->AppContainer->view->render($response, 'openapiui');
}
public function DocumentationSpec(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$applicationService = new ApplicationService();
- $specJson = json_decode(file_get_contents(__DIR__ . '/../helpers/grocy.openapi.json'));
+ $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');
return $this->ApiResponse($specJson);
}
+
+ public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
+ {
+ return $this->AppContainer->view->render($response, 'manageapikeys', [
+ 'apiKeys' => $this->Database->api_keys()
+ ]);
+ }
+
+ public function CreateNewApiKey(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
+ {
+ $newApiKey = $this->ApiKeyService->CreateApiKey();
+ $newApiKeyId = $this->ApiKeyService->GetApiKeyId($newApiKey);
+ return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId"));
+ }
}
diff --git a/grocy.openapi.json b/grocy.openapi.json
new file mode 100644
index 00000000..b6de56b2
--- /dev/null
+++ b/grocy.openapi.json
@@ -0,0 +1,1137 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "grocy REST API",
+ "description": "Authentication is done via API keys (header *GROCY-API-KEY*), which you can manage [here](PlaceHolderManageApiKeysUrl).
Additionally requests from within the frontend are also valid (via session cookie).",
+ "version": "xxx",
+ "contact": {
+ "email": "bernd@berrnd.de"
+ },
+ "license": {
+ "name": "grocy.info",
+ "url": "https://grocy.info"
+ }
+ },
+ "servers": [
+ {
+ "url": "xxx"
+ }
+ ],
+ "tags": [
+ {
+ "name": "Generic entity interactions",
+ "description": "A limited set of entities are directly exposed for convenience"
+ }
+ ],
+ "paths": {
+ "/get-objects/{entity}": {
+ "get": {
+ "description": "Returns all objects of the given entity",
+ "tags": [
+ "Generic entity interactions"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "entity",
+ "required": true,
+ "description": "A valid entity name",
+ "schema": {
+ "$ref": "#/components/internalSchemas/Entity"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "An entity object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Product"
+ },
+ {
+ "$ref": "#/components/schemas/Habit"
+ },
+ {
+ "$ref": "#/components/schemas/Battery"
+ },
+ {
+ "$ref": "#/components/schemas/Location"
+ },
+ {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ {
+ "$ref": "#/components/schemas/ShoppingListItem"
+ },
+ {
+ "$ref": "#/components/schemas/StockEntry"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/get-object/{entity}/{objectId}": {
+ "get": {
+ "description": "Returns a single object of the given entity",
+ "tags": [
+ "Generic entity interactions"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "entity",
+ "required": true,
+ "description": "A valid entity name",
+ "schema": {
+ "$ref": "#/components/internalSchemas/Entity"
+ }
+ },
+ {
+ "in": "path",
+ "name": "objectId",
+ "required": true,
+ "description": "A valid object id of the given entity",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "An entity object",
+ "content": {
+ "application/json": {
+ "schema":{
+ "type": "object",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Product"
+ },
+ {
+ "$ref": "#/components/schemas/Habit"
+ },
+ {
+ "$ref": "#/components/schemas/Battery"
+ },
+ {
+ "$ref": "#/components/schemas/Location"
+ },
+ {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ {
+ "$ref": "#/components/schemas/ShoppingListItem"
+ },
+ {
+ "$ref": "#/components/schemas/StockEntry"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/add-object/{entity}": {
+ "post": {
+ "description": "Adds a single object of the given entity",
+ "tags": [
+ "Generic entity interactions"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "entity",
+ "required": true,
+ "description": "A valid entity name",
+ "schema": {
+ "$ref": "#/components/internalSchemas/Entity"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "A valid entity object of entity specified in parameter *entity*",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Product"
+ },
+ {
+ "$ref": "#/components/schemas/Habit"
+ },
+ {
+ "$ref": "#/components/schemas/Battery"
+ },
+ {
+ "$ref": "#/components/schemas/Location"
+ },
+ {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ {
+ "$ref": "#/components/schemas/ShoppingListItem"
+ },
+ {
+ "$ref": "#/components/schemas/StockEntry"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/edit-object/{entity}/{objectId}": {
+ "post": {
+ "description": "Edits the given object of the given entity",
+ "tags": [
+ "Generic entity interactions"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "entity",
+ "required": true,
+ "description": "A valid entity name",
+ "schema": {
+ "$ref": "#/components/internalSchemas/Entity"
+ }
+ },
+ {
+ "in": "path",
+ "name": "objectId",
+ "required": true,
+ "description": "A valid object id of the given entity",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "A valid entity object of entity specified in parameter *entity*",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/Product"
+ },
+ {
+ "$ref": "#/components/schemas/Habit"
+ },
+ {
+ "$ref": "#/components/schemas/Battery"
+ },
+ {
+ "$ref": "#/components/schemas/Location"
+ },
+ {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ {
+ "$ref": "#/components/schemas/ShoppingListItem"
+ },
+ {
+ "$ref": "#/components/schemas/StockEntry"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/delete-object/{entity}/{objectId}": {
+ "get": {
+ "description": "Deletes a single object of the given entity",
+ "tags": [
+ "Generic entity interactions"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "entity",
+ "required": true,
+ "description": "A valid entity name",
+ "schema": {
+ "$ref": "#/components/internalSchemas/Entity"
+ }
+ },
+ {
+ "in": "path",
+ "name": "objectId",
+ "required": true,
+ "description": "A valid object id of the given entity",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/add-product/{productId}/{amount}": {
+ "get": {
+ "description": "Adds the the given amount of the given product to stock",
+ "tags": [
+ "Stock"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "productId",
+ "required": true,
+ "description": "A valid product id",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "path",
+ "name": "amount",
+ "required": true,
+ "description": "The amount to add",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "bestbeforedate",
+ "required": false,
+ "description": "The best before date of the product to add, when omitted, the current date is used",
+ "schema": {
+ "type": "date"
+ }
+ },
+ {
+ "in": "query",
+ "name": "transactiontype",
+ "required": false,
+ "description": "The transaction type for this transaction, when omitted, *purchase* is used",
+ "schema": {
+ "$ref": "#/components/internalSchemas/StockTransactionType"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/consume-product/{productId}/{amount}": {
+ "get": {
+ "description": "Removes the the given amount of the given product from stock",
+ "tags": [
+ "Stock"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "productId",
+ "required": true,
+ "description": "A valid product id",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "path",
+ "name": "amount",
+ "required": false,
+ "description": "The amount to remove",
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ {
+ "in": "query",
+ "name": "spoiled",
+ "required": true,
+ "description": "True when the given product was spoiled, defaults to false",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "transactiontype",
+ "required": false,
+ "description": "The transaction type for this transaction, when omitted, *consume* is used",
+ "schema": {
+ "$ref": "#/components/internalSchemas/StockTransactionType"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/inventory-product/{productId}/{newAmount}": {
+ "get": {
+ "description": "Inventories the the given product (adds/removes based on the given new current amount)",
+ "tags": [
+ "Stock"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "productId",
+ "required": true,
+ "description": "A valid product id",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "path",
+ "name": "newAmount",
+ "required": true,
+ "description": "The new current amount for the given product",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "bestbeforedate",
+ "required": false,
+ "description": "The best before date which applies to added products",
+ "schema": {
+ "type": "date"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/get-product-details/{productId}": {
+ "get": {
+ "description": "Returns details of the given product",
+ "tags": [
+ "Stock"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "productId",
+ "required": true,
+ "description": "A valid product id",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A ProductDetailsResponse object",
+ "content": {
+ "application/json": {
+ "schema":{
+ "$ref": "#/components/schemas/ProductDetailsResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/get-current-stock": {
+ "get": {
+ "description": "Returns all products which are currently in stock incl. the next expiring date per product",
+ "tags": [
+ "Stock"
+ ],
+ "responses": {
+ "200": {
+ "description": "An array of CurrentStockResponse objects",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Product"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/stock/add-missing-products-to-shoppinglist": {
+ "get": {
+ "description": "Adds currently missing products (below defined min. stock amount) to the shopping list",
+ "tags": [
+ "Stock"
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/habits/track-habit-execution/{habitId}": {
+ "get": {
+ "description": "Tracks an execution of the given habit",
+ "tags": [
+ "Habits"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "habitId",
+ "required": true,
+ "description": "A valid habit id",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "tracked_time",
+ "required": false,
+ "description": "The time of when the habit was executed, when omitted, the current time is used",
+ "schema": {
+ "type": "date-time"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/habits/get-habit-details/{habitId}": {
+ "get": {
+ "description": "Returns details of the given habit",
+ "tags": [
+ "Habits"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "habitId",
+ "required": true,
+ "description": "A valid habit id",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A HabitDetailsResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HabitDetailsResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/batteries/track-charge-cycle/{batteryId}": {
+ "get": {
+ "description": "Tracks a charge cycle of the given battery",
+ "tags": [
+ "Batteries"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "batteryId",
+ "required": true,
+ "description": "A valid battery id",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A VoidApiActionResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VoidApiActionResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/batteries/get-battery-details/{batteryId}": {
+ "get": {
+ "description": "Returns details of the given battery",
+ "tags": [
+ "Batteries"
+ ],
+ "parameters": [
+ {
+ "in": "path",
+ "name": "batteryId",
+ "required": true,
+ "description": "A valid battery id",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A BatteryDetailsResponse object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BatteryDetailsResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "internalSchemas": {
+ "Entity": {
+ "type": "string",
+ "enum": [
+ "products",
+ "habits",
+ "batteries",
+ "locations",
+ "quantity_units",
+ "shopping_list",
+ "stock"
+ ]
+ },
+ "StockTransactionType": {
+ "type": "string",
+ "enum": [
+ "purchase",
+ "consume",
+ "inventory-correction"
+ ]
+ }
+ },
+ "schemas": {
+ "Product": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "location_id": {
+ "type": "integer"
+ },
+ "qu_id_purchase": {
+ "type": "integer"
+ },
+ "qu_id_stock": {
+ "type": "integer"
+ },
+ "qu_factor_purchase_to_stock": {
+ "type": "number",
+ "format": "double"
+ },
+ "barcode": {
+ "type": "string",
+ "description": "Can contain multiple barcodes separated by comma"
+ },
+ "min_stock_amount": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "default_best_before_days": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "QuantityUnit": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "Location": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "StockEntry": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "product_id": {
+ "type": "integer"
+ },
+ "amount": {
+ "type": "integer"
+ },
+ "best_before_date": {
+ "type": "string",
+ "format": "date"
+ },
+ "purchased_date": {
+ "type": "string",
+ "format": "date"
+ },
+ "stock_id": {
+ "type": "string",
+ "description": "A unique id which references this stock entry during its lifetime"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "ProductDetailsResponse": {
+ "type": "object",
+ "properties": {
+ "product": {
+ "$ref": "#/components/schemas/Product"
+ },
+ "quantity_unit_purchase": {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ "quantity_unit_stock": {
+ "$ref": "#/components/schemas/QuantityUnit"
+ },
+ "last_purchased": {
+ "type": "string",
+ "format": "date"
+ },
+ "last_used": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "stock_amount": {
+ "type": "integer"
+ }
+ }
+ },
+ "HabitDetailsResponse": {
+ "type": "object",
+ "properties": {
+ "habit": {
+ "$ref": "#/components/schemas/Habit"
+ },
+ "last_tracked": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When this habit was last tracked"
+ },
+ "track_count": {
+ "type": "integer",
+ "description": "How often this habit was tracked so far"
+ }
+ }
+ },
+ "BatteryDetailsResponse": {
+ "type": "object",
+ "properties": {
+ "habit": {
+ "$ref": "#/components/schemas/Battery"
+ },
+ "last_charged": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When this battery was last charged"
+ },
+ "charge_cycles_count": {
+ "type": "integer",
+ "description": "How often this battery was charged so far"
+ }
+ }
+ },
+ "Session": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "session_key": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "last_used": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "ApiKey": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "api_key": {
+ "type": "string"
+ },
+ "expires": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "last_used": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "ShoppingListItem": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "product_id": {
+ "type": "integer"
+ },
+ "note": {
+ "type": "string"
+ },
+ "amount": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0,
+ "description": "The manual entered amount"
+ },
+ "amount_autoadded": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0,
+ "description": "The automatically added amount based on defined minimum stock amounts"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "Battery": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "used_in": {
+ "type": "string"
+ },
+ "charge_interval_days": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "BatteryChargeCycle": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "battery_id": {
+ "type": "integer"
+ },
+ "tracked_time": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "Habit": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "period_type": {
+ "type": "string",
+ "enum": [
+ "manually",
+ "dynamic-regular"
+ ]
+ },
+ "period_days": {
+ "type": "integer"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "HabitLogEntry": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "habit_id": {
+ "type": "integer"
+ },
+ "tracked_time": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "StockLogEntry": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "product_id": {
+ "type": "integer"
+ },
+ "amount": {
+ "type": "integer"
+ },
+ "best_before_date": {
+ "type": "string",
+ "format": "date"
+ },
+ "purchased_date": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "used_date": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "spoiled": {
+ "type": "boolean",
+ "default": false
+ },
+ "stock_id": {
+ "type": "string"
+ },
+ "transaction_type": {
+ "$ref": "#/components/internalSchemas/StockTransactionType"
+ },
+ "row_created_timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "VoidApiActionResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "CurrentStockResponse": {
+ "type": "object",
+ "properties": {
+ "product_id": {
+ "type": "integer"
+ },
+ "amount": {
+ "type": "integer"
+ },
+ "best_before_date": {
+ "type": "date",
+ "description": "The next best before date for this product"
+ }
+ }
+ }
+ },
+ "securitySchemes": {
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "GROCY-API-KEY"
+ }
+ }
+ },
+ "security": [
+ {
+ "ApiKeyAuth": [ ]
+ }
+ ]
+}
diff --git a/helpers/UrlManager.php b/helpers/UrlManager.php
index dd6886a2..f5bb271e 100644
--- a/helpers/UrlManager.php
+++ b/helpers/UrlManager.php
@@ -4,8 +4,16 @@ namespace Grocy\Helpers;
class UrlManager
{
- public function __construct(string $basePath) {
- $this->BasePath = $basePath;
+ public function __construct(string $basePath)
+ {
+ if ($basePath === '/')
+ {
+ $this->BasePath = $this->GetBaseUrl();
+ }
+ else
+ {
+ $this->BasePath = $basePath;
+ }
}
protected $BasePath;
@@ -14,4 +22,9 @@ class UrlManager
{
return rtrim($this->BasePath, '/') . $relativePath;
}
+
+ private function GetBaseUrl()
+ {
+ return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
+ }
}
diff --git a/helpers/grocy.openapi.json b/helpers/grocy.openapi.json
deleted file mode 100644
index c10b8824..00000000
--- a/helpers/grocy.openapi.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "openapi": "3.0.0",
- "info": {
- "title": "grocy REST API",
- "description": "xxx",
- "version": "xxx"
- },
- "servers": [
- {
- "url": "xxx"
- }
- ],
- "paths": {
- "/get-objects/{entity}": {
- "get": {
- "description": "Returns all objects of the given entity",
- "parameters": [
- {
- "in": "path",
- "name": "entity",
- "required": true,
- "description": "A valid entity name",
- "schema": {
- "$ref": "#/components/schemas/Entity"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- }
- }
- }
- }
- },
- "components": {
- "schemas": {
- "Entity": {
- "type": "string",
- "enum": [
- "product",
- "habit"
- ]
- }
- }
- }
-}
diff --git a/localization/de.php b/localization/de.php
index dbdc09d1..5ea498f2 100644
--- a/localization/de.php
+++ b/localization/de.php
@@ -101,6 +101,17 @@ return array(
'Are you sure to delete quantity unit "#1"?' => 'Mengeneinheit "#1" wirklich löschen?',
'Are you sure to delete product "#1"?' => 'Produkt "#1" wirklich löschen?',
'Are you sure to delete location "#1"?' => 'Standort "#1" wirklich löschen?',
+ 'Manage API keys' => 'API-Keys verwalten',
+ 'REST API & data model documentation' => 'REST-API & Datenmodell Dokumentation',
+ 'API keys' => 'API-Keys',
+ 'Create new API key' => 'Neuen API-Key erstellen',
+ 'API key' => 'API-Key',
+ 'Expires' => 'Läuft ab',
+ 'Created' => 'Erstellt',
+ 'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig',
+ 'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
+ 'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
+ 'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird',
//Constants
'manually' => 'Manuell',
diff --git a/middleware/ApiKeyAuthMiddleware.php b/middleware/ApiKeyAuthMiddleware.php
new file mode 100644
index 00000000..3e28dfea
--- /dev/null
+++ b/middleware/ApiKeyAuthMiddleware.php
@@ -0,0 +1,58 @@
+SessionCookieName = $sessionCookieName;
+ $this->ApiKeyHeaderName = $apiKeyHeaderName;
+ }
+
+ protected $SessionCookieName;
+ protected $ApiKeyHeaderName;
+
+ public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
+ {
+ $route = $request->getAttribute('route');
+ $routeName = $route->getName();
+
+ if ($this->ApplicationService->IsDemoInstallation())
+ {
+ $response = $next($request, $response);
+ }
+ else
+ {
+ $validSession = true;
+ $validApiKey = true;
+
+ $sessionService = new SessionService();
+ if (!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName]))
+ {
+ $validSession = false;
+ }
+
+ $apiKeyService = new ApiKeyService();
+ if (!$request->hasHeader($this->ApiKeyHeaderName) || !$apiKeyService->IsValidApiKey($request->getHeaderLine($this->ApiKeyHeaderName)))
+ {
+ $validApiKey = false;
+ }
+
+ if (!$validSession && !$validApiKey)
+ {
+ $response = $response->withStatus(401);
+ }
+ else
+ {
+ $response = $next($request, $response);
+ }
+ }
+
+ return $response;
+ }
+}
diff --git a/migrations/0022.sql b/migrations/0022.sql
new file mode 100644
index 00000000..2629ec83
--- /dev/null
+++ b/migrations/0022.sql
@@ -0,0 +1,7 @@
+CREATE TABLE api_keys (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
+ api_key TEXT NOT NULL UNIQUE,
+ expires DATETIME,
+ last_used DATETIME,
+ row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
+)
diff --git a/migrations/0023.sql b/migrations/0023.sql
new file mode 100644
index 00000000..6479ee13
--- /dev/null
+++ b/migrations/0023.sql
@@ -0,0 +1 @@
+DELETE FROM sessions
diff --git a/migrations/0024.sql b/migrations/0024.sql
new file mode 100644
index 00000000..ed815485
--- /dev/null
+++ b/migrations/0024.sql
@@ -0,0 +1,2 @@
+ALTER TABLE sessions
+ADD COLUMN last_used DATETIME
diff --git a/public/css/grocy.css b/public/css/grocy.css
index a2b4a30d..a393caad 100644
--- a/public/css/grocy.css
+++ b/public/css/grocy.css
@@ -188,3 +188,12 @@ a.discrete-link:focus {
#toast-container > div {
box-shadow: none;
}
+
+.navbar-default .navbar-nav > .open > a {
+ background-color: #d6d6d6 !important;
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+ background-color: #e5e5e5 !important;
+}
diff --git a/public/viewjs/apidoc.js b/public/viewjs/apidoc.js
deleted file mode 100644
index f8497e60..00000000
--- a/public/viewjs/apidoc.js
+++ /dev/null
@@ -1,18 +0,0 @@
-$(function ()
-{
- const swaggerUi = SwaggerUIBundle({
- url: U('/api/get-open-api-specification'),
- dom_id: '#swagger-ui',
- deepLinking: true,
- presets: [
- SwaggerUIBundle.presets.apis,
- SwaggerUIStandalonePreset
- ],
- plugins: [
- SwaggerUIBundle.plugins.DownloadUrl
- ],
- layout: 'StandaloneLayout'
- });
-
- window.ui = swaggerUi;
-});
diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js
index a75ba564..5e16d5c9 100644
--- a/public/viewjs/batteries.js
+++ b/public/viewjs/batteries.js
@@ -1,7 +1,10 @@
$(document).on('click', '.battery-delete-button', function(e)
{
+ var objectName = $(e.currentTarget).attr('data-battery-name');
+ var objectId = $(e.currentTarget).attr('data-battery-id');
+
bootbox.confirm({
- message: L('Are you sure to delete battery "#1"?', $(e.currentTarget).attr('data-battery-name')),
+ message: L('Are you sure to delete battery "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
@@ -16,7 +19,7 @@
{
if (result === true)
{
- Grocy.Api.Get('delete-object/batteries/' + $(e.currentTarget).attr('data-battery-id'),
+ Grocy.Api.Get('delete-object/batteries/' + objectId,
function(result)
{
window.location.href = U('/batteries');
diff --git a/public/viewjs/components/habitcard.js b/public/viewjs/components/habitcard.js
index 41cbcbd2..5514c4d5 100644
--- a/public/viewjs/components/habitcard.js
+++ b/public/viewjs/components/habitcard.js
@@ -1,6 +1,6 @@
Grocy.Components.HabitCard = { };
-Grocy.Components.HabitCard.Refresh = function (habitId)
+Grocy.Components.HabitCard.Refresh = function(habitId)
{
Grocy.Api.Get('habits/get-habit-details/' + habitId,
function(habitDetails)
diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js
index 563b6bae..db0ff57b 100644
--- a/public/viewjs/consume.js
+++ b/public/viewjs/consume.js
@@ -60,7 +60,7 @@ $('#product_id').on('change', function(e)
$('#product_id_text_input').addClass('has-error');
$('#product_id_text_input').parent('.input-group').addClass('has-error');
$('#product_id_text_input').closest('.form-group').addClass('has-error');
- $('#product-error').text('This product is not in stock.');
+ $('#product-error').text(L('This product is not in stock'));
$('#product-error').show();
$('#product_id_text_input').focus();
}
diff --git a/public/viewjs/habitform.js b/public/viewjs/habitform.js
index 0f0fd7a6..c4f37df6 100644
--- a/public/viewjs/habitform.js
+++ b/public/viewjs/habitform.js
@@ -41,7 +41,7 @@ $('.input-group-habit-period-type').on('change', function(e)
if (periodType === 'dynamic-regular')
{
- $('#habit-period-type-info').text('This means it is estimated that a new "execution" of this habit is tracked ' + periodDays.toString() + ' days after the last was tracked.');
+ $('#habit-period-type-info').text(L('This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked', periodDays.toString()));
$('#habit-period-type-info').show();
}
else
diff --git a/public/viewjs/habits.js b/public/viewjs/habits.js
index 84e52197..2b6b0644 100644
--- a/public/viewjs/habits.js
+++ b/public/viewjs/habits.js
@@ -1,7 +1,10 @@
$(document).on('click', '.habit-delete-button', function(e)
{
+ var objectName = $(e.currentTarget).attr('data-habit-name');
+ var objectId = $(e.currentTarget).attr('data-habit-id');
+
bootbox.confirm({
- message: L('Are you sure to delete habit "#1"?', $(e.currentTarget).attr('data-habit-name')),
+ message: L('Are you sure to delete habit "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
@@ -16,7 +19,7 @@
{
if (result === true)
{
- Grocy.Api.Get('delete-object/habits/' + $(e.currentTarget).attr('data-habit-id'),
+ Grocy.Api.Get('delete-object/habits/' + objectId,
function(result)
{
window.location.href = U('/habits');
diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js
index d4fc67e9..030ed5cd 100644
--- a/public/viewjs/inventory.js
+++ b/public/viewjs/inventory.js
@@ -291,14 +291,14 @@ $('#new_amount').on('change', function(e)
if (newAmount > productStockAmount)
{
var amountToAdd = newAmount - productDetails.stock_amount;
- $('#inventory-change-info').text('This means ' + amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be added to stock');
+ $('#inventory-change-info').text(L('This means #1 will be added to stock', amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').show();
$('#best_before_date').attr('required', 'required');
}
else if (newAmount < productStockAmount)
{
var amountToRemove = productStockAmount - newAmount;
- $('#inventory-change-info').text('This means ' + amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be removed from stock');
+ $('#inventory-change-info').text(L('This means #1 will be removed from stock', amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').show();
$('#best_before_date').removeAttr('required');
}
diff --git a/public/viewjs/locations.js b/public/viewjs/locations.js
index 619f1a00..1f4043ce 100644
--- a/public/viewjs/locations.js
+++ b/public/viewjs/locations.js
@@ -1,7 +1,10 @@
$(document).on('click', '.location-delete-button', function(e)
{
+ var objectName = $(e.currentTarget).attr('data-location-name');
+ var objectId = $(e.currentTarget).attr('data-location-id');
+
bootbox.confirm({
- message: L('Are you sure to delete location "#1"?', $(e.currentTarget).attr('data-location-name')),
+ message: L('Are you sure to delete location "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
@@ -16,7 +19,7 @@
{
if (result === true)
{
- Grocy.Api.Get('delete-object/locations/' + $(e.currentTarget).attr('data-location-id'),
+ Grocy.Api.Get('delete-object/locations/' + objectId,
function(result)
{
window.location.href = U('/locations');
diff --git a/public/viewjs/manageapikeys.js b/public/viewjs/manageapikeys.js
new file mode 100644
index 00000000..40bc25a1
--- /dev/null
+++ b/public/viewjs/manageapikeys.js
@@ -0,0 +1,50 @@
+$(document).on('click', '.apikey-delete-button', function(e)
+{
+ var objectName = $(e.currentTarget).attr('data-apikey-apikey');
+ var objectId = $(e.currentTarget).attr('data-apikey-id');
+
+ bootbox.confirm({
+ message: L('Are you sure to delete API key "#1"?', objectName),
+ buttons: {
+ confirm: {
+ label: L('Yes'),
+ className: 'btn-success'
+ },
+ cancel: {
+ label: L('No'),
+ className: 'btn-danger'
+ }
+ },
+ callback: function(result)
+ {
+ if (result === true)
+ {
+ Grocy.Api.Get('delete-object/api_keys/' + objectId,
+ function(result)
+ {
+ window.location.href = U('/manageapikeys');
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+ }
+ }
+ });
+});
+
+$('#apikeys-table').DataTable({
+ 'pageLength': 50,
+ 'order': [[4, 'desc']],
+ 'columnDefs': [
+ { 'orderable': false, 'targets': 0 }
+ ],
+ 'language': JSON.parse(L('datatables_localization'))
+});
+
+var createdApiKeyId = GetUriParam('CreatedApiKeyId');
+if (createdApiKeyId !== undefined)
+{
+ $('#apiKeyRow_' + createdApiKeyId).effect('highlight', { }, 3000);
+}
diff --git a/public/viewjs/openapiui.js b/public/viewjs/openapiui.js
new file mode 100644
index 00000000..a598aa75
--- /dev/null
+++ b/public/viewjs/openapiui.js
@@ -0,0 +1,26 @@
+function HideTopbarPlugin()
+{
+ return {
+ components: {
+ Topbar: function () { return null }
+ }
+ }
+}
+
+const swaggerUi = SwaggerUIBundle({
+ url: Grocy.OpenApi.SpecUrl,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ presets: [
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ],
+ plugins: [
+ SwaggerUIBundle.plugins.DownloadUrl,
+ HideTopbarPlugin
+ ],
+ layout: 'StandaloneLayout',
+ docExpansion: "list"
+});
+
+window.ui = swaggerUi;
diff --git a/public/viewjs/products.js b/public/viewjs/products.js
index ff58d44e..5a2d5477 100644
--- a/public/viewjs/products.js
+++ b/public/viewjs/products.js
@@ -1,7 +1,10 @@
$(document).on('click', '.product-delete-button', function(e)
{
+ var objectName = $(e.currentTarget).attr('data-product-name');
+ var objectId = $(e.currentTarget).attr('data-product-id');
+
bootbox.confirm({
- message: L('Are you sure to delete product "#1"?', $(e.currentTarget).attr('data-product-name')),
+ message: L('Are you sure to delete product "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
@@ -16,7 +19,7 @@
{
if (result === true)
{
- Grocy.Api.Get('delete-object/products/' + $(e.currentTarget).attr('data-product-id'),
+ Grocy.Api.Get('delete-object/products/' + objectId,
function(result)
{
window.location.href = U('/products');
diff --git a/public/viewjs/quantityunits.js b/public/viewjs/quantityunits.js
index ef28335c..44302c2a 100644
--- a/public/viewjs/quantityunits.js
+++ b/public/viewjs/quantityunits.js
@@ -1,7 +1,10 @@
$(document).on('click', '.quantityunit-delete-button', function(e)
{
+ var objectName = $(e.currentTarget).attr('data-quantityunit-name');
+ var objectId = $(e.currentTarget).attr('data-quantityunit-id');
+
bootbox.confirm({
- message: L('Are you sure to delete quantity unit "#1"?', $(e.currentTarget).attr('data-quantityunit-name')),
+ message: L('Are you sure to delete quantity unit "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
@@ -16,7 +19,7 @@
{
if (result === true)
{
- Grocy.Api.Get('delete-object/quantity_units/' + $(e.currentTarget).attr('data-quantityunit-id'),
+ Grocy.Api.Get('delete-object/quantity_units/' + objectId,
function(result)
{
window.location.href = U('/quantityunits');
diff --git a/routes.php b/routes.php
index e828c272..b7855ca5 100644
--- a/routes.php
+++ b/routes.php
@@ -3,6 +3,8 @@
use \Grocy\Middleware\JsonMiddleware;
use \Grocy\Middleware\CliMiddleware;
use \Grocy\Middleware\SessionAuthMiddleware;
+use \Grocy\Middleware\ApiKeyAuthMiddleware;
+use \Tuupola\Middleware\CorsMiddleware;
$app->group('', function()
{
@@ -47,12 +49,14 @@ $app->group('', function()
$this->get('/battery/{batteryId}', 'Grocy\Controllers\BatteriesController:BatteryEditForm');
// Other routes
- $this->get('/apidoc', 'Grocy\Controllers\OpenApiController:DocumentationUi');
+ $this->get('/api', 'Grocy\Controllers\OpenApiController:DocumentationUi');
+ $this->get('/manageapikeys', 'Grocy\Controllers\OpenApiController:ApiKeysList');
+ $this->get('/manageapikeys/new', 'Grocy\Controllers\OpenApiController:CreateNewApiKey');
})->add(new SessionAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName()));
$app->group('/api', function()
{
- $this->get('/get-open-api-specification', 'Grocy\Controllers\OpenApiController:DocumentationSpec');
+ $this->get('/get-openapi-specification', 'Grocy\Controllers\OpenApiController:DocumentationSpec');
$this->get('/get-objects/{entity}', 'Grocy\Controllers\GenericEntityApiController:GetObjects');
$this->get('/get-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:GetObject');
@@ -72,7 +76,16 @@ $app->group('/api', function()
$this->get('/batteries/track-charge-cycle/{batteryId}', 'Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', 'Grocy\Controllers\BatteriesApiController:BatteryDetails');
-})->add(new SessionAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName()))->add(JsonMiddleware::class);
+})->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName))
+->add(JsonMiddleware::class)
+->add(new CorsMiddleware([
+ 'origin' => ["*"],
+ 'methods' => ["GET", "POST"],
+ 'headers.allow' => [ $appContainer->ApiKeyHeaderName ],
+ 'headers.expose' => [ ],
+ 'credentials' => false,
+ 'cache' => 0,
+]));
$app->group('/cli', function()
{
diff --git a/services/ApiKeyService.php b/services/ApiKeyService.php
new file mode 100644
index 00000000..06756ec5
--- /dev/null
+++ b/services/ApiKeyService.php
@@ -0,0 +1,64 @@
+Database->api_keys()->where('api_key = :1 AND expires > :2', $apiKey, date('Y-m-d H:i:s', time()))->fetch();
+ if ($apiKeyRow !== null)
+ {
+ $apiKeyRow->update(array(
+ 'last_used' => date('Y-m-d H:i:s', time())
+ ));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function CreateApiKey()
+ {
+ $newApiKey = $this->GenerateApiKey();
+
+ $apiKeyRow = $this->Database->api_keys()->createRow(array(
+ 'api_key' => $newApiKey,
+ 'expires' => '2999-12-31 23:59:59' // Default is that API keys expire never
+ ));
+ $apiKeyRow->save();
+
+ return $newApiKey;
+ }
+
+ public function RemoveApiKey($apiKey)
+ {
+ $this->Database->api_keys()->where('api_key', $apiKey)->delete();
+ }
+
+ public function GetApiKeyId($apiKey)
+ {
+ $apiKey = $this->Database->api_keys()->where('api_key', $apiKey)->fetch();
+ return $apiKey->id;
+ }
+
+ private function GenerateApiKey()
+ {
+ return RandomString(50);
+ }
+}
diff --git a/services/SessionService.php b/services/SessionService.php
index 780e10bc..44becd41 100644
--- a/services/SessionService.php
+++ b/services/SessionService.php
@@ -15,7 +15,18 @@ class SessionService extends BaseService
}
else
{
- return $this->Database->sessions()->where('session_key = :1 AND expires > :2', $sessionKey, time())->count() === 1;
+ $sessionRow = $this->Database->sessions()->where('session_key = :1 AND expires > :2', $sessionKey, date('Y-m-d H:i:s', time()))->fetch();
+ if ($sessionRow !== null)
+ {
+ $sessionRow->update(array(
+ 'last_used' => date('Y-m-d H:i:s', time())
+ ));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
}
}
@@ -28,7 +39,7 @@ class SessionService extends BaseService
$sessionRow = $this->Database->sessions()->createRow(array(
'session_key' => $newSessionKey,
- 'expires' => time() + 2592000 // 30 days
+ 'expires' => date('Y-m-d H:i:s', time() + 2592000) // Default is that sessions expire in 30 days
));
$sessionRow->save();
diff --git a/version.txt b/version.txt
index 53adb84c..f8e233b2 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.8.2
+1.9.0
diff --git a/views/apidoc.blade.php b/views/apidoc.blade.php
deleted file mode 100644
index 3df710b3..00000000
--- a/views/apidoc.blade.php
+++ /dev/null
@@ -1,17 +0,0 @@
-@extends('layout.default')
-
-@section('title', $L('REST API documentation'))
-@section('viewJsName', 'apidoc')
-
-@section('content')
-
{{ $L('REST API & data model documentation') }}
+ +# | +{{ $L('API key') }} | +{{ $L('Expires') }} | +{{ $L('Last used') }} | +{{ $L('Created') }} | +
---|---|---|---|---|
+ + + + | ++ {{ $apiKey->api_key }} + | ++ {{ $apiKey->expires }} + + | ++ @if(empty($apiKey->last_used)){{ $L('never') }}@else{{ $apiKey->last_used }}@endif + + | ++ {{ $apiKey->row_created_timestamp }} + + | +