[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\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

View File

@ -42,3 +42,9 @@ file_filter = localization/<lang>/userfield_types.po
source_file = localization/userfield_types.pot
source_lang = en
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
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, false, false);
$errorMiddleware = $app->addErrorMiddleware(true, false, false);
$errorMiddleware->setDefaultErrorHandler(
new \Grocy\Controllers\ExceptionController($app, $container)
);
$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
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

View File

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

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class BatteriesApiController extends BaseApiController
{
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)
{
User::checkPermission($request, User::PERMISSION_BATTERY_TRACK_CHARGE_CYCLE);
$requestBody = $request->getParsedBody();
try
@ -49,6 +53,8 @@ class BatteriesApiController extends BaseApiController
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
{
$this->ApiResponse($response, $this->getBatteriesService()->UndoChargeCycle($args['chargeCycleId']));

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class ChoresApiController extends BaseApiController
{
public function __construct(\DI\Container $container)
@ -15,6 +17,8 @@ class ChoresApiController extends BaseApiController
try
{
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'])))
{
@ -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,6 +63,8 @@ class ChoresApiController extends BaseApiController
{
try
{
User::checkPermission($request, User::PERMISSION_CHORE_UNDO);
$this->ApiResponse($response, $this->getChoresService()->UndoChoreExecution($args['executionId']));
return $this->EmptyApiResponse($response);
}
@ -70,6 +78,8 @@ class ChoresApiController extends BaseApiController
{
try
{
User::checkPermission($request, User::PERMISSION_CHORE_EDIT);
$requestBody = $request->getParsedBody();
$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;
use Grocy\Controllers\Users\User;
use \Grocy\Services\FilesService;
class FilesApiController extends BaseApiController
@ -15,6 +16,8 @@ class FilesApiController extends BaseApiController
{
try
{
User::checkPermission($request, User::PERMISSION_UPLOAD_FILE);
if (IsValidFileName(base64_decode($args['fileName'])))
{
$fileName = base64_decode($args['fileName']);
@ -97,6 +100,8 @@ class FilesApiController extends BaseApiController
{
try
{
User::checkPermission($request, User::PERMISSION_DELETE_FILE);
if (IsValidFileName(base64_decode($args['fileName'])))
{
$fileName = base64_decode($args['fileName']);

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class GenericEntityApiController extends BaseApiController
{
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)
{
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity']))
{
$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)
{
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity']))
{
$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)
{
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
if ($this->IsValidEntity($args['entity']))
{
$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)
{
if ($this->IsValidEntity($args['entity']) && !$this->IsEntityWithPreventedListing($args['entity']))
{
try
@ -172,6 +181,8 @@ class GenericEntityApiController extends BaseApiController
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();
try

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class RecipesApiController extends BaseApiController
{
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)
{
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
$requestBody = $request->getParsedBody();
$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)
{
User::checkPermission($request, User::PERMISSION_PRODUCT_CONSUME);
try
{
$this->getRecipesService()->ConsumeRecipe($args['recipeId']);

View File

@ -2,6 +2,7 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
use \Grocy\Services\StockService;
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)
{
User::checkPermission($request, User::PERMISSION_PRODUCT_PURCHASE);
$requestBody = $request->getParsedBody();
try
@ -136,6 +139,8 @@ class StockApiController extends BaseApiController
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();
try
@ -185,6 +190,8 @@ class StockApiController extends BaseApiController
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();
try
@ -239,6 +246,8 @@ class StockApiController extends BaseApiController
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();
$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)
{
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
$requestBody = $request->getParsedBody();
try
@ -372,6 +383,8 @@ class StockApiController extends BaseApiController
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();
try
@ -439,6 +452,8 @@ class StockApiController extends BaseApiController
public function AddMissingProductsToShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
try
{
$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)
{
User::checkPermission($request, User::PERMISSION_STOCK_CORRECTION);
try
{
$this->ApiResponse($response, $this->getStockService()->UndoTransaction($args['transactionId']));

View File

@ -2,6 +2,8 @@
namespace Grocy\Controllers;
use Grocy\Controllers\Users\User;
class TasksApiController extends BaseApiController
{
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)
{
User::checkPermission($request, User::PERMISSION_TASKS_MARK_COMPLETED);
$requestBody = $request->getParsedBody();
try
@ -37,6 +41,8 @@ class TasksApiController extends BaseApiController
public function UndoTask(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
User::checkPermission($request, User::PERMISSION_TASKS_UNDO);
try
{
$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;
use Grocy\Controllers\Users\User;
class UsersApiController extends BaseApiController
{
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)
{
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,6 +47,7 @@ class UsersApiController extends BaseApiController
public function DeleteUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
User::checkPermission($request, User::PERMISSION_EDIT_USER);
try
{
$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)
{
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());
}
}
}

View File

@ -2,10 +2,13 @@
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)
{
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')
{
User::checkPermission($request, User::PERMISSION_CREATE_USER);
return $this->renderPage($response, 'userform', [
'mode' => 'create'
]);
}
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', [
'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']),
]);
}
}

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"
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
$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
if (GROCY_FEATURE_FLAG_STOCK)
@ -168,6 +169,9 @@ $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
$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/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"));

View File

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

View File

@ -20,7 +20,7 @@
<hr>
<div class="row mt-3">
<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') }}
</a>
</div>
@ -57,10 +57,10 @@
@foreach($batteries as $battery)
<tr>
<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>
</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>
</a>
</td>

View File

@ -50,7 +50,7 @@
@foreach($chargeCycles as $chargeCycleEntry)
<tr id="charge-cycle-{{ $chargeCycleEntry->id }}-row" class="@if($chargeCycleEntry->undone == 1) text-muted @endif">
<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>
</a>
</td>

View File

@ -69,7 +69,7 @@
@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">
<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-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name }}">
<i class="fas fa-fire"></i>
@ -85,7 +85,7 @@
<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>
</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>
</a>
</div>

View File

@ -53,7 +53,7 @@
@foreach($choresLog as $choreLogEntry)
<tr id="chore-execution-{{ $choreLogEntry->id }}-row" class="@if($choreLogEntry->undone == 1) text-muted @endif">
<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>
</a>
</td>

View File

@ -95,7 +95,7 @@
@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">
<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-name="{{ FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->name }}">
<i class="fas fa-play"></i>
@ -111,7 +111,7 @@
<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>
</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>
</a>
</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)
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)
<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') }}">
<i class="fas fa-shopping-cart"></i>
<span class="nav-link-text">{{ $__t('Purchase') }}</span>
</a>
</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') }}">
<i class="fas fa-utensils"></i>
<span class="nav-link-text">{{ $__t('Consume') }}</span>
</a>
</li>
@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') }}">
<i class="fas fa-exchange-alt"></i>
<span class="nav-link-text">{{ $__t('Transfer') }}</span>
</a>
</li>
@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') }}">
<i class="fas fa-list"></i>
<span class="nav-link-text">{{ $__t('Inventory') }}</span>
@ -191,7 +192,7 @@
</li>
@endif
@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') }}">
<i class="fas fa-play"></i>
<span class="nav-link-text">{{ $__t('Chore tracking') }}</span>
@ -199,7 +200,7 @@
</li>
@endif
@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') }}">
<i class="fas fa-fire"></i>
<span class="nav-link-text">{{ $__t('Battery tracking') }}</span>
@ -417,7 +418,7 @@
@endif
<div class="dropdown-divider"></div>
@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>
<a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $__t('Manage API keys') }}</a>
@endif

View File

@ -121,14 +121,14 @@
@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">
<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-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
data-consume-amount="1">
<i class="fas fa-utensils"></i> 1
</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-name="{{ $currentStockEntry->product_name }}"
data-product-qu-name="{{ $currentStockEntry->qu_unit_name }}"
@ -156,22 +156,22 @@
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>
</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>
</a>
<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>
</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>
</a>
@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>
</a>
@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>
</a>
<div class="dropdown-divider"></div>
@ -185,11 +185,11 @@
<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>
</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>
</a>
<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-name="{{ $currentStockEntry->product_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 }}">
<i class="fas fa-edit"></i>
</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 }}">
<i class="fas fa-trash"></i>
</a>