[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 <bernd@berrnd.de>
This commit is contained in:
fipwmaqzufheoxq92ebc 2020-08-29 12:05:32 +02:00 committed by GitHub
parent f28697e5b4
commit b7d1b21f1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 930 additions and 67 deletions

View File

@ -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\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_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\chore_assignment_types.po localization\en_GB\chore_assignment_types.po
copy /Y localization\en\permissions.po localization\en_GB\permissions.po
popd popd

View File

@ -42,3 +42,9 @@ file_filter = localization/<lang>/userfield_types.po
source_file = localization/userfield_types.pot source_file = localization/userfield_types.pot
source_lang = en source_lang = en
type = PO type = PO
[grocy.permissions]
file_filter = localization/<lang>/permissions.po
source_file = localization/permissions.pot
source_lang = en
type = PO

View File

@ -65,6 +65,9 @@ if (!empty(GROCY_BASE_PATH))
// Add default middleware // Add default middleware
$app->addRoutingMiddleware(); $app->addRoutingMiddleware();
$app->addErrorMiddleware(true, false, false); $errorMiddleware = $app->addErrorMiddleware(true, false, false);
$errorMiddleware->setDefaultErrorHandler(
new \Grocy\Controllers\ExceptionController($app, $container)
);
$app->run(); $app->run();

View File

@ -82,6 +82,11 @@ Setting('DISABLE_BROWSER_BARCODE_CAMERA_SCANNING', false);
# Needs to be a number where Sunday = 0, Monday = 1 and so forth # Needs to be a number where Sunday = 0, Monday = 1 and so forth
Setting('MEAL_PLAN_FIRST_DAY_OF_WEEK', ''); 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 # Default user settings
# These settings can be changed per user, here the defaults # These settings can be changed per user, here the defaults

View File

@ -2,6 +2,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use \Grocy\Services\DatabaseService; use \Grocy\Services\DatabaseService;
use \Grocy\Services\ApplicationService; use \Grocy\Services\ApplicationService;
use \Grocy\Services\LocalizationService; use \Grocy\Services\LocalizationService;
@ -66,6 +67,11 @@ class BaseController
} }
$this->View->set('featureFlags', $constants); $this->View->set('featureFlags', $constants);
if (GROCY_AUTHENTICATED)
{
$this->View->set('permissions', User::PermissionList());
}
return $this->View->render($response, $page, $data); return $this->View->render($response, $page, $data);
} }

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class BatteriesApiController extends BaseApiController class BatteriesApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -11,6 +13,8 @@ class BatteriesApiController extends BaseApiController
public function TrackChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function TrackChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_BATTERY_TRACK_CHARGE_CYCLE);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -49,6 +53,8 @@ class BatteriesApiController extends BaseApiController
public function UndoChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function UndoChargeCycle(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_BATTERY_UNDO_TRACK_CHARGE_CYCLE);
try try
{ {
$this->ApiResponse($response, $this->getBatteriesService()->UndoChargeCycle($args['chargeCycleId'])); $this->ApiResponse($response, $this->getBatteriesService()->UndoChargeCycle($args['chargeCycleId']));

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class ChoresApiController extends BaseApiController class ChoresApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -15,6 +17,8 @@ class ChoresApiController extends BaseApiController
try try
{ {
User::checkPermission($request, User::PERMISSION_CHORE_TRACK);
$trackedTime = date('Y-m-d H:i:s'); $trackedTime = date('Y-m-d H:i:s');
if (array_key_exists('tracked_time', $requestBody) && (IsIsoDateTime($requestBody['tracked_time']) || IsIsoDate($requestBody['tracked_time']))) if (array_key_exists('tracked_time', $requestBody) && (IsIsoDateTime($requestBody['tracked_time']) || IsIsoDate($requestBody['tracked_time'])))
{ {
@ -26,6 +30,8 @@ class ChoresApiController extends BaseApiController
{ {
$doneBy = $requestBody['done_by']; $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); $choreExecutionId = $this->getChoresService()->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->ApiResponse($response, $this->getDatabase()->chores_log($choreExecutionId)); return $this->ApiResponse($response, $this->getDatabase()->chores_log($choreExecutionId));
@ -57,6 +63,8 @@ class ChoresApiController extends BaseApiController
{ {
try try
{ {
User::checkPermission($request, User::PERMISSION_CHORE_UNDO);
$this->ApiResponse($response, $this->getChoresService()->UndoChoreExecution($args['executionId'])); $this->ApiResponse($response, $this->getChoresService()->UndoChoreExecution($args['executionId']));
return $this->EmptyApiResponse($response); return $this->EmptyApiResponse($response);
} }
@ -70,6 +78,8 @@ class ChoresApiController extends BaseApiController
{ {
try try
{ {
User::checkPermission($request, User::PERMISSION_CHORE_EDIT);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
$choreId = null; $choreId = null;

View File

@ -0,0 +1,69 @@
<?php
namespace Grocy\Controllers;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Slim\Exception\HttpException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpNotFoundException;
use Throwable;
class ExceptionController extends BaseApiController
{
/**
* @var \Slim\App
*/
private $app;
public function __construct(\Slim\App $app, \DI\Container $container)
{
parent::__construct($container);
$this->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
]);
}
}

View File

@ -2,6 +2,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use \Grocy\Services\FilesService; use \Grocy\Services\FilesService;
class FilesApiController extends BaseApiController class FilesApiController extends BaseApiController
@ -15,6 +16,8 @@ class FilesApiController extends BaseApiController
{ {
try try
{ {
User::checkPermission($request, User::PERMISSION_UPLOAD_FILE);
if (IsValidFileName(base64_decode($args['fileName']))) if (IsValidFileName(base64_decode($args['fileName'])))
{ {
$fileName = base64_decode($args['fileName']); $fileName = base64_decode($args['fileName']);
@ -97,6 +100,8 @@ class FilesApiController extends BaseApiController
{ {
try try
{ {
User::checkPermission($request, User::PERMISSION_DELETE_FILE);
if (IsValidFileName(base64_decode($args['fileName']))) if (IsValidFileName(base64_decode($args['fileName'])))
{ {
$fileName = base64_decode($args['fileName']); $fileName = base64_decode($args['fileName']);

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class GenericEntityApiController extends BaseApiController class GenericEntityApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -66,6 +68,8 @@ class GenericEntityApiController extends BaseApiController
public function AddObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity'])) if ($this->IsValidEntity($args['entity']))
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -97,6 +101,8 @@ class GenericEntityApiController extends BaseApiController
public function EditObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function EditObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity'])) if ($this->IsValidEntity($args['entity']))
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -126,6 +132,8 @@ class GenericEntityApiController extends BaseApiController
public function DeleteObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function DeleteObject(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity'])) if ($this->IsValidEntity($args['entity']))
{ {
$row = $this->getDatabase()->{$args['entity']}($args['objectId']); $row = $this->getDatabase()->{$args['entity']}($args['objectId']);
@ -141,6 +149,7 @@ class GenericEntityApiController extends BaseApiController
public function SearchObjects(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) 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 try
@ -172,6 +181,8 @@ class GenericEntityApiController extends BaseApiController
public function SetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function SetUserfields(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class RecipesApiController extends BaseApiController class RecipesApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -11,6 +13,8 @@ class RecipesApiController extends BaseApiController
public function AddNotFulfilledProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddNotFulfilledProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
$excludedProductIds = null; $excludedProductIds = null;
@ -25,6 +29,8 @@ class RecipesApiController extends BaseApiController
public function ConsumeRecipe(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ConsumeRecipe(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_PRODUCT_CONSUME);
try try
{ {
$this->getRecipesService()->ConsumeRecipe($args['recipeId']); $this->getRecipesService()->ConsumeRecipe($args['recipeId']);

View File

@ -2,6 +2,7 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use \Grocy\Services\StockService; use \Grocy\Services\StockService;
class StockApiController extends BaseApiController class StockApiController extends BaseApiController
@ -62,6 +63,8 @@ class StockApiController extends BaseApiController
public function AddProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_PRODUCT_PURCHASE);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -136,6 +139,8 @@ class StockApiController extends BaseApiController
public function EditStockEntry(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function EditStockEntry(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_STOCK_EDIT);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -185,6 +190,8 @@ class StockApiController extends BaseApiController
public function TransferProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function TransferProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_STOCK_TRANSFER);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -239,6 +246,8 @@ class StockApiController extends BaseApiController
public function ConsumeProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ConsumeProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_PRODUCT_CONSUME);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
$result = null; $result = null;
@ -310,6 +319,8 @@ class StockApiController extends BaseApiController
public function InventoryProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function InventoryProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -372,6 +383,8 @@ class StockApiController extends BaseApiController
public function OpenProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function OpenProduct(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_PRODUCT_OPEN);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -439,6 +452,8 @@ class StockApiController extends BaseApiController
public function AddMissingProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddMissingProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
try try
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -460,6 +475,8 @@ class StockApiController extends BaseApiController
public function ClearShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ClearShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
try try
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -482,6 +499,8 @@ class StockApiController extends BaseApiController
public function AddProductToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function AddProductToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
try try
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -523,6 +542,8 @@ class StockApiController extends BaseApiController
public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
try try
{ {
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
@ -559,6 +580,8 @@ class StockApiController extends BaseApiController
public function ExternalBarcodeLookup(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ExternalBarcodeLookup(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
try try
{ {
$addFoundProduct = false; $addFoundProduct = false;
@ -577,6 +600,8 @@ class StockApiController extends BaseApiController
public function UndoBooking(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function UndoBooking(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
try try
{ {
$this->ApiResponse($response, $this->getStockService()->UndoBooking($args['bookingId'])); $this->ApiResponse($response, $this->getStockService()->UndoBooking($args['bookingId']));
@ -590,6 +615,8 @@ class StockApiController extends BaseApiController
public function UndoTransaction(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function UndoTransaction(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
try try
{ {
$this->ApiResponse($response, $this->getStockService()->UndoTransaction($args['transactionId'])); $this->ApiResponse($response, $this->getStockService()->UndoTransaction($args['transactionId']));

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class TasksApiController extends BaseApiController class TasksApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -16,6 +18,8 @@ class TasksApiController extends BaseApiController
public function MarkTaskAsCompleted(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function MarkTaskAsCompleted(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_TASKS_MARK_COMPLETED);
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -37,6 +41,8 @@ class TasksApiController extends BaseApiController
public function UndoTask(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function UndoTask(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_TASKS_UNDO);
try try
{ {
$this->getTasksService()->UndoTask($args['taskId']); $this->getTasksService()->UndoTask($args['taskId']);

View File

@ -0,0 +1,15 @@
<?php
namespace Grocy\Controllers\Users;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpForbiddenException;
use Throwable;
class PermissionMissingException extends HttpForbiddenException
{
public function __construct(ServerRequestInterface $request, string $permission, ?Throwable $previous = null)
{
parent::__construct($request, 'Permission missing: ' . $permission, $previous);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Grocy\Controllers\Users;
use Grocy\Services\DatabaseService;
use LessQL\Result;
class User
{
const PERMISSION_ADMIN = 'ADMIN';
const PERMISSION_CREATE_USER = 'CREATE_USER';
const PERMISSION_EDIT_USER = 'EDIT_USER';
const PERMISSION_READ_USER = 'READ_USER';
const PERMISSION_EDIT_SELF = 'EDIT_SELF';
const PERMISSION_BATTERY_UNDO_TRACK_CHARGE_CYCLE = 'BATTERY_UNDO_TRACK_CHARGE_CYCLE';
const PERMISSION_BATTERY_TRACK_CHARGE_CYCLE = 'BATTERY_TRACK_CHARGE_CYCLE';
const PERMISSION_CHORE_TRACK = 'CHORE_TRACK';
const PERMISSION_CHORE_TRACK_OTHERS = 'CHORE_TRACK_OTHERS';
const PERMISSION_CHORE_EDIT = 'CHORE_EDIT';
const PERMISSION_CHORE_UNDO = 'CHORE_UNDO';
const PERMISSION_UPLOAD_FILE = 'UPLOAD_FILE';
const PERMISSION_DELETE_FILE = 'DELETE_FILE';
const PERMISSION_MASTER_DATA_EDIT = 'MASTER_DATA_EDIT';
const PERMISSION_TASKS_UNDO = 'TASKS_UNDO';
const PERMISSION_TASKS_MARK_COMPLETED = 'TASKS_MARK_COMPLETED';
const PERMISSION_STOCK_TRANSFER = 'STOCK_TRANSFER';
const PERMISSION_STOCK_EDIT = 'STOCK_EDIT';
const PERMISSION_PRODUCT_CONSUME = 'PRODUCT_CONSUME';
const PERMISSION_STOCK_CORRECTION = 'STOCK_CORRECTION';
const PERMISSION_PRODUCT_OPEN = 'PRODUCT_OPEN';
const PERMISSION_SHOPPINGLIST_ITEMS_ADD = 'SHOPPINGLIST_ITEMS_ADD';
const PERMISSION_SHOPPINGLIST_ITEMS_DELETE = 'SHOPPINGLIST_ITEMS_DELETE';
const PERMISSION_PRODUCT_PURCHASE = 'PRODUCT_PURCHASE';
/**
* @var \LessQL\Database|null
*/
protected $db;
public function __construct()
{
$this->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();
}
}

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class UsersApiController extends BaseApiController class UsersApiController extends BaseApiController
{ {
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
@ -11,6 +13,7 @@ class UsersApiController extends BaseApiController
public function GetUsers(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function GetUsers(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_READ_USER);
try try
{ {
return $this->ApiResponse($response, $this->getUsersService()->GetUsersAsDto()); 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) 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(); $requestBody = $request->getParsedBody();
try try
@ -43,6 +47,7 @@ class UsersApiController extends BaseApiController
public function DeleteUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function DeleteUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_EDIT_USER);
try try
{ {
$this->getUsersService()->DeleteUser($args['userId']); $this->getUsersService()->DeleteUser($args['userId']);
@ -56,6 +61,11 @@ class UsersApiController extends BaseApiController
public function EditUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function EditUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if ($args['userId'] == GROCY_USER_ID) {
User::checkPermission($request, User::PERMISSION_EDIT_SELF);
} else {
User::checkPermission($request, User::PERMISSION_EDIT_USER);
}
$requestBody = $request->getParsedBody(); $requestBody = $request->getParsedBody();
try try
@ -108,4 +118,66 @@ class UsersApiController extends BaseApiController
return $this->GenericErrorResponse($response, $ex->getMessage()); 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());
}
}
} }

View File

@ -2,10 +2,13 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class UsersController extends BaseController class UsersController extends BaseController
{ {
public function UsersList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function UsersList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
User::checkPermission($request, User::PERMISSION_READ_USER);
return $this->renderPage($response, 'users', [ return $this->renderPage($response, 'users', [
'users' => $this->getDatabase()->users()->orderBy('username') 'users' => $this->getDatabase()->users()->orderBy('username')
]); ]);
@ -15,16 +18,30 @@ class UsersController extends BaseController
{ {
if ($args['userId'] == 'new') if ($args['userId'] == 'new')
{ {
User::checkPermission($request, User::PERMISSION_CREATE_USER);
return $this->renderPage($response, 'userform', [ return $this->renderPage($response, 'userform', [
'mode' => 'create' 'mode' => 'create'
]); ]);
} }
else else
{ {
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', [ return $this->renderPage($response, 'userform', [
'user' => $this->getDatabase()->users($args['userId']), 'user' => $this->getDatabase()->users($args['userId']),
'mode' => 'edit' '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']),
]);
}
} }

View File

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

View File

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

View File

@ -1849,3 +1849,45 @@ msgstr ""
msgid "Clear filter" msgid "Clear filter"
msgstr "" 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 ""

109
migrations/0111.sql Normal file
View File

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

View File

@ -581,3 +581,7 @@ canvas.drawingBuffer {
} }
} }
.not-allowed {
pointer-events: none;
opacity: 0.5;
}

View File

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

View File

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

View File

@ -33,6 +33,7 @@ $app->group('', function(RouteCollectorProxy $group)
// User routes // User routes
$group->get('/users', '\Grocy\Controllers\UsersController:UsersList'); $group->get('/users', '\Grocy\Controllers\UsersController:UsersList');
$group->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm'); $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) if (GROCY_FEATURE_FLAG_STOCK)
@ -168,6 +169,9 @@ $app->group('/api', function(RouteCollectorProxy $group)
$group->post('/users', '\Grocy\Controllers\UsersApiController:CreateUser'); $group->post('/users', '\Grocy\Controllers\UsersApiController:CreateUser');
$group->put('/users/{userId}', '\Grocy\Controllers\UsersApiController:EditUser'); $group->put('/users/{userId}', '\Grocy\Controllers\UsersApiController:EditUser');
$group->delete('/users/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser'); $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', '\Grocy\Controllers\UsersApiController:GetUserSettings');

View File

@ -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/stock_transaction_types.pot'));
$this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/strings.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/userfield_types.pot'));
$this->Pot = $this->Pot->mergeWith(Translations::fromPoFile(__DIR__ . '/../localization/permissions.pot'));
if (GROCY_MODE !== 'production') if (GROCY_MODE !== 'production')
{ {
@ -91,6 +92,10 @@ class LocalizationService
{ {
$this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/userfield_types.po")); $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")) 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")); $this->Po = $this->Po->mergeWith(Translations::fromPoFile(__DIR__ . "/../localization/$culture/demo_data.po"));

View File

@ -12,7 +12,17 @@ class UsersService extends BaseService
'last_name' => $lastName, 'last_name' => $lastName,
'password' => password_hash($password, PASSWORD_DEFAULT) '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) public function EditUser(int $userId, string $username, string $firstName, string $lastName, string $password)

View File

@ -20,7 +20,7 @@
<hr> <hr>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-xs-12 col-md-2 col-xl-1"> <div class="col-xs-12 col-md-2 col-xl-1">
<a class="btn btn-primary btn-sm responsive-button w-100 mb-3" href="{{ $U('/battery/new') }}"> <a class="btn btn-primary btn-sm responsive-button w-100 mb-3 permission-MASTER_DATA_EDIT" href="{{ $U('/battery/new') }}">
{{ $__t('Add') }} {{ $__t('Add') }}
</a> </a>
</div> </div>
@ -57,10 +57,10 @@
@foreach($batteries as $battery) @foreach($batteries as $battery)
<tr> <tr>
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/battery/') }}{{ $battery->id }}"> <a class="btn btn-info btn-sm permission-MASTER_DATA_EDIT" href="{{ $U('/battery/') }}{{ $battery->id }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm battery-delete-button" href="#" data-battery-id="{{ $battery->id }}" data-battery-name="{{ $battery->name }}"> <a class="btn btn-danger btn-sm battery-delete-button permission-MASTER_DATA_EDIT" href="#" data-battery-id="{{ $battery->id }}" data-battery-name="{{ $battery->name }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>

View File

@ -50,7 +50,7 @@
@foreach($chargeCycles as $chargeCycleEntry) @foreach($chargeCycles as $chargeCycleEntry)
<tr id="charge-cycle-{{ $chargeCycleEntry->id }}-row" class="@if($chargeCycleEntry->undone == 1) text-muted @endif"> <tr id="charge-cycle-{{ $chargeCycleEntry->id }}-row" class="@if($chargeCycleEntry->undone == 1) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-sm undo-battery-execution-button @if($chargeCycleEntry->undone == 1) disabled @endif" href="#" data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $__t('Undo charge cycle') }}"> <a class="btn btn-secondary btn-sm undo-battery-execution-button @if($chargeCycleEntry->undone == 1) disabled @endif permission-BATTERY_UNDO_TRACK_CHARGE_CYCLE" href="#" data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $__t('Undo charge cycle') }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</td> </td>

View File

@ -69,7 +69,7 @@
@foreach($current as $currentBatteryEntry) @foreach($current as $currentBatteryEntry)
<tr id="battery-{{ $currentBatteryEntry->battery_id }}-row" class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) table-warning @endif"> <tr id="battery-{{ $currentBatteryEntry->battery_id }}-row" class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->charge_interval_days > 0 && $currentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm track-charge-cycle-button" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Track charge cycle of battery %s', FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name) }}" <a class="btn btn-success btn-sm track-charge-cycle-button permission-BATTERY_TRACK_CHARGE_CYCLE" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Track charge cycle of battery %s', FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name) }}"
data-battery-id="{{ $currentBatteryEntry->battery_id }}" data-battery-id="{{ $currentBatteryEntry->battery_id }}"
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name }}"> data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name }}">
<i class="fas fa-fire"></i> <i class="fas fa-fire"></i>
@ -85,7 +85,7 @@
<a class="dropdown-item" type="button" href="{{ $U('/batteriesjournal?battery=') }}{{ $currentBatteryEntry->battery_id }}"> <a class="dropdown-item" type="button" href="{{ $U('/batteriesjournal?battery=') }}{{ $currentBatteryEntry->battery_id }}">
<span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Journal for this battery') }}</span> <span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Journal for this battery') }}</span>
</a> </a>
<a class="dropdown-item" type="button" href="{{ $U('/battery/') }}{{ $currentBatteryEntry->battery_id }}"> <a class="dropdown-item permission-MASTER_DATA_EDIT" type="button" href="{{ $U('/battery/') }}{{ $currentBatteryEntry->battery_id }}">
<span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit battery') }}</span> <span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit battery') }}</span>
</a> </a>
</div> </div>

View File

@ -53,7 +53,7 @@
@foreach($choresLog as $choreLogEntry) @foreach($choresLog as $choreLogEntry)
<tr id="chore-execution-{{ $choreLogEntry->id }}-row" class="@if($choreLogEntry->undone == 1) text-muted @endif"> <tr id="chore-execution-{{ $choreLogEntry->id }}-row" class="@if($choreLogEntry->undone == 1) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-sm undo-chore-execution-button @if($choreLogEntry->undone == 1) disabled @endif" href="#" data-execution-id="{{ $choreLogEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $__t('Undo chore execution') }}"> <a class="btn btn-secondary btn-sm undo-chore-execution-button permission-CHORE_UNDO @if($choreLogEntry->undone == 1) disabled @endif" href="#" data-execution-id="{{ $choreLogEntry->id }}" data-toggle="tooltip" data-placement="left" title="{{ $__t('Undo chore execution') }}">
<i class="fas fa-undo"></i> <i class="fas fa-undo"></i>
</a> </a>
</td> </td>

View File

@ -95,7 +95,7 @@
@foreach($currentChores as $curentChoreEntry) @foreach($currentChores as $curentChoreEntry)
<tr id="chore-{{ $curentChoreEntry->chore_id }}-row" class="@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) table-warning @endif"> <tr id="chore-{{ $curentChoreEntry->chore_id }}-row" class="@if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_PERIOD_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) table-warning @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm track-chore-button" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Track execution of chore %s', FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name) }}" <a class="btn btn-success btn-sm track-chore-button permission-CHORE_TRACK" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Track execution of chore %s', FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name) }}"
data-chore-id="{{ $curentChoreEntry->chore_id }}" data-chore-id="{{ $curentChoreEntry->chore_id }}"
data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}"> data-chore-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
@ -111,7 +111,7 @@
<a class="dropdown-item" type="button" href="{{ $U('/choresjournal?chore=') }}{{ $curentChoreEntry->chore_id }}"> <a class="dropdown-item" type="button" href="{{ $U('/choresjournal?chore=') }}{{ $curentChoreEntry->chore_id }}">
<span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Journal for this chore') }}</span> <span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Journal for this chore') }}</span>
</a> </a>
<a class="dropdown-item" type="button" href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}"> <a class="dropdown-item permission-MASTER_DATA_EDIT" type="button" href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}">
<span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit chore') }}</span> <span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit chore') }}</span>
</a> </a>
</div> </div>

View File

@ -0,0 +1,15 @@
<label>
<input type="checkbox" name="{{ $perm->permission_name }}" class="permission-cb" data-perm-id="{{ $perm->permission_id }}" @if($perm->has_permission) checked @endif autocomplete="off">
{{ $__t($perm->permission_name) }}
</label>
<div id="permission-sub-{{ $perm->permission_name }}">
<ul>
@foreach($perm->uihelper_user_permissionsList(array('user_id' => $user->id))->via('parent') as $p)
<li>
@include('components.userpermission_select', array(
'perm' => $p
))
</li>
@endforeach
</ul>
</div>

View File

@ -0,0 +1,11 @@
@extends('errors.base')
@section('title', $__t('Unauthorized'))
@section('content')
<div class="row">
<div class="col">
<div class="alert alert-danger">{{ $__t('You are not allowed to view this page') }}</div>
</div>
</div>
@stop

View File

@ -0,0 +1,13 @@
@extends('errors.base')
@section('title', $__t('Page not found'))
@section('content')
<meta http-equiv="refresh" content="5;url={{$U('/')}}">
<div class="row">
<div class="col">
<div class="alert alert-danger">{{ $__t('This page does not exists') }}</div>
<div>{{ $__t('You will be redirected to the default page in %s seconds', '5') }}</div>
</div>
</div>
@stop

View File

@ -0,0 +1,16 @@
@extends('errors.base')
@section('title', $__t('Server error'))
@section('content')
<div class="row">
<div class="col">
<div class="alert alert-danger">{{ $__t('A server error occured while processing your request') }}</div>
<div class="alert alert-warning">
{{ $__t('If you think this is a bug, please report it') }}<br>
&rarr; <a target="_blank" href="https://github.com/grocy/grocy/issues">https://github.com/grocy/grocy/issues</a>
</div>
</div>
</div>
@parent
@stop

View File

@ -0,0 +1,20 @@
@extends('layout.default')
@section('content')
<div class="row">
<div class="col">
<div>
<h6>{{ $__t('Error source') }}</h6>
<pre><code>{!! $exception->getFile() !!}:{!! $exception->getLine() !!}</code></pre>
</div>
<div>
<h6>{{ $__t('Error message') }}</h6>
<pre><code>{!! $exception->getMessage() !!}</code></pre>
</div>
<div>
<h6>{{ $__t('Stack trace') }}</h6>
<pre><code>{!! $exception->getTraceAsString() !!}</code></pre>
</div>
</div>
</div>
@stop

View File

@ -62,6 +62,7 @@
@if (GROCY_AUTHENTICATED) @if (GROCY_AUTHENTICATED)
Grocy.UserSettings = {!! json_encode($userSettings) !!}; Grocy.UserSettings = {!! json_encode($userSettings) !!};
Grocy.UserId = {{ GROCY_USER_ID }}; Grocy.UserId = {{ GROCY_USER_ID }};
Grocy.UserPermissions = {!! json_encode($permissions) !!};
@else @else
Grocy.UserSettings = { }; Grocy.UserSettings = { };
Grocy.UserId = -1; Grocy.UserId = -1;
@ -163,27 +164,27 @@
@if(GROCY_FEATURE_FLAG_STOCK) @if(GROCY_FEATURE_FLAG_STOCK)
<div class="nav-item-divider"></div> <div class="nav-item-divider"></div>
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Purchase') }}" data-nav-for-page="purchase"> <li class="nav-item nav-item-sidebar permission-PRODUCT_PURCHASE" data-toggle="tooltip" data-placement="right" title="{{ $__t('Purchase') }}" data-nav-for-page="purchase">
<a class="nav-link discrete-link" href="{{ $U('/purchase') }}"> <a class="nav-link discrete-link" href="{{ $U('/purchase') }}">
<i class="fas fa-shopping-cart"></i> <i class="fas fa-shopping-cart"></i>
<span class="nav-link-text">{{ $__t('Purchase') }}</span> <span class="nav-link-text">{{ $__t('Purchase') }}</span>
</a> </a>
</li> </li>
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume') }}" data-nav-for-page="consume"> <li class="nav-item nav-item-sidebar permission-PRODUCT_CONSUME" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume') }}" data-nav-for-page="consume">
<a class="nav-link discrete-link" href="{{ $U('/consume') }}"> <a class="nav-link discrete-link" href="{{ $U('/consume') }}">
<i class="fas fa-utensils"></i> <i class="fas fa-utensils"></i>
<span class="nav-link-text">{{ $__t('Consume') }}</span> <span class="nav-link-text">{{ $__t('Consume') }}</span>
</a> </a>
</li> </li>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Transfer') }}" data-nav-for-page="transfer"> <li class="nav-item nav-item-sidebar permission-STOCK_TRANSFER" data-toggle="tooltip" data-placement="right" title="{{ $__t('Transfer') }}" data-nav-for-page="transfer">
<a class="nav-link discrete-link" href="{{ $U('/transfer') }}"> <a class="nav-link discrete-link" href="{{ $U('/transfer') }}">
<i class="fas fa-exchange-alt"></i> <i class="fas fa-exchange-alt"></i>
<span class="nav-link-text">{{ $__t('Transfer') }}</span> <span class="nav-link-text">{{ $__t('Transfer') }}</span>
</a> </a>
</li> </li>
@endif @endif
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Inventory') }}" data-nav-for-page="inventory"> <li class="nav-item nav-item-sidebar permission-STOCK_CORRECTION" data-toggle="tooltip" data-placement="right" title="{{ $__t('Inventory') }}" data-nav-for-page="inventory">
<a class="nav-link discrete-link" href="{{ $U('/inventory') }}"> <a class="nav-link discrete-link" href="{{ $U('/inventory') }}">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
<span class="nav-link-text">{{ $__t('Inventory') }}</span> <span class="nav-link-text">{{ $__t('Inventory') }}</span>
@ -191,7 +192,7 @@
</li> </li>
@endif @endif
@if(GROCY_FEATURE_FLAG_CHORES) @if(GROCY_FEATURE_FLAG_CHORES)
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Chore tracking') }}" data-nav-for-page="choretracking"> <li class="nav-item nav-item-sidebar permission-CHORE_TRACK_OTHERS" data-toggle="tooltip" data-placement="right" title="{{ $__t('Chore tracking') }}" data-nav-for-page="choretracking">
<a class="nav-link discrete-link" href="{{ $U('/choretracking') }}"> <a class="nav-link discrete-link" href="{{ $U('/choretracking') }}">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span class="nav-link-text">{{ $__t('Chore tracking') }}</span> <span class="nav-link-text">{{ $__t('Chore tracking') }}</span>
@ -199,7 +200,7 @@
</li> </li>
@endif @endif
@if(GROCY_FEATURE_FLAG_BATTERIES) @if(GROCY_FEATURE_FLAG_BATTERIES)
<li class="nav-item nav-item-sidebar" data-toggle="tooltip" data-placement="right" title="{{ $__t('Battery tracking') }}" data-nav-for-page="batterytracking"> <li class="nav-item nav-item-sidebar permission-BATTERY_TRACK_CHARGE_CYCLE" data-toggle="tooltip" data-placement="right" title="{{ $__t('Battery tracking') }}" data-nav-for-page="batterytracking">
<a class="nav-link discrete-link" href="{{ $U('/batterytracking') }}"> <a class="nav-link discrete-link" href="{{ $U('/batterytracking') }}">
<i class="fas fa-fire"></i> <i class="fas fa-fire"></i>
<span class="nav-link-text">{{ $__t('Battery tracking') }}</span> <span class="nav-link-text">{{ $__t('Battery tracking') }}</span>
@ -417,7 +418,7 @@
@endif @endif
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@if(GROCY_SHOW_AUTH_VIEWS) @if(GROCY_SHOW_AUTH_VIEWS)
<a class="dropdown-item discrete-link" href="{{ $U('/users') }}"><i class="fas fa-users"></i>&nbsp;{{ $__t('Manage users') }}</a> <a class="dropdown-item discrete-link permission-READ_USER" href="{{ $U('/users') }}"><i class="fas fa-users"></i>&nbsp;{{ $__t('Manage users') }}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $__t('Manage API keys') }}</a> <a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $__t('Manage API keys') }}</a>
@endif @endif

View File

@ -121,14 +121,14 @@
@foreach($currentStock as $currentStockEntry) @foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif ($currentStockEntry->product_missing) table-info @endif"> <tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif ($currentStockEntry->product_missing) table-info @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1 || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}" <a class="permission-PRODUCT_CONSUME btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount < 1 || $currentStockEntry->enable_tare_weight_handling == 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . $currentStockEntry->qu_unit_name, $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="1"> data-consume-amount="1">
<i class="fas fa-utensils"></i> 1 <i class="fas fa-utensils"></i> 1
</a> </a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="d-none d-sm-inline-block btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}" <a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="permission-PRODUCT_CONSUME d-none d-sm-inline-block btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
@ -156,22 +156,22 @@
data-consume-amount="{{ $currentStockEntry->amount }}"> data-consume-amount="{{ $currentStockEntry->amount }}">
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}</span> <span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume all %s which are currently in stock', $currentStockEntry->product_name) }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" type="button" href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $currentStockEntry->product_id ) }}"> <a class="dropdown-item show-as-dialog-link permission-SHOPPINGLIST_ITEMS_ADD" type="button" href="{{ $U('/shoppinglistitem/new?embedded&updateexistingproduct&product=' . $currentStockEntry->product_id ) }}">
<span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Add to shopping list') }}</span> <span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Add to shopping list') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item show-as-dialog-link" type="button" href="{{ $U('/purchase?embedded&product=' . $currentStockEntry->product_id ) }}"> <a class="dropdown-item show-as-dialog-link permission-PRODUCT_PURCHASE" type="button" href="{{ $U('/purchase?embedded&product=' . $currentStockEntry->product_id ) }}">
<span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Purchase') }}</span> <span class="dropdown-item-icon"><i class="fas fa-shopping-cart"></i></span> <span class="dropdown-item-text">{{ $__t('Purchase') }}</span>
</a> </a>
<a class="dropdown-item show-as-dialog-link" type="button" href="{{ $U('/consume?embedded&product=' . $currentStockEntry->product_id ) }}"> <a class="dropdown-item show-as-dialog-link permission-PRODUCT_CONSUME" type="button" href="{{ $U('/consume?embedded&product=' . $currentStockEntry->product_id ) }}">
<span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume') }}</span> <span class="dropdown-item-icon"><i class="fas fa-utensils"></i></span> <span class="dropdown-item-text">{{ $__t('Consume') }}</span>
</a> </a>
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<a class="dropdown-item show-as-dialog-link @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="{{ $U('/transfer?embedded&product=' . $currentStockEntry->product_id) }}"> <a class="dropdown-item show-as-dialog-link permission-STOCK_TRANSFER @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="{{ $U('/transfer?embedded&product=' . $currentStockEntry->product_id) }}">
<span class="dropdown-item-icon"><i class="fas fa-exchange-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Transfer') }}</span> <span class="dropdown-item-icon"><i class="fas fa-exchange-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Transfer') }}</span>
</a> </a>
@endif @endif
<a class="dropdown-item show-as-dialog-link" type="button" href="{{ $U('/inventory?embedded&product=' . $currentStockEntry->product_id ) }}"> <a class="dropdown-item show-as-dialog-link permission-STOCK_CORRECTION" type="button" href="{{ $U('/inventory?embedded&product=' . $currentStockEntry->product_id ) }}">
<span class="dropdown-item-icon"><i class="fas fa-list"></i></span> <span class="dropdown-item-text">{{ $__t('Inventory') }}</span> <span class="dropdown-item-icon"><i class="fas fa-list"></i></span> <span class="dropdown-item-text">{{ $__t('Inventory') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@ -185,11 +185,11 @@
<a class="dropdown-item" type="button" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}"> <a class="dropdown-item" type="button" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
<span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Stock journal for this product') }}</span> <span class="dropdown-item-icon"><i class="fas fa-file-alt"></i></span> <span class="dropdown-item-text">{{ $__t('Stock journal for this product') }}</span>
</a> </a>
<a class="dropdown-item" type="button" href="{{ $U('/product/') }}{{ $currentStockEntry->product_id . '?returnto=%2Fstockoverview' }}"> <a class="dropdown-item permission-MASTER_DATA_EDIT" type="button" href="{{ $U('/product/') }}{{ $currentStockEntry->product_id . '?returnto=%2Fstockoverview' }}">
<span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit product') }}</span> <span class="dropdown-item-icon"><i class="fas fa-edit"></i></span> <span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item product-consume-button product-consume-button-spoiled @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#" <a class="dropdown-item product-consume-button product-consume-button-spoiled permission-PRODUCT_CONSUME @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ $currentStockEntry->product_name }}" data-product-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}" data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"

View File

@ -0,0 +1,34 @@
@extends('layout.default')
@section('title', $__t('Permissions for user %s', GetUserDisplayName($user)))
@section('activeNav', '')
@section('viewJsName', 'userpermissions')
@push('pageScripts')
<script>
Grocy.EditObjectId = {{ $user->id }};
</script>
@endpush
@section('content')
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
</div>
</div>
<hr>
<div class="row mt-3">
<div class="col">
<ul>
@foreach($permissions as $perm)
<li>
@include('components.userpermission_select', array(
'permission' => $perm
))
</li>
@endforeach
</ul>
<button id="permission-save" class="btn btn-success" type="submit">{{ $__t('Save') }}</button>
</div>
</div>
@endsection

View File

@ -47,6 +47,9 @@
<a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}"> <a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-info btn-sm" href="{{ $U('/user/' . $user->id . '/permissions') }}">
<i class="fas fa-lock"></i>
</a>
<a class="btn btn-danger btn-sm user-delete-button @if($user->id == GROCY_USER_ID) disabled @endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}"> <a class="btn btn-danger btn-sm user-delete-button @if($user->id == GROCY_USER_ID) disabled @endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>