From b7d1b21f1d6976ca8bb8b0d5326471e1ead718f2 Mon Sep 17 00:00:00 2001 From: fipwmaqzufheoxq92ebc <29818044+fipwmaqzufheoxq92ebc@users.noreply.github.com> Date: Sat, 29 Aug 2020 12:05:32 +0200 Subject: [PATCH] [WIP] Implemented basic permissions (#960) * Add permissions to Database & add "User"-classes * Add UI & API for Permissions, protect "User"-(Api)-Controller with new permissions. * Add some permissions. * Add permission localization * Add error handling. * Error pages: only redirect on 404 * ExceptionController: return JSON-Response on api-routes * Rename PRODUCT_ADD to PRODUCT_PURCHASE * Move translation to new file * Fix checkboxes stay selected on reload. * Remove configurable User-implementation * Remove MASTER_DATA_READ * Disable buttons the user isn't allowed to use. * Add default permissions for new users * When migration to permissions, everyone starts as ADMIN * Permission-Localization: add to transifex & LocalizationService * Review Co-authored-by: Bernd Bestel --- .devtools/transifex_download.bat | 1 + .tx/config | 6 + app.php | 5 +- config-dist.php | 5 + controllers/BaseController.php | 6 + controllers/BatteriesApiController.php | 10 +- controllers/ChoresApiController.php | 16 ++- controllers/ExceptionController.php | 69 +++++++++++ controllers/FilesApiController.php | 9 +- controllers/GenericEntityApiController.php | 27 +++-- controllers/RecipesApiController.php | 10 +- controllers/StockApiController.php | 53 ++++++--- controllers/TasksApiController.php | 10 +- .../Users/PermissionMissingException.php | 15 +++ controllers/Users/User.php | 91 +++++++++++++++ controllers/UsersApiController.php | 78 ++++++++++++- controllers/UsersController.php | 23 +++- localization/en/permissions.po | 85 ++++++++++++++ localization/permissions.pot | 85 ++++++++++++++ localization/strings.pot | 42 +++++++ migrations/0111.sql | 109 ++++++++++++++++++ public/css/grocy.css | 4 + public/js/grocy.js | 8 ++ public/viewjs/userpermissions.js | 42 +++++++ routes.php | 8 +- services/LocalizationService.php | 5 + services/UsersService.php | 12 +- views/batteries.blade.php | 6 +- views/batteriesjournal.blade.php | 2 +- views/batteriesoverview.blade.php | 4 +- views/choresjournal.blade.php | 2 +- views/choresoverview.blade.php | 4 +- .../userpermission_select.blade.php | 15 +++ views/errors/403.blade.php | 11 ++ views/errors/404.blade.php | 13 +++ views/errors/500.blade.php | 16 +++ views/errors/base.blade.php | 20 ++++ views/layout/default.blade.php | 15 +-- views/stockoverview.blade.php | 18 +-- views/userpermissions.blade.php | 34 ++++++ views/users.blade.php | 3 + 41 files changed, 930 insertions(+), 67 deletions(-) create mode 100644 controllers/ExceptionController.php create mode 100644 controllers/Users/PermissionMissingException.php create mode 100644 controllers/Users/User.php create mode 100644 localization/en/permissions.po create mode 100644 localization/permissions.pot create mode 100644 migrations/0111.sql create mode 100644 public/viewjs/userpermissions.js create mode 100644 views/components/userpermission_select.blade.php create mode 100644 views/errors/403.blade.php create mode 100644 views/errors/404.blade.php create mode 100644 views/errors/500.blade.php create mode 100644 views/errors/base.blade.php create mode 100644 views/userpermissions.blade.php diff --git a/.devtools/transifex_download.bat b/.devtools/transifex_download.bat index aad23dac..ac3536ab 100644 --- a/.devtools/transifex_download.bat +++ b/.devtools/transifex_download.bat @@ -6,4 +6,5 @@ copy /Y localization\en\stock_transaction_types.po localization\en_GB\stock_tran copy /Y localization\en\component_translations.po localization\en_GB\component_translations.po copy /Y localization\en\chore_period_types.po localization\en_GB\chore_period_types.po copy /Y localization\en\chore_assignment_types.po localization\en_GB\chore_assignment_types.po +copy /Y localization\en\permissions.po localization\en_GB\permissions.po popd diff --git a/.tx/config b/.tx/config index 4f86a1be..ee8b0313 100644 --- a/.tx/config +++ b/.tx/config @@ -42,3 +42,9 @@ file_filter = localization//userfield_types.po source_file = localization/userfield_types.pot source_lang = en type = PO + +[grocy.permissions] +file_filter = localization//permissions.po +source_file = localization/permissions.pot +source_lang = en +type = PO diff --git a/app.php b/app.php index c036c41b..7ff895d1 100644 --- a/app.php +++ b/app.php @@ -65,6 +65,9 @@ if (!empty(GROCY_BASE_PATH)) // Add default middleware $app->addRoutingMiddleware(); -$app->addErrorMiddleware(true, false, false); +$errorMiddleware = $app->addErrorMiddleware(true, false, false); +$errorMiddleware->setDefaultErrorHandler( + new \Grocy\Controllers\ExceptionController($app, $container) +); $app->run(); diff --git a/config-dist.php b/config-dist.php index b8bc56c1..44e0f8e5 100644 --- a/config-dist.php +++ b/config-dist.php @@ -82,6 +82,11 @@ Setting('DISABLE_BROWSER_BARCODE_CAMERA_SCANNING', false); # Needs to be a number where Sunday = 0, Monday = 1 and so forth Setting('MEAL_PLAN_FIRST_DAY_OF_WEEK', ''); +# Default permissions for new users +# the array needs to contain the technical/constant names +# see the file controllers/Users/User.php for possible values +Setting('DEFAULT_PERMISSIONS', ['ADMIN']); + # Default user settings # These settings can be changed per user, here the defaults diff --git a/controllers/BaseController.php b/controllers/BaseController.php index 0de7c51f..b82c22ba 100644 --- a/controllers/BaseController.php +++ b/controllers/BaseController.php @@ -2,6 +2,7 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; use \Grocy\Services\DatabaseService; use \Grocy\Services\ApplicationService; use \Grocy\Services\LocalizationService; @@ -66,6 +67,11 @@ class BaseController } $this->View->set('featureFlags', $constants); + if (GROCY_AUTHENTICATED) + { + $this->View->set('permissions', User::PermissionList()); + } + return $this->View->render($response, $page, $data); } diff --git a/controllers/BatteriesApiController.php b/controllers/BatteriesApiController.php index b0cc0700..271e6ed2 100644 --- a/controllers/BatteriesApiController.php +++ b/controllers/BatteriesApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class BatteriesApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -11,7 +13,9 @@ class BatteriesApiController extends BaseApiController public function TrackChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_BATTERY_TRACK_CHARGE_CYCLE); + + $requestBody = $request->getParsedBody(); try { @@ -49,7 +53,9 @@ class BatteriesApiController extends BaseApiController public function UndoChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_BATTERY_UNDO_TRACK_CHARGE_CYCLE); + + try { $this->ApiResponse($response, $this->getBatteriesService()->UndoChargeCycle($args['chargeCycleId'])); return $this->EmptyApiResponse($response); diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index e05495fa..b118e31d 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class ChoresApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -15,7 +17,9 @@ class ChoresApiController extends BaseApiController try { - $trackedTime = date('Y-m-d H:i:s'); + User::checkPermission($request, User::PERMISSION_CHORE_TRACK); + + $trackedTime = date('Y-m-d H:i:s'); if (array_key_exists('tracked_time', $requestBody) && (IsIsoDateTime($requestBody['tracked_time']) || IsIsoDate($requestBody['tracked_time']))) { $trackedTime = $requestBody['tracked_time']; @@ -26,6 +30,8 @@ class ChoresApiController extends BaseApiController { $doneBy = $requestBody['done_by']; } + if($doneBy != GROCY_USER_ID) + User::checkPermission($request, User::PERMISSION_CHORE_TRACK_OTHERS); $choreExecutionId = $this->getChoresService()->TrackChore($args['choreId'], $trackedTime, $doneBy); return $this->ApiResponse($response, $this->getDatabase()->chores_log($choreExecutionId)); @@ -57,7 +63,9 @@ class ChoresApiController extends BaseApiController { try { - $this->ApiResponse($response, $this->getChoresService()->UndoChoreExecution($args['executionId'])); + User::checkPermission($request, User::PERMISSION_CHORE_UNDO); + + $this->ApiResponse($response, $this->getChoresService()->UndoChoreExecution($args['executionId'])); return $this->EmptyApiResponse($response); } catch (\Exception $ex) @@ -70,7 +78,9 @@ class ChoresApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_CHORE_EDIT); + + $requestBody = $request->getParsedBody(); $choreId = null; if (array_key_exists('chore_id', $requestBody) && !empty($requestBody['chore_id']) && is_numeric($requestBody['chore_id'])) diff --git a/controllers/ExceptionController.php b/controllers/ExceptionController.php new file mode 100644 index 00000000..0cd6b99a --- /dev/null +++ b/controllers/ExceptionController.php @@ -0,0 +1,69 @@ +app = $app; + } + + public function __invoke(ServerRequestInterface $request, + Throwable $exception, + bool $displayErrorDetails, + bool $logErrors, + bool $logErrorDetails, + ?LoggerInterface $logger = null) + { + $response = $this->app->getResponseFactory()->createResponse(); + + $isApiRoute = string_starts_with($request->getUri()->getPath(), '/api/'); + if ($isApiRoute) { + $status = 500; + if ($exception instanceof HttpException) { + $status = $exception->getCode(); + } + $data = [ + 'error_message' => $exception->getMessage(), + ]; + if ($displayErrorDetails) { + $data['error_details'] = [ + 'stack_trace' => $exception->getTraceAsString(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + } + return $this->ApiResponse($response->withStatus($status), $data); + } + if ($exception instanceof HttpNotFoundException) { + return $this->renderPage($response->withStatus(404), 'errors/404', [ + 'exception' => $exception + ]); + } + if ($exception instanceof HttpForbiddenException) { + return $this->renderPage($response->withStatus(403), 'errors/403', [ + 'exception' => $exception + ]); + } + + return $this->renderPage($response->withStatus(500), 'errors/500', [ + 'exception' => $exception + ]); + + } +} diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index b12bce38..56847204 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -2,6 +2,7 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; use \Grocy\Services\FilesService; class FilesApiController extends BaseApiController @@ -15,7 +16,9 @@ class FilesApiController extends BaseApiController { try { - if (IsValidFileName(base64_decode($args['fileName']))) + User::checkPermission($request, User::PERMISSION_UPLOAD_FILE); + + if (IsValidFileName(base64_decode($args['fileName']))) { $fileName = base64_decode($args['fileName']); } @@ -97,7 +100,9 @@ class FilesApiController extends BaseApiController { try { - if (IsValidFileName(base64_decode($args['fileName']))) + User::checkPermission($request, User::PERMISSION_DELETE_FILE); + + if (IsValidFileName(base64_decode($args['fileName']))) { $fileName = base64_decode($args['fileName']); } diff --git a/controllers/GenericEntityApiController.php b/controllers/GenericEntityApiController.php index de0775ac..28d7e8f6 100644 --- a/controllers/GenericEntityApiController.php +++ b/controllers/GenericEntityApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class GenericEntityApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -11,7 +13,7 @@ class GenericEntityApiController extends BaseApiController public function GetObjects(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $objects = $this->getDatabase()->{$args['entity']}(); + $objects = $this->getDatabase()->{$args['entity']}(); $allUserfields = $this->getUserfieldsService()->GetAllValues($args['entity']); foreach ($objects as $object) @@ -41,7 +43,7 @@ class GenericEntityApiController extends BaseApiController public function GetObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity'])) + if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity'])) { $userfields = $this->getUserfieldsService()->GetValues($args['entity'], $args['objectId']); if (count($userfields) === 0) @@ -66,7 +68,9 @@ class GenericEntityApiController extends BaseApiController public function AddObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - if ($this->IsValidEntity($args['entity'])) + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + if ($this->IsValidEntity($args['entity'])) { $requestBody = $request->getParsedBody(); @@ -97,7 +101,9 @@ class GenericEntityApiController extends BaseApiController public function EditObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - if ($this->IsValidEntity($args['entity'])) + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + if ($this->IsValidEntity($args['entity'])) { $requestBody = $request->getParsedBody(); @@ -126,7 +132,9 @@ class GenericEntityApiController extends BaseApiController public function DeleteObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - if ($this->IsValidEntity($args['entity'])) + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + if ($this->IsValidEntity($args['entity'])) { $row = $this->getDatabase()->{$args['entity']}($args['objectId']); $row->delete(); @@ -141,7 +149,8 @@ class GenericEntityApiController extends BaseApiController public function SearchObjects(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity'])) + + if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity'])) { try { @@ -160,7 +169,7 @@ class GenericEntityApiController extends BaseApiController public function GetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + try { return $this->ApiResponse($response, $this->getUserfieldsService()->GetValues($args['entity'], $args['objectId'])); } @@ -172,7 +181,9 @@ class GenericEntityApiController extends BaseApiController public function SetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + $requestBody = $request->getParsedBody(); try { diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index cb6f6c0f..02903efa 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class RecipesApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -11,7 +13,9 @@ class RecipesApiController extends BaseApiController public function AddNotFulfilledProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); + + $requestBody = $request->getParsedBody(); $excludedProductIds = null; if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) @@ -25,7 +29,9 @@ class RecipesApiController extends BaseApiController public function ConsumeRecipe(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_PRODUCT_CONSUME); + + try { $this->getRecipesService()->ConsumeRecipe($args['recipeId']); return $this->EmptyApiResponse($response); diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 3eb360a5..bc2e5aa7 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -2,6 +2,7 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; use \Grocy\Services\StockService; class StockApiController extends BaseApiController @@ -62,7 +63,9 @@ class StockApiController extends BaseApiController public function AddProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_PRODUCT_PURCHASE); + + $requestBody = $request->getParsedBody(); try { @@ -136,7 +139,9 @@ class StockApiController extends BaseApiController public function EditStockEntry(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_STOCK_EDIT); + + $requestBody = $request->getParsedBody(); try { @@ -185,7 +190,9 @@ class StockApiController extends BaseApiController public function TransferProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_STOCK_TRANSFER); + + $requestBody = $request->getParsedBody(); try { @@ -239,7 +246,9 @@ class StockApiController extends BaseApiController public function ConsumeProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_PRODUCT_CONSUME); + + $requestBody = $request->getParsedBody(); $result = null; @@ -310,7 +319,9 @@ class StockApiController extends BaseApiController public function InventoryProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION); + + $requestBody = $request->getParsedBody(); try { @@ -372,7 +383,9 @@ class StockApiController extends BaseApiController public function OpenProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_PRODUCT_OPEN); + + $requestBody = $request->getParsedBody(); try { @@ -439,7 +452,9 @@ class StockApiController extends BaseApiController public function AddMissingProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); + + try { $requestBody = $request->getParsedBody(); @@ -460,7 +475,9 @@ class StockApiController extends BaseApiController public function ClearShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE); + + try { $requestBody = $request->getParsedBody(); @@ -482,7 +499,9 @@ class StockApiController extends BaseApiController public function AddProductToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); + + try { $requestBody = $request->getParsedBody(); @@ -523,7 +542,9 @@ class StockApiController extends BaseApiController public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE); + + try { $requestBody = $request->getParsedBody(); @@ -559,7 +580,9 @@ class StockApiController extends BaseApiController public function ExternalBarcodeLookup(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + try { $addFoundProduct = false; if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1)) @@ -577,7 +600,9 @@ class StockApiController extends BaseApiController public function UndoBooking(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION); + + try { $this->ApiResponse($response, $this->getStockService()->UndoBooking($args['bookingId'])); return $this->EmptyApiResponse($response); @@ -590,7 +615,9 @@ class StockApiController extends BaseApiController public function UndoTransaction(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION); + + try { $this->ApiResponse($response, $this->getStockService()->UndoTransaction($args['transactionId'])); return $this->EmptyApiResponse($response); diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index fb8ad72a..657a5cd6 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class TasksApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -16,7 +18,9 @@ class TasksApiController extends BaseApiController public function MarkTaskAsCompleted(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + User::checkPermission($request, User::PERMISSION_TASKS_MARK_COMPLETED); + + $requestBody = $request->getParsedBody(); try { @@ -37,7 +41,9 @@ class TasksApiController extends BaseApiController public function UndoTask(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_TASKS_UNDO); + + try { $this->getTasksService()->UndoTask($args['taskId']); return $this->EmptyApiResponse($response); diff --git a/controllers/Users/PermissionMissingException.php b/controllers/Users/PermissionMissingException.php new file mode 100644 index 00000000..5bbc616e --- /dev/null +++ b/controllers/Users/PermissionMissingException.php @@ -0,0 +1,15 @@ +db = DatabaseService::getInstance()->GetDbConnection(); + + } + + protected function getPermissions(): Result + { + return $this->db->user_permissions_resolved()->where('user_id', GROCY_USER_ID); + } + + public function hasPermission(string $permission): bool + { + // global $PERMISSION_CACHE; + // if(isset($PERMISSION_CACHE[$permission])) + // return $PERMISSION_CACHE[$permission]; + return $this->getPermissions()->where('permission_name', $permission)->fetch() !== null; + } + + public static function checkPermission($request, string ...$permissions): void + { + $user = new User(); + foreach ($permissions as $permission) { + if (!$user->hasPermission($permission)) { + throw new PermissionMissingException($request, $permission); + } + } + + } + + public function getPermissionList() + { + return $this->db->uihelper_user_permissions()->where('user_id', GROCY_USER_ID); + } + + public static function hasPermissions(string ...$permissions) + { + $user = new User(); + foreach ($permissions as $permission) { + if (!$user->hasPermission($permission)) { + return false; + } + } + return true; + } + + public static function PermissionList() + { + $user = new User(); + return $user->getPermissionList(); + } +} diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php index c7f21cf0..20f4a00b 100644 --- a/controllers/UsersApiController.php +++ b/controllers/UsersApiController.php @@ -2,6 +2,8 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class UsersApiController extends BaseApiController { public function __construct(\DI\Container $container) @@ -11,7 +13,8 @@ class UsersApiController extends BaseApiController public function GetUsers(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_READ_USER); + try { return $this->ApiResponse($response, $this->getUsersService()->GetUsersAsDto()); } @@ -23,6 +26,7 @@ class UsersApiController extends BaseApiController public function CreateUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { + User::checkPermission($request, User::PERMISSION_CREATE_USER); $requestBody = $request->getParsedBody(); try @@ -43,7 +47,8 @@ class UsersApiController extends BaseApiController public function DeleteUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - try + User::checkPermission($request, User::PERMISSION_EDIT_USER); + try { $this->getUsersService()->DeleteUser($args['userId']); return $this->EmptyApiResponse($response); @@ -56,7 +61,12 @@ class UsersApiController extends BaseApiController public function EditUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + if ($args['userId'] == GROCY_USER_ID) { + User::checkPermission($request, User::PERMISSION_EDIT_SELF); + } else { + User::checkPermission($request, User::PERMISSION_EDIT_USER); + } + $requestBody = $request->getParsedBody(); try { @@ -108,4 +118,66 @@ class UsersApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function AddPermission(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + try { + User::checkPermission($request, User::PERMISSION_ADMIN); + $requestBody = $request->getParsedBody(); + + $this->getDatabase()->user_permissions()->createRow(array( + 'user_id' => $args['userId'], + 'permission_id' => $requestBody['permission_id'], + ))->save(); + return $this->EmptyApiResponse($response); + } catch (\Slim\Exception\HttpSpecializedException $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage(), $ex->getCode()); + } catch (\Exception $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function ListPermissions(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + try { + User::checkPermission($request, User::PERMISSION_ADMIN); + + return $this->ApiResponse($response, + $this->getDatabase()->user_permissions()->where($args['userId']) + ); + } catch (\Slim\Exception\HttpSpecializedException $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage(), $ex->getCode()); + } catch (\Exception $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function SetPermissions(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + try { + User::checkPermission($request, User::PERMISSION_ADMIN); + $requestBody = $request->getParsedBody(); + $db = $this->getDatabase(); + $db->user_permissions() + ->where('user_id', $args['userId']) + ->delete(); + + $perms = []; + + foreach ($requestBody['permissions'] as $perm_id) { + $perms[] = array( + 'user_id' => $args['userId'], + 'permission_id' => $perm_id + ); + } + + $db->insert('user_permissions', $perms, 'batch'); + + return $this->EmptyApiResponse($response); + } catch (\Slim\Exception\HttpSpecializedException $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage(), $ex->getCode()); + } catch (\Exception $ex) { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/controllers/UsersController.php b/controllers/UsersController.php index 01894a66..5ebc38fa 100644 --- a/controllers/UsersController.php +++ b/controllers/UsersController.php @@ -2,11 +2,14 @@ namespace Grocy\Controllers; +use Grocy\Controllers\Users\User; + class UsersController extends BaseController { public function UsersList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - return $this->renderPage($response, 'users', [ + User::checkPermission($request, User::PERMISSION_READ_USER); + return $this->renderPage($response, 'users', [ 'users' => $this->getDatabase()->users()->orderBy('username') ]); } @@ -15,16 +18,30 @@ class UsersController extends BaseController { if ($args['userId'] == 'new') { - return $this->renderPage($response, 'userform', [ + User::checkPermission($request, User::PERMISSION_CREATE_USER); + return $this->renderPage($response, 'userform', [ 'mode' => 'create' ]); } else { - return $this->renderPage($response, 'userform', [ + if($args['userId'] == GROCY_USER_ID) + User::checkPermission($request, User::PERMISSION_EDIT_SELF); + else User::checkPermission($request, User::PERMISSION_EDIT_USER); + return $this->renderPage($response, 'userform', [ 'user' => $this->getDatabase()->users($args['userId']), 'mode' => 'edit' ]); } } + + public function PermissionList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + User::checkPermission($request, User::PERMISSION_READ_USER); + return $this->renderPage($response, 'userpermissions', [ + 'user' => $this->getDatabase()->users($args['userId']), + 'permissions' => $this->getDatabase()->uihelper_user_permissions() + ->where('parent IS NULL')->where('user_id', $args['userId']), + ]); + } } diff --git a/localization/en/permissions.po b/localization/en/permissions.po new file mode 100644 index 00000000..2de19502 --- /dev/null +++ b/localization/en/permissions.po @@ -0,0 +1,85 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" +"PO-Revision-Date: 2019-05-01T17:59:17+00:00\n" +"Last-Translator: \n" +"Language-Team: http://www.transifex.com/grocy/grocy/language/en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"X-Domain: grocy/permissions\n" + +msgid "ADMIN" +msgstr "" + +msgid "CREATE_USER" +msgstr "Create new users" + +msgid "EDIT_USER" +msgstr "Edit existing users (including passwords)" + +msgid "READ_USER" +msgstr "View user data" + +msgid "EDIT_SELF" +msgstr "Edit own user data, e.g. password and name" + +msgid "BATTERY_UNDO_TRACK_CHARGE_CYCLE" +msgstr "Batteries: undo tracking of charge cycles" + +msgid "BATTERY_TRACK_CHARGE_CYCLE" +msgstr "Batteries: track charge cycle" + +msgid "CHORE_TRACK" +msgstr "Chores: track execution" + +msgid "CHORE_TRACK_OTHERS" +msgstr "Chores: Track execution for others" + +msgid "CHORE_EDIT" +msgstr "Chores: Edit chore data" + +msgid "CHORE_UNDO" +msgstr "Chores: undo tracked execution" + +msgid "UPLOAD_FILE" +msgstr "Upload files, e.g. product images" + +msgid "DELETE_FILE" +msgstr "Delete (uploaded) files" + +msgid "MASTER_DATA_EDIT" +msgstr "Edit Master data (e.g. products)" + +msgid "TASKS_UNDO" +msgstr "Tasks: undo tracked execution" + +msgid "TASKS_MARK_COMPLETED" +msgstr "Tasks: mark as completed" + +msgid "STOCK_EDIT" +msgstr "Stock: edit entries" + +msgid "STOCK_TRANSFER" +msgstr "Stock: transfer products between locations" + +msgid "STOCK_CORRECTION" +msgstr "Stock: correct wrong entries" + +msgid "PRODUCT_CONSUME" +msgstr "Consume Products" + +msgid "PRODUCT_OPEN" +msgstr "Open products" + +msgid "PRODUCT_PURCHASE" +msgstr "Purchase new products and add them to stock" + +msgid "SHOPPINGLIST_ITEMS_ADD" +msgstr "Add items to shopping list" + +msgid "SHOPPINGLIST_ITEMS_DELETE" +msgstr "Remove items from shopping list" \ No newline at end of file diff --git a/localization/permissions.pot b/localization/permissions.pot new file mode 100644 index 00000000..1d265271 --- /dev/null +++ b/localization/permissions.pot @@ -0,0 +1,85 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: http://www.transifex.com/grocy/grocy/language/en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" +"PO-Revision-Date: 2019-05-01T17:59:17+00:00\n" +"Language: en\n" +"X-Domain: grocy/permissions\n" + +msgid "ADMIN" +msgstr "" + +msgid "CREATE_USER" +msgstr "" + +msgid "EDIT_USER" +msgstr "" + +msgid "READ_USER" +msgstr "" + +msgid "EDIT_SELF" +msgstr "" + +msgid "BATTERY_UNDO_TRACK_CHARGE_CYCLE" +msgstr "" + +msgid "BATTERY_TRACK_CHARGE_CYCLE" +msgstr "" + +msgid "CHORE_TRACK" +msgstr "" + +msgid "CHORE_TRACK_OTHERS" +msgstr "" + +msgid "CHORE_EDIT" +msgstr "" + +msgid "CHORE_UNDO" +msgstr "" + +msgid "UPLOAD_FILE" +msgstr "" + +msgid "DELETE_FILE" +msgstr "" + +msgid "MASTER_DATA_EDIT" +msgstr "" + +msgid "TASKS_UNDO" +msgstr "" + +msgid "TASKS_MARK_COMPLETED" +msgstr "" + +msgid "STOCK_EDIT" +msgstr "" + +msgid "STOCK_TRANSFER" +msgstr "" + +msgid "STOCK_CORRECTION" +msgstr "" + +msgid "PRODUCT_CONSUME" +msgstr "" + +msgid "PRODUCT_OPEN" +msgstr "" + +msgid "PRODUCT_PURCHASE" +msgstr "" + +msgid "SHOPPINGLIST_ITEMS_ADD" +msgstr "" + +msgid "SHOPPINGLIST_ITEMS_DELETE" +msgstr "" \ No newline at end of file diff --git a/localization/strings.pot b/localization/strings.pot index 109beff1..2d8b7f7a 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1849,3 +1849,45 @@ msgstr "" msgid "Clear filter" msgstr "" + +msgid "Permissions for user %s" +msgstr "" + +msgid "Are you sure you want to stop being an ADMIN?" +msgstr "" + +msgid "Permissions saved" +msgstr "" + +msgid "You are not allowed to view this page" +msgstr "" + +msgid "Page not found" +msgstr "" + +msgid "Unauthorized" +msgstr "" + +msgid "Error source" +msgstr "" + +msgid "Error message" +msgstr "" + +msgid "Stack trace" +msgstr "" + +msgid "This page does not exists" +msgstr "" + +msgid "You will be redirected to the default page in %s seconds" +msgstr "" + +msgid "Server error" +msgstr "" + +msgid "A server error occured while processing your request" +msgstr "" + +msgid "If you think this is a bug, please report it" +msgstr "" diff --git a/migrations/0111.sql b/migrations/0111.sql new file mode 100644 index 00000000..9b78a2fa --- /dev/null +++ b/migrations/0111.sql @@ -0,0 +1,109 @@ +CREATE TABLE user_permissions +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + permission_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + + UNIQUE (user_id, permission_id) +); + +CREATE TABLE permission_hierarchy +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + parent INTEGER NULL -- If the user has the parent permission, the user also has the child permission +); + +INSERT INTO permission_hierarchy + (name, parent) +VALUES + ('ADMIN', NULL); + +INSERT INTO user_permissions + (permission_id, user_id) +SELECT (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN'), id +FROM users; + +CREATE VIEW permission_tree +AS +WITH RECURSIVE perm AS ( + SELECT id AS root, id AS child, name, parent + FROM permission_hierarchy + UNION + SELECT perm.root, ph.id, ph.name, ph.id + FROM permission_hierarchy ph, perm + WHERE ph.parent = perm.child +) +SELECT root AS id, name AS name +FROM perm; + +CREATE VIEW user_permissions_resolved +AS +SELECT + u.id AS id, -- Dummy for LessQL + u.id AS user_id, + pt.name AS permission_name +FROM permission_tree pt, users u +WHERE pt.id IN (SELECT permission_id FROM user_permissions sub_up WHERE sub_up.user_id = u.id); + +CREATE VIEW uihelper_user_permissions +AS +SELECT + ph.id AS id, + u.id AS user_id, + ph.name AS permission_name, + ph.id AS permission_id, + (ph.name IN ( + SELECT pc.permission_name + FROM user_permissions_resolved pc + WHERE pc.user_id = u.id + ) + ) AS has_permission, + ph.parent AS parent +FROM users u, permission_hierarchy ph; + +INSERT INTO permission_hierarchy + (name, parent) +VALUES + ('CREATE_USER', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')); + +INSERT INTO permission_hierarchy + (name, parent) +VALUES + ('EDIT_USER', last_insert_rowid()); + +INSERT INTO permission_hierarchy + (name, parent) +VALUES + ('READ_USER', last_insert_rowid()), + ('EDIT_SELF', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')); + +INSERT INTO permission_hierarchy + (name, parent) +VALUES + -- Batteries + ('BATTERY_UNDO_TRACK_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('BATTERY_TRACK_CHARGE_CYCLE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- Chores + ('CHORE_TRACK', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('CHORE_TRACK_OTHERS', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('CHORE_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('CHORE_UNDO', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- Files + ('UPLOAD_FILE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('DELETE_FILE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- master data + ('MASTER_DATA_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- Tasks + ('TASKS_UNDO', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('TASKS_MARK_COMPLETED', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- Stock / Products + ('STOCK_EDIT', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('STOCK_TRANSFER', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('STOCK_CORRECTION', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('PRODUCT_PURCHASE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('PRODUCT_CONSUME', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('PRODUCT_OPEN', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + -- shopping list + ('SHOPPINGLIST_ITEMS_ADD', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')), + ('SHOPPINGLIST_ITEMS_DELETE', (SELECT id FROM permission_hierarchy WHERE name = 'ADMIN')); diff --git a/public/css/grocy.css b/public/css/grocy.css index 295aea0b..e259138f 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -581,3 +581,7 @@ canvas.drawingBuffer { } } +.not-allowed { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/public/js/grocy.js b/public/js/grocy.js index 275cd6fe..4f45cb0f 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -662,3 +662,11 @@ $.extend(true, $.fn.dataTable.defaults, { }); } }); + +$(Grocy.UserPermissions).each(function (index, item) +{ + if(item.has_permission == 0) + { + $('.permission-'+item.permission_name).addClass('disabled').addClass('not-allowed'); + } +}); diff --git a/public/viewjs/userpermissions.js b/public/viewjs/userpermissions.js new file mode 100644 index 00000000..04e60572 --- /dev/null +++ b/public/viewjs/userpermissions.js @@ -0,0 +1,42 @@ +$('input.permission-cb').click( + function () { + check_hierachy(this.checked, this.name); + } +); + +function check_hierachy(checked, name) { + var disabled = checked; + $('#permission-sub-' + name).find('input.permission-cb') + .prop('checked', disabled) + .attr('disabled', disabled); +} + +$('#permission-save').click( + function () { + var permission_list = $('input.permission-cb') + .filter(function () { + return $(this).prop('checked') && !$(this).attr('disabled'); + }).map(function () { + return $(this).data('perm-id'); + }).toArray(); + Grocy.Api.Put('users/' + Grocy.EditObjectId + '/permissions', { + 'permissions': permission_list, + }, function (result) { + toastr.success(__t("Permissions saved")); + }, function (xhr) { + toastr.error(__t(JSON.parse(xhr.response).error_message)); + } + ); + } +); + +if (Grocy.EditObjectId == Grocy.UserId) { + $('input.permission-cb[name=ADMIN]').click(function () { + if (!this.checked) { + if (!confirm(__t('Are you sure you want to stop being an ADMIN?'))) { + this.checked = true; + check_hierachy(this.checked, this.name); + } + } + }) +} diff --git a/routes.php b/routes.php index 11117c72..e786050e 100644 --- a/routes.php +++ b/routes.php @@ -33,8 +33,9 @@ $app->group('', function(RouteCollectorProxy $group) // User routes $group->get('/users', '\Grocy\Controllers\UsersController:UsersList'); $group->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm'); + $group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList'); - // Stock routes + // Stock routes if (GROCY_FEATURE_FLAG_STOCK) { $group->get('/stockoverview', '\Grocy\Controllers\StockController:Overview'); @@ -168,8 +169,11 @@ $app->group('/api', function(RouteCollectorProxy $group) $group->post('/users', '\Grocy\Controllers\UsersApiController:CreateUser'); $group->put('/users/{userId}', '\Grocy\Controllers\UsersApiController:EditUser'); $group->delete('/users/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser'); + $group->get('/users/{userId}/permissions', '\Grocy\Controllers\UsersApiController:ListPermissions'); + $group->post('/users/{userId}/permissions', '\Grocy\Controllers\UsersApiController:AddPermission'); + $group->put('/users/{userId}/permissions', '\Grocy\Controllers\UsersApiController:SetPermissions'); - // User + // User $group->get('/user/settings', '\Grocy\Controllers\UsersApiController:GetUserSettings'); $group->get('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:GetUserSetting'); $group->put('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:SetUserSetting'); diff --git a/services/LocalizationService.php b/services/LocalizationService.php index abbe93e0..2a052a6e 100644 --- a/services/LocalizationService.php +++ b/services/LocalizationService.php @@ -60,6 +60,7 @@ class LocalizationService $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/stock_transaction_types.pot')); $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/strings.pot')); $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/userfield_types.pot')); + $this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/permissions.pot')); if (GROCY_MODE !== 'production') { @@ -91,6 +92,10 @@ class LocalizationService { $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/userfield_types.po")); } + if (file_exists(__DIR__ . "/../localization/$culture/permissions.po")) + { + $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/permissions.po")); + } if (GROCY_MODE !== 'production' && file_exists(__DIR__ . "/../localization/$culture/demo_data.po")) { $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/demo_data.po")); diff --git a/services/UsersService.php b/services/UsersService.php index 42f298d2..b6b2face 100644 --- a/services/UsersService.php +++ b/services/UsersService.php @@ -12,7 +12,17 @@ class UsersService extends BaseService 'last_name' => $lastName, 'password' => password_hash($password, PASSWORD_DEFAULT) )); - return $newUserRow->save(); + $newUserRow = $newUserRow->save(); + $permList = array(); + foreach ($this->getDatabase()->permission_hierarchy()->where('name', GROCY_DEFAULT_PERMISSIONS)->fetchAll() as $perm) { + $permList[] = array( + 'user_id' => $newUserRow->id, + 'permission_id' => $perm->id + ); + } + $this->getDatabase()->user_permissions()->insert($permList); + + return $newUserRow; } public function EditUser(int $userId, string $username, string $firstName, string $lastName, string $password) diff --git a/views/batteries.blade.php b/views/batteries.blade.php index 9926c37d..8d2a1239 100644 --- a/views/batteries.blade.php +++ b/views/batteries.blade.php @@ -20,7 +20,7 @@
@@ -57,10 +57,10 @@ @foreach($batteries as $battery) - + - + diff --git a/views/batteriesjournal.blade.php b/views/batteriesjournal.blade.php index a4d3e479..bce9d714 100644 --- a/views/batteriesjournal.blade.php +++ b/views/batteriesjournal.blade.php @@ -50,7 +50,7 @@ @foreach($chargeCycles as $chargeCycleEntry) - + diff --git a/views/batteriesoverview.blade.php b/views/batteriesoverview.blade.php index 5da87a25..91aa97d2 100644 --- a/views/batteriesoverview.blade.php +++ b/views/batteriesoverview.blade.php @@ -69,7 +69,7 @@ @foreach($current as $currentBatteryEntry) - @@ -85,7 +85,7 @@ {{ $__t('Journal for this battery') }} - + {{ $__t('Edit battery') }}
diff --git a/views/choresjournal.blade.php b/views/choresjournal.blade.php index 70243079..8c10f939 100644 --- a/views/choresjournal.blade.php +++ b/views/choresjournal.blade.php @@ -53,7 +53,7 @@ @foreach($choresLog as $choreLogEntry) - + diff --git a/views/choresoverview.blade.php b/views/choresoverview.blade.php index d83a80c9..77e7ba1b 100644 --- a/views/choresoverview.blade.php +++ b/views/choresoverview.blade.php @@ -95,7 +95,7 @@ @foreach($currentChores as $curentChoreEntry) - @@ -111,7 +111,7 @@ {{ $__t('Journal for this chore') }} - + {{ $__t('Edit chore') }} diff --git a/views/components/userpermission_select.blade.php b/views/components/userpermission_select.blade.php new file mode 100644 index 00000000..7b3d8b14 --- /dev/null +++ b/views/components/userpermission_select.blade.php @@ -0,0 +1,15 @@ + +
+
    + @foreach($perm->uihelper_user_permissionsList(array('user_id' => $user->id))->via('parent') as $p) +
  • + @include('components.userpermission_select', array( + 'perm' => $p + )) +
  • + @endforeach +
+
diff --git a/views/errors/403.blade.php b/views/errors/403.blade.php new file mode 100644 index 00000000..acf3e29c --- /dev/null +++ b/views/errors/403.blade.php @@ -0,0 +1,11 @@ +@extends('errors.base') + +@section('title', $__t('Unauthorized')) + +@section('content') +
+
+
{{ $__t('You are not allowed to view this page') }}
+
+
+@stop diff --git a/views/errors/404.blade.php b/views/errors/404.blade.php new file mode 100644 index 00000000..42d34978 --- /dev/null +++ b/views/errors/404.blade.php @@ -0,0 +1,13 @@ +@extends('errors.base') + +@section('title', $__t('Page not found')) + +@section('content') + +
+
+
{{ $__t('This page does not exists') }}
+
{{ $__t('You will be redirected to the default page in %s seconds', '5') }}
+
+
+@stop diff --git a/views/errors/500.blade.php b/views/errors/500.blade.php new file mode 100644 index 00000000..ee62aed7 --- /dev/null +++ b/views/errors/500.blade.php @@ -0,0 +1,16 @@ +@extends('errors.base') + +@section('title', $__t('Server error')) + +@section('content') +
+
+
{{ $__t('A server error occured while processing your request') }}
+
+ {{ $__t('If you think this is a bug, please report it') }}
+ → https://github.com/grocy/grocy/issues +
+
+
+ @parent +@stop diff --git a/views/errors/base.blade.php b/views/errors/base.blade.php new file mode 100644 index 00000000..de5ae07e --- /dev/null +++ b/views/errors/base.blade.php @@ -0,0 +1,20 @@ +@extends('layout.default') + +@section('content') +
+
+
+
{{ $__t('Error source') }}
+
{!! $exception->getFile() !!}:{!! $exception->getLine() !!}
+
+
+
{{ $__t('Error message') }}
+
{!! $exception->getMessage() !!}
+
+
+
{{ $__t('Stack trace') }}
+
{!! $exception->getTraceAsString() !!}
+
+
+
+@stop diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 3872148f..eaddabe7 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -62,6 +62,7 @@ @if (GROCY_AUTHENTICATED) Grocy.UserSettings = {!! json_encode($userSettings) !!}; Grocy.UserId = {{ GROCY_USER_ID }}; + Grocy.UserPermissions = {!! json_encode($permissions) !!}; @else Grocy.UserSettings = { }; Grocy.UserId = -1; @@ -163,27 +164,27 @@ @if(GROCY_FEATURE_FLAG_STOCK) - - @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) - @endif - @endif @if(GROCY_FEATURE_FLAG_CHORES) - @endif @if(GROCY_FEATURE_FLAG_BATTERIES) -