From 276bc94cc693fa6681a855967690215480acf0e4 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 21 Jan 2019 22:13:42 +0100 Subject: [PATCH] More improvements on the REST API (references #139) --- controllers/FilesApiController.php | 18 ++-- grocy.openapi.json | 109 ++++++++++++------------ public/js/grocy.js | 26 ++++-- public/viewjs/batteriesjournal.js | 2 +- public/viewjs/batterytracking.js | 2 +- public/viewjs/choresjournal.js | 2 +- public/viewjs/choretracking.js | 2 +- public/viewjs/components/productcard.js | 2 +- public/viewjs/equipment.js | 2 +- public/viewjs/inventory.js | 2 +- public/viewjs/purchase.js | 2 +- public/viewjs/userform.js | 4 +- routes.php | 10 +-- views/equipmentform.blade.php | 2 +- views/productform.blade.php | 2 +- 15 files changed, 102 insertions(+), 85 deletions(-) diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index bc065631..9af9f751 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -18,13 +18,13 @@ class FilesApiController extends BaseApiController { try { - if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + if (IsValidFileName(base64_decode($args['fileName']))) { - $fileName = $request->getQueryParams()['file_name']; + $fileName = base64_decode($args['fileName']); } else { - throw new \Exception('file_name query parameter missing or contains an invalid filename'); + throw new \Exception('Invalid filename'); } $data = $request->getBody()->getContents(); @@ -42,13 +42,13 @@ class FilesApiController extends BaseApiController { try { - if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + if (IsValidFileName(base64_decode($args['fileName']))) { - $fileName = $request->getQueryParams()['file_name']; + $fileName = base64_decode($args['fileName']); } else { - throw new \Exception('file_name query parameter missing or contains an invalid filename'); + throw new \Exception('Invalid filename'); } $filePath = $this->FilesService->GetFilePath($args['group'], $fileName); @@ -74,13 +74,13 @@ class FilesApiController extends BaseApiController { try { - if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + if (IsValidFileName(base64_decode($args['fileName']))) { - $fileName = $request->getQueryParams()['file_name']; + $fileName = base64_decode($args['fileName']); } else { - throw new \Exception('file_name query parameter missing or contains an invalid filename'); + throw new \Exception('Invalid filename'); } $filePath = $this->FilesService->GetFilePath($args['group'], $fileName); diff --git a/grocy.openapi.json b/grocy.openapi.json index 8e1fbfd3..7506b89c 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -26,7 +26,7 @@ "paths": { "/system/db-changed-time": { "get": { - "description": "Returns the time when the database was last changed", + "summary": "Returns the time when the database was last changed", "tags": [ "System" ], @@ -46,7 +46,8 @@ }, "/system/log-missing-localization": { "post": { - "description": "Logs a missing localization string (only when MODE == 'dev', so should only be called then)", + "summary": "Logs a missing localization string", + "description": "Only when MODE == 'dev', so should only be called then", "tags": [ "System" ], @@ -80,7 +81,7 @@ }, "/objects/{entity}": { "get": { - "description": "Returns all objects of the given entity", + "summary": "Returns all objects of the given entity", "tags": [ "Generic entity interactions" ], @@ -144,7 +145,7 @@ } }, "post": { - "description": "Adds a single object of the given entity", + "summary": "Adds a single object of the given entity", "tags": [ "Generic entity interactions" ], @@ -211,7 +212,7 @@ }, "/objects/{entity}/{objectId}": { "get": { - "description": "Returns a single object of the given entity", + "summary": "Returns a single object of the given entity", "tags": [ "Generic entity interactions" ], @@ -282,7 +283,7 @@ } }, "put": { - "description": "Edits the given object of the given entity", + "summary": "Edits the given object of the given entity", "tags": [ "Generic entity interactions" ], @@ -356,7 +357,7 @@ } }, "delete": { - "description": "Deletes a single object of the given entity", + "summary": "Deletes a single object of the given entity", "tags": [ "Generic entity interactions" ], @@ -397,9 +398,10 @@ } } }, - "/file/{group}": { + "/files/{group}/{fileName}": { "get": { - "description": "Serves the given file (with proper Content-Type header)", + "summary": "Serves the given file", + "description": "With proper Content-Type header", "tags": [ "Files" ], @@ -414,10 +416,10 @@ } }, { - "in": "query", - "name": "file_name", + "in": "path", + "name": "fileName", "required": true, - "description": "The file name (including extension)", + "description": "The file name (including extension)
**BASE64 encoded**", "schema": { "type": "string" } @@ -448,7 +450,8 @@ } }, "put": { - "description": "Uploads a single file to /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", + "summary": "Uploads a single file", + "description": "The file will be stored at /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", "tags": [ "Files" ], @@ -463,10 +466,10 @@ } }, { - "in": "query", - "name": "file_name", + "in": "path", + "name": "fileName", "required": true, - "description": "The file name (including extension)", + "description": "The file name (including extension)
**BASE64 encoded**", "schema": { "type": "string" } @@ -499,7 +502,7 @@ } }, "delete": { - "description": "Deletes the given file", + "summary": "Deletes the given file", "tags": [ "Files" ], @@ -514,10 +517,10 @@ } }, { - "in": "query", - "name": "file_name", + "in": "path", + "name": "fileName", "required": true, - "description": "The file name (including extension)", + "description": "The file name (including extension)
**BASE64 encoded**", "schema": { "type": "string" } @@ -542,7 +545,7 @@ }, "/users": { "get": { - "description": "Returns all users", + "summary": "Returns all users", "tags": [ "User management" ], @@ -573,7 +576,7 @@ } }, "post": { - "description": "Creates a new user", + "summary": "Creates a new user", "tags": [ "User management" ], @@ -607,7 +610,7 @@ }, "/users/{userId}": { "put": { - "description": "Edits the given user", + "summary": "Edits the given user", "tags": [ "User management" ], @@ -650,7 +653,7 @@ } }, "delete": { - "description": "Deletes the given user", + "summary": "Deletes the given user", "tags": [ "User management" ], @@ -684,7 +687,7 @@ }, "/user/settings/{settingKey}": { "get": { - "description": "Gets the given setting of the currently logged in user", + "summary": "Gets the given setting of the currently logged in user", "tags": [ "User settings" ], @@ -723,7 +726,7 @@ } }, "put": { - "description": "Sets the given setting of the currently logged in user", + "summary": "Sets the given setting of the currently logged in user", "tags": [ "User settings" ], @@ -768,7 +771,7 @@ }, "/stock": { "get": { - "description": "Returns all products which are currently in stock incl. the next expiring date per product", + "summary": "Returns all products which are currently in stock incl. the next expiring date per product", "tags": [ "Stock" ], @@ -791,7 +794,7 @@ }, "/stock/volatile": { "get": { - "description": "Returns all products which are expiring soon, are already expired or currently missing", + "summary": "Returns all products which are expiring soon, are already expired or currently missing", "tags": [ "Stock" ], @@ -826,7 +829,7 @@ }, "/stock/products/{productId}": { "get": { - "description": "Returns details of the given product", + "summary": "Returns details of the given product", "tags": [ "Stock" ], @@ -867,7 +870,7 @@ }, "/stock/products/{productId}/entries": { "get": { - "description": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)", + "summary": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)", "tags": [ "Stock" ], @@ -911,7 +914,7 @@ }, "/stock/products/{productId}/price-history": { "get": { - "description": "Returns the price history of the given product", + "summary": "Returns the price history of the given product", "tags": [ "Stock" ], @@ -955,7 +958,7 @@ }, "/stock/products/{productId}/add": { "post": { - "description": "Adds the given amount of the given product to stock", + "summary": "Adds the given amount of the given product to stock", "tags": [ "Stock" ], @@ -1030,7 +1033,7 @@ }, "/stock/products/{productId}/consume": { "post": { - "description": "Removes the given amount of the given product from stock", + "summary": "Removes the given amount of the given product from stock", "tags": [ "Stock" ], @@ -1103,7 +1106,7 @@ }, "/stock/products/{productId}/inventory": { "post": { - "description": "Inventories the given product (adds/removes based on the given new amount)", + "summary": "Inventories the given product (adds/removes based on the given new amount)", "tags": [ "Stock" ], @@ -1165,7 +1168,7 @@ }, "/stock/products/{productId}/open": { "post": { - "description": "Marks the given amount of the given product as opened", + "summary": "Marks the given amount of the given product as opened", "tags": [ "Stock" ], @@ -1229,7 +1232,7 @@ }, "/stock/shoppinglist/add-missing-products": { "post": { - "description": "Adds currently missing products (below defined min. stock amount) to the shopping list", + "summary": "Adds currently missing products (below defined min. stock amount) to the shopping list", "tags": [ "Stock" ], @@ -1242,7 +1245,7 @@ }, "/stock/shoppinglist/clear": { "post": { - "description": "Removes all items from the shopping list", + "summary": "Removes all items from the shopping list", "tags": [ "Stock" ], @@ -1255,7 +1258,7 @@ }, "/stock/bookings/{bookingId}/undo": { "post": { - "description": "Undoes a booking", + "summary": "Undoes a booking", "tags": [ "Stock" ], @@ -1289,7 +1292,7 @@ }, "/stock/barcodes/external-lookup": { "get": { - "description": "Executes an external barcode lookoup via the configured plugin with the given barcode", + "summary": "Executes an external barcode lookoup via the configured plugin with the given barcode", "tags": [ "Stock" ], @@ -1340,7 +1343,7 @@ }, "/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": { "post": { - "description": "Adds all missing products for the given recipe to the shopping list", + "summary": "Adds all missing products for the given recipe to the shopping list", "tags": [ "Recipes" ], @@ -1364,7 +1367,7 @@ }, "/recipes/{recipeId}/consume": { "post": { - "description": "Consumes all products of the given recipe", + "summary": "Consumes all products of the given recipe", "tags": [ "Recipes" ], @@ -1388,7 +1391,7 @@ }, "/chores": { "get": { - "description": "Returns all chores incl. the next estimated execution time per chore", + "summary": "Returns all chores incl. the next estimated execution time per chore", "tags": [ "Chores" ], @@ -1411,7 +1414,7 @@ }, "/chores/{choreId}": { "get": { - "description": "Returns details of the given chore", + "summary": "Returns details of the given chore", "tags": [ "Chores" ], @@ -1452,7 +1455,7 @@ }, "/chores/{choreId}/execute": { "post": { - "description": "Tracks an execution of the given chore", + "summary": "Tracks an execution of the given chore", "tags": [ "Chores" ], @@ -1517,9 +1520,9 @@ } } }, - "/chores/{executionId}/undo": { + "/chores/executions/{executionId}/undo": { "post": { - "description": "Undoes a chore execution", + "summary": "Undoes a chore execution", "tags": [ "Chores" ], @@ -1553,7 +1556,7 @@ }, "/batteries": { "get": { - "description": "Returns all batteries incl. the next estimated charge time per battery", + "summary": "Returns all batteries incl. the next estimated charge time per battery", "tags": [ "Batteries" ], @@ -1576,7 +1579,7 @@ }, "/batteries/{batteryId}": { "get": { - "description": "Returns details of the given battery", + "summary": "Returns details of the given battery", "tags": [ "Batteries" ], @@ -1617,7 +1620,7 @@ }, "/batteries/{batteryId}/charge": { "post": { - "description": "Tracks a charge cycle of the given battery", + "summary": "Tracks a charge cycle of the given battery", "tags": [ "Batteries" ], @@ -1678,9 +1681,9 @@ } } }, - "/batteries/{chargeCycleId}/undo": { + "/batteries/charge-cycles/{chargeCycleId}/undo": { "post": { - "description": "Undoes a battery charge cycle", + "summary": "Undoes a battery charge cycle", "tags": [ "Batteries" ], @@ -1714,7 +1717,7 @@ }, "/tasks": { "get": { - "description": "Returns all tasks which are not done yet", + "summary": "Returns all tasks which are not done yet", "tags": [ "Tasks" ], @@ -1737,7 +1740,7 @@ }, "/tasks/{taskId}/complete": { "post": { - "description": "Marks the given task as completed", + "summary": "Marks the given task as completed", "tags": [ "Tasks" ], diff --git a/public/js/grocy.js b/public/js/grocy.js index 5fda4828..1e113f75 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -270,17 +270,24 @@ Grocy.Api.Delete = function(apiFunction, jsonData, success, error) Grocy.Api.UploadFile = function(file, group, fileName, success, error) { var xhr = new XMLHttpRequest(); - var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); + var url = U('/api/files/' + group + '/' + btoa(fileName)); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) + if (xhr.status === 200 || xhr.status === 204) { if (success) { - success(JSON.parse(xhr.responseText)); + if (xhr.status === 200) + { + success(JSON.parse(xhr.responseText)); + } + else + { + success({ }); + } } } else @@ -301,17 +308,24 @@ Grocy.Api.UploadFile = function(file, group, fileName, success, error) Grocy.Api.DeleteFile = function(fileName, group, success, error) { var xhr = new XMLHttpRequest(); - var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); + var url = U('/api/files/' + group + '/' + btoa(fileName)); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) + if (xhr.status === 200 || xhr.status === 204) { if (success) { - success(JSON.parse(xhr.responseText)); + if (xhr.status === 200) + { + success(JSON.parse(xhr.responseText)); + } + else + { + success({ }); + } } } else diff --git a/public/viewjs/batteriesjournal.js b/public/viewjs/batteriesjournal.js index c4f825e2..fbfddefb 100644 --- a/public/viewjs/batteriesjournal.js +++ b/public/viewjs/batteriesjournal.js @@ -56,7 +56,7 @@ $(document).on('click', '.undo-battery-execution-button', function(e) var element = $(e.currentTarget); var chargeCycleId = $(e.currentTarget).attr('data-charge-cycle-id'); - Grocy.Api.Post('batteries/' + chargeCycleId.toString() + '/undo', { }, + Grocy.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', { }, function(result) { element.closest("tr").addClass("text-muted"); diff --git a/public/viewjs/batterytracking.js b/public/viewjs/batterytracking.js index d21a2055..002b66d8 100644 --- a/public/viewjs/batterytracking.js +++ b/public/viewjs/batterytracking.js @@ -92,7 +92,7 @@ $('#tracked_time').find('input').on('keypress', function (e) function UndoChargeCycle(chargeCycleId) { - Grocy.Api.Post('batteries/' + chargeCycleId.toString() + '/undo', { }, + Grocy.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', { }, function(result) { toastr.success(L("Charge cycle successfully undone")); diff --git a/public/viewjs/choresjournal.js b/public/viewjs/choresjournal.js index a2923bc5..257a392a 100644 --- a/public/viewjs/choresjournal.js +++ b/public/viewjs/choresjournal.js @@ -56,7 +56,7 @@ $(document).on('click', '.undo-chore-execution-button', function(e) var element = $(e.currentTarget); var executionId = $(e.currentTarget).attr('data-execution-id'); - Grocy.Api.Post('chores/' + executionId.toString() + '/undo', { }, + Grocy.Api.Post('chores/executions/' + executionId.toString() + '/undo', { }, function(result) { element.closest("tr").addClass("text-muted"); diff --git a/public/viewjs/choretracking.js b/public/viewjs/choretracking.js index fc840833..15b7132d 100644 --- a/public/viewjs/choretracking.js +++ b/public/viewjs/choretracking.js @@ -89,7 +89,7 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) function UndoChoreExecution(executionId) { - Grocy.Api.Post('chores/' + executionId.toString() + '/undo', { }, + Grocy.Api.Post('chores/executions/' + executionId.toString() + '/undo', { }, function(result) { toastr.success(L("Chore execution successfully undone")); diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 14874934..806a8349 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -41,7 +41,7 @@ Grocy.Components.ProductCard.Refresh = function(productId) { $("#productcard-no-product-picture").addClass("d-none"); $("#productcard-product-picture").removeClass("d-none"); - $("#productcard-product-picture").attr("src", U('/api/file/productpictures?file_name=' + productDetails.product.picture_file_name)); + $("#productcard-product-picture").attr("src", U('/api/files/productpictures/' + btoa(productDetails.product.picture_file_name))); } else { diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js index cbdbcee3..ad181a5e 100644 --- a/public/viewjs/equipment.js +++ b/public/viewjs/equipment.js @@ -43,7 +43,7 @@ function DisplayEquipment(id) if (equipmentItem.instruction_manual_file_name !== null && !equipmentItem.instruction_manual_file_name.isEmpty()) { - var pdfUrl = U('/api/file/equipmentmanuals?file_name=' + equipmentItem.instruction_manual_file_name); + var pdfUrl = U('/api/files/equipmentmanuals/' + btoa(equipmentItem.instruction_manual_file_name)); $("#selected-equipment-instruction-manual").attr("src", pdfUrl); $("#selected-equipment-instruction-manual").removeClass("d-none"); $("#selected-equipment-has-no-instruction-manual-hint").addClass("d-none"); diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 852267e5..983461fe 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -197,7 +197,7 @@ $('#new_amount').on('keyup', function(e) function UndoStockBooking(bookingId) { - Grocy.Api.Get('booking/' + bookingId.toString() + '/undo', + Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { }, function(result) { toastr.success(L("Booking successfully undone")); diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 34bfe7aa..a4988438 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -202,7 +202,7 @@ if (GetUriParam("flow") === "shoppinglistitemtostock") function UndoStockBooking(bookingId) { - Grocy.Api.Get('sbooking/' + bookingId.toString() + '/undo', + Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { }, function(result) { toastr.success(L("Booking successfully undone")); diff --git a/public/viewjs/userform.js b/public/viewjs/userform.js index c39e39b5..96c4d193 100644 --- a/public/viewjs/userform.js +++ b/public/viewjs/userform.js @@ -7,7 +7,7 @@ if (Grocy.EditMode === 'create') { - Grocy.Api.Post('users/create', jsonData, + Grocy.Api.Post('users', jsonData, function(result) { window.location.href = U('/users'); @@ -21,7 +21,7 @@ } else { - Grocy.Api.Post('users/edit/' + Grocy.EditObjectId, jsonData, + Grocy.Api.Put('users/' + Grocy.EditObjectId, jsonData, function(result) { window.location.href = U('/users'); diff --git a/routes.php b/routes.php index 30161a04..57e16a7f 100644 --- a/routes.php +++ b/routes.php @@ -94,9 +94,9 @@ $app->group('/api', function() $this->delete('/objects/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject'); // Files - $this->put('/file/{group}', '\Grocy\Controllers\FilesApiController:UploadFile'); - $this->get('/file/{group}', '\Grocy\Controllers\FilesApiController:ServeFile'); - $this->delete('/file/{group}', '\Grocy\Controllers\FilesApiController:DeleteFile'); + $this->put('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:UploadFile'); + $this->get('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:ServeFile'); + $this->delete('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:DeleteFile'); // Users $this->get('/users', '\Grocy\Controllers\UsersApiController:GetUsers'); @@ -131,13 +131,13 @@ $app->group('/api', function() $this->get('/chores', '\Grocy\Controllers\ChoresApiController:Current'); $this->get('/chores/{choreId}', '\Grocy\Controllers\ChoresApiController:ChoreDetails'); $this->post('/chores/{choreId}/execute', '\Grocy\Controllers\ChoresApiController:TrackChoreExecution'); - $this->post('/chores/{executionId}/undo', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution'); + $this->post('/chores/executions/{executionId}/undo', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution'); // Batteries $this->get('/batteries', '\Grocy\Controllers\BatteriesApiController:Current'); $this->get('/batteries/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails'); $this->post('/batteries/{batteryId}/charge', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle'); - $this->post('/batteries/{chargeCycleId}/undo', '\Grocy\Controllers\BatteriesApiController:UndoChargeCycle'); + $this->post('/batteries/charge-cycles/{chargeCycleId}/undo', '\Grocy\Controllers\BatteriesApiController:UndoChargeCycle'); // Tasks $this->get('/tasks', '\Grocy\Controllers\TasksApiController:Current'); diff --git a/views/equipmentform.blade.php b/views/equipmentform.blade.php index bc9d093d..88f78b1c 100644 --- a/views/equipmentform.blade.php +++ b/views/equipmentform.blade.php @@ -64,7 +64,7 @@ @if(!empty($equipment->instruction_manual_file_name)) - +

{{ $L('The current instruction manual will be deleted when you save the equipment') }}

@else

{{ $L('No instruction manual available') }}

diff --git a/views/productform.blade.php b/views/productform.blade.php index 79254479..57515134 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -143,7 +143,7 @@ @if(!empty($product->picture_file_name)) -

+

{{ $L('The current picture will be deleted when you save the product') }}

@else

{{ $L('No picture available') }}