Improve create and start permission handling (#613)
This commit is contained in:
@@ -211,17 +211,6 @@ class TimesheetController extends BaseApiController
|
||||
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd()) {
|
||||
$this->repository->stopActiveEntries(
|
||||
$timesheet->getUser(),
|
||||
@@ -289,17 +278,6 @@ class TimesheetController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
if ($form->has('duration')) {
|
||||
$duration = $form->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $form->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($timesheet);
|
||||
$entityManager->flush();
|
||||
|
||||
@@ -97,18 +97,6 @@ trait TimesheetControllerTrait
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
if ($editForm->has('duration')) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $editForm->getData();
|
||||
$duration = $editForm->get('duration')->getData();
|
||||
$end = null;
|
||||
if ($duration > 0) {
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
}
|
||||
$record->setEnd($end);
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($entry);
|
||||
$entityManager->flush();
|
||||
@@ -174,17 +162,6 @@ trait TimesheetControllerTrait
|
||||
$createForm->handleRequest($request);
|
||||
|
||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||
if ($createForm->has('duration')) {
|
||||
$duration = $createForm->get('duration')->getData();
|
||||
if ($duration > 0) {
|
||||
/** @var Timesheet $record */
|
||||
$record = $createForm->getData();
|
||||
$end = clone $record->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
$record->setEnd($end);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
try {
|
||||
|
||||
@@ -90,8 +90,10 @@ class Configuration implements ConfigurationInterface
|
||||
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
|
||||
if (class_exists($class)) {
|
||||
$rounding = new $class();
|
||||
|
||||
return !($rounding instanceof RoundingInterface);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->thenInvalid('Chosen rounding mode is invalid')
|
||||
|
||||
@@ -117,7 +117,38 @@ class TimesheetEditForm extends AbstractType
|
||||
if ($options['duration_only']) {
|
||||
$builder->add('duration', DurationType::class, [
|
||||
'required' => false,
|
||||
'docu_chapter' => 'timesheet.html#duration-format',
|
||||
'attr' => [
|
||||
'placeholder' => '00:00',
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::POST_SET_DATA,
|
||||
function (FormEvent $event) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
if (null === $data->getEnd()) {
|
||||
$event->getForm()->get('duration')->setData(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// make sure that duration is mapped back to end field
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
/** @var Timesheet $data */
|
||||
$data = $event->getData();
|
||||
$duration = $data->getDuration();
|
||||
$end = null;
|
||||
if (null !== $duration) {
|
||||
$end = clone $data->getBegin();
|
||||
$end->modify('+ ' . $duration . 'seconds');
|
||||
}
|
||||
$data->setEnd($end);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$builder->add('end', DateTimePickerType::class, [
|
||||
'label' => 'label.end',
|
||||
@@ -270,7 +301,7 @@ class TimesheetEditForm extends AbstractType
|
||||
'include_user' => false,
|
||||
'include_exported' => false,
|
||||
'include_rate' => true,
|
||||
'docu_chapter' => 'timesheet',
|
||||
'docu_chapter' => 'timesheet.html',
|
||||
'method' => 'POST',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -73,12 +73,18 @@ class DurationType extends AbstractType
|
||||
}
|
||||
},
|
||||
function ($formatToInt) use ($formatter, $pattern) {
|
||||
if (null === $formatToInt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($formatToInt)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!preg_match($pattern, $formatToInt)) {
|
||||
throw new TransformationFailedException('Invalid duration format given');
|
||||
}
|
||||
|
||||
try {
|
||||
return $formatter->parseDurationString($formatToInt);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -55,7 +55,7 @@ class InvoiceRendererType extends AbstractType
|
||||
return $choiceValue;
|
||||
},
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'docu_chapter' => 'invoices',
|
||||
'docu_chapter' => 'invoices.html',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
55
src/Security/CurrentUser.php
Normal file
55
src/Security/CurrentUser.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class CurrentUser
|
||||
{
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @param TokenStorageInterface $storage
|
||||
* @param UserRepository $repository
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $storage, UserRepository $repository)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
if (null === $this->storage->getToken()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->storage->getToken()->getUser();
|
||||
|
||||
if (!($user instanceof User)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->repository->getById($user->getId());
|
||||
}
|
||||
}
|
||||
@@ -33,5 +33,4 @@ interface RoundingInterface
|
||||
* @param $minutes
|
||||
*/
|
||||
public function roundDuration(Timesheet $record, $minutes);
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Timesheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use App\Security\CurrentUser;
|
||||
|
||||
class UserDateTimeFactory
|
||||
{
|
||||
@@ -20,18 +20,13 @@ class UserDateTimeFactory
|
||||
protected $timezone;
|
||||
|
||||
/**
|
||||
* @param TokenStorageInterface $tokenStorage
|
||||
* @param CurrentUser $user
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $tokenStorage)
|
||||
public function __construct(CurrentUser $user)
|
||||
{
|
||||
if (null === $tokenStorage->getToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @var $user User */
|
||||
$user = $tokenStorage->getToken()->getUser();
|
||||
$timezone = date_default_timezone_get();
|
||||
|
||||
$user = $user->getUser();
|
||||
if ($user instanceof User && null !== $user->getPreferenceValue('timezone')) {
|
||||
$timezone = $user->getPreferenceValue('timezone');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Utils\Duration;
|
||||
use App\Utils\LocaleSettings;
|
||||
@@ -121,6 +122,7 @@ class Extensions extends \Twig_Extension
|
||||
new TwigFilter('currency', [$this, 'currency']),
|
||||
new TwigFilter('country', [$this, 'country']),
|
||||
new TwigFilter('icon', [$this, 'icon']),
|
||||
new TwigFilter('docu_link', [$this, 'documentationLink']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -247,6 +249,15 @@ class Extensions extends \Twig_Extension
|
||||
return self::$icons[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function documentationLink($url = '')
|
||||
{
|
||||
return Constants::HOMEPAGE . '/documentation/' . $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $amount
|
||||
* @param string $currency
|
||||
|
||||
@@ -27,6 +27,7 @@ class Timesheet extends Constraint
|
||||
public const DISABLED_ACTIVITY_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d87';
|
||||
public const DISABLED_PROJECT_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d88';
|
||||
public const DISABLED_CUSTOMER_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d89';
|
||||
public const START_DISALLOWED = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d90';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
|
||||
@@ -38,6 +39,7 @@ class Timesheet extends Constraint
|
||||
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
|
||||
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
|
||||
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
|
||||
self::START_DISALLOWED => 'You are not allowed to start this timesheet record.',
|
||||
];
|
||||
|
||||
public $message = 'This timesheet has invalid settings.';
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
@@ -18,17 +19,29 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class TimesheetValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $auth;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rules = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $durationOnly = false;
|
||||
|
||||
/**
|
||||
* @param AuthorizationCheckerInterface $auth
|
||||
* @param array $ruleset
|
||||
* @param bool $durationOnly
|
||||
*/
|
||||
public function __construct(array $ruleset)
|
||||
public function __construct(AuthorizationCheckerInterface $auth, array $ruleset, bool $durationOnly)
|
||||
{
|
||||
$this->auth = $auth;
|
||||
$this->rules = $ruleset;
|
||||
$this->durationOnly = $durationOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +74,31 @@ class TimesheetValidator extends ConstraintValidator
|
||||
|
||||
$this->validateBeginAndEnd($value, $this->context);
|
||||
$this->validateActivityAndProject($value, $this->context);
|
||||
$this->validatePermissions($value, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validatePermissions(Timesheet $timesheet, ExecutionContextInterface $context)
|
||||
{
|
||||
// special case that would otherwise need to be validated in several controllers:
|
||||
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
|
||||
// which might be disallowed for the current user
|
||||
if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) {
|
||||
if (!$this->auth->isGranted('start', $timesheet)) {
|
||||
$context->buildViolation('You are not allowed to start this timesheet record.')
|
||||
->atPath($this->durationOnly ? 'duration' : 'end')
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(TimesheetConstraint::START_DISALLOWED)
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check active entries against hard_limit
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user