@@ -16,30 +16,19 @@ final class ApiRequestMatcher implements RequestMatcherInterface
|
||||
{
|
||||
public function matches(Request $request): bool
|
||||
{
|
||||
// we do not want to handle URLs that
|
||||
// we do not want to handle URLs that are not in the API scope
|
||||
if (!str_starts_with($request->getRequestUri(), '/api/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// API documentation is only available to registered users
|
||||
// API documentation is only available to registered and logged-in users
|
||||
if (str_starts_with($request->getRequestUri(), '/api/doc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// let's use this firewall if a Bearer token is set in the header
|
||||
// other cases like "bearer" are rejected earlier
|
||||
if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// let's use this firewall if the deprecated username & token combination is available
|
||||
if ($request->headers->has(TokenAuthenticator::HEADER_USERNAME) &&
|
||||
$request->headers->has(TokenAuthenticator::HEADER_TOKEN)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// checking for a previous session allows us to skip the API firewall and token access handler
|
||||
// we simply re-use the existing session when doing API calls from the frontend
|
||||
// we simply re-use the existing session when doing API calls from the frontend.
|
||||
// it is not necessary to check headers. if there is no valid session, we should always use this firewall
|
||||
return !$request->hasPreviousSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,5 +56,5 @@ abstract class AbstractRoleCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
abstract protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void;
|
||||
abstract protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,13 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
if ($user->isSuperAdmin()) {
|
||||
$user->setSuperAdmin(false);
|
||||
$manipulator->saveUser($user);
|
||||
$userService->saveUser($user);
|
||||
$output->success(sprintf('Super administrator role has been removed from the user "%s".', $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" doesn\'t have the super administrator role.', $username));
|
||||
@@ -47,7 +47,7 @@ final class DemoteUserCommand extends AbstractRoleCommand
|
||||
} else {
|
||||
if ($user->hasRole($role)) {
|
||||
$user->removeRole($role);
|
||||
$manipulator->saveUser($user);
|
||||
$userService->saveUser($user);
|
||||
$output->success(sprintf('Role "%s" has been removed from user "%s".', $role, $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" didn\'t have "%s" role.', $username, $role));
|
||||
|
||||
@@ -33,13 +33,13 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
||||
);
|
||||
}
|
||||
|
||||
protected function executeRoleCommand(UserService $manipulator, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
protected function executeRoleCommand(UserService $userService, SymfonyStyle $output, User $user, bool $super, $role): void
|
||||
{
|
||||
$username = $user->getUserIdentifier();
|
||||
if ($super) {
|
||||
if (!$user->isSuperAdmin()) {
|
||||
$user->setSuperAdmin(true);
|
||||
$manipulator->saveUser($user);
|
||||
$userService->saveUser($user);
|
||||
$output->success(sprintf('User "%s" has been promoted as a super administrator.', $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" does already have the super administrator role.', $username));
|
||||
@@ -47,7 +47,7 @@ final class PromoteUserCommand extends AbstractRoleCommand
|
||||
} else {
|
||||
if (!$user->hasRole($role)) {
|
||||
$user->addRole($role);
|
||||
$manipulator->saveUser($user);
|
||||
$userService->saveUser($user);
|
||||
$output->success(sprintf('Role "%s" has been added to user "%s".', $role, $username));
|
||||
} else {
|
||||
$output->warning(sprintf('User "%s" did already have "%s" role.', $username, $role));
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.16.1';
|
||||
public const VERSION = '2.17.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 21601;
|
||||
public const VERSION_ID = 21700;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ContractController extends AbstractController
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->isGranted('contract_other_profile');
|
||||
$canChangeUser = $this->isGranted('hours_other_profile');
|
||||
$defaultDate = $dateTimeFactory->createStartOfYear();
|
||||
$now = $dateTimeFactory->createDateTime();
|
||||
|
||||
|
||||
@@ -58,7 +58,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[Route(path: '/admin/project')]
|
||||
final class ProjectController extends AbstractController
|
||||
{
|
||||
public function __construct(private ProjectRepository $repository, private SystemConfiguration $configuration, private EventDispatcherInterface $dispatcher, private ProjectService $projectService)
|
||||
public function __construct(
|
||||
private readonly ProjectRepository $repository,
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ProjectService $projectService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -480,11 +485,17 @@ final class ProjectController extends AbstractController
|
||||
|
||||
$csrfTokenManager->refreshToken('project.duplicate');
|
||||
|
||||
$newProject = $projectDuplicationService->duplicate($project, $project->getName() . ' [COPY]');
|
||||
try {
|
||||
$newProject = $projectDuplicationService->duplicate($project, $project->getName() . ' [COPY]');
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
$this->flashSuccess('action.update.success');
|
||||
return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->logException($ex);
|
||||
$this->flashError('action.update.error', 'Failed to copy project: ' . $ex->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $newProject->getId()]);
|
||||
return $this->redirectToRoute('admin_project');
|
||||
}
|
||||
|
||||
#[Route(path: '/{id}/delete', name: 'admin_project_delete', methods: ['GET', 'POST'])]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\PrepareUserEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
@@ -29,7 +28,6 @@ use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -41,7 +39,10 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('view_user')]
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
public function __construct(private UserPasswordHasherInterface $passwordHasher, private UserRepository $repository, private EventDispatcherInterface $dispatcher)
|
||||
public function __construct(
|
||||
private readonly UserRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -100,37 +101,23 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function createNewDefaultUser(SystemConfiguration $config): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
$user->setTimezone($config->getUserDefaultTimezone());
|
||||
$user->setLanguage($config->getUserDefaultLanguage());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
#[Route(path: '/create', name: 'admin_user_create', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('create_user')]
|
||||
public function createAction(Request $request, SystemConfiguration $config, UserRepository $userRepository, EventDispatcherInterface $dispatcher): Response
|
||||
public function createAction(Request $request, UserService $userService, EventDispatcherInterface $dispatcher): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$user = $userService->createNewUser();
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$password = $this->passwordHasher->hashPassword($user, $user->getPlainPassword());
|
||||
$user->setPassword($password);
|
||||
|
||||
$userRepository->saveUser($user);
|
||||
$userService->saveUser($user);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
try {
|
||||
$event = new PrepareUserEvent($user, false);
|
||||
$dispatcher->dispatch($event);
|
||||
$userRepository->saveUser($user);
|
||||
$this->repository->saveUser($user);
|
||||
} catch (\Exception $ex) {
|
||||
// it should be no problem, if creating default user preferences fails
|
||||
}
|
||||
|
||||
@@ -481,6 +481,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
$this->addTeam($team);
|
||||
}
|
||||
|
||||
$this->number = null;
|
||||
$currentMeta = $this->meta;
|
||||
$this->meta = new ArrayCollection();
|
||||
/** @var ProjectMeta $meta */
|
||||
|
||||
@@ -71,7 +71,7 @@ class Tag
|
||||
|
||||
public function setName(?string $tagName): Tag
|
||||
{
|
||||
$this->name = $tagName;
|
||||
$this->name = $tagName !== null ? trim($tagName) : $tagName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
private ?string $description = null;
|
||||
#[ORM\Column(name: 'rate', type: 'float', nullable: false)]
|
||||
#[Assert\GreaterThanOrEqual(0)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private float $rate = 0.00;
|
||||
@@ -224,8 +225,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
/**
|
||||
* Get entry id, returns null for new entities which were not persisted.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -282,10 +281,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
return $this->end === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $end
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setEnd(?DateTime $end): Timesheet
|
||||
{
|
||||
$this->end = $end;
|
||||
@@ -300,10 +295,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $duration
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setDuration(?int $duration): Timesheet
|
||||
{
|
||||
$this->duration = $duration;
|
||||
@@ -313,9 +304,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
/**
|
||||
* Do not rely on the results of this method for running records.
|
||||
*
|
||||
* @param bool $calculate
|
||||
* @return int|null
|
||||
*/
|
||||
public function getDuration(bool $calculate = true): ?int
|
||||
{
|
||||
@@ -384,11 +372,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $rate
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setRate($rate): Timesheet
|
||||
public function setRate(float $rate): Timesheet
|
||||
{
|
||||
$this->rate = $rate;
|
||||
|
||||
@@ -454,18 +438,11 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExported(): bool
|
||||
{
|
||||
return $this->exported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $exported
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setExported(bool $exported): Timesheet
|
||||
{
|
||||
$this->exported = $exported;
|
||||
@@ -473,9 +450,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTimezone(): ?string
|
||||
{
|
||||
return $this->timezone;
|
||||
@@ -486,8 +460,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
* It is reserved for some very rare use-cases.
|
||||
*
|
||||
* @internal
|
||||
* @param string $timezone
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setTimezone(string $timezone): Timesheet
|
||||
{
|
||||
@@ -498,8 +470,6 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
/**
|
||||
* This method returns ALWAYS: "timesheet"
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
@@ -635,6 +605,10 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
// this needs to be done, otherwise doctrine will not see the item as changed
|
||||
// and the calculators will not run
|
||||
$this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
$meta->setEntity($this);
|
||||
$this->meta->add($meta);
|
||||
|
||||
@@ -141,6 +141,11 @@ class PageActionsEvent extends ThemeEvent
|
||||
$this->payload['actions'][$key] = null;
|
||||
}
|
||||
|
||||
public function addQuickImport(string $url): void
|
||||
{
|
||||
$this->addAction('import', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'import', 'icon' => 'upload', 'translation_domain' => 'actions']);
|
||||
}
|
||||
|
||||
public function addQuickExport(string $url): void
|
||||
{
|
||||
$this->addAction('download', ['url' => $url, 'class' => 'toolbar-action', 'title' => 'export']);
|
||||
|
||||
@@ -94,7 +94,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$contract = new MenuItemModel('contract', 'work_contract', null, [], 'contract');
|
||||
if ($user->hasContractSettings() || $auth->isGranted('contract_other_profile')) {
|
||||
if ($user->hasContractSettings() || $auth->isGranted('hours_other_profile')) {
|
||||
$contract->addChild(new MenuItemModel('contract_status', 'work_times', 'user_contract', [], 'work_times'));
|
||||
}
|
||||
|
||||
|
||||
@@ -45,29 +45,47 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
|
||||
*
|
||||
* @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
*
|
||||
* @param string|null $value
|
||||
* @param array<string>|string|null $value
|
||||
* @return Tag[]
|
||||
* @throws TransformationFailedException
|
||||
*/
|
||||
public function reverseTransform(mixed $value): mixed
|
||||
public function reverseTransform(mixed $value): array
|
||||
{
|
||||
// check for empty tag list
|
||||
if ('' === $value || null === $value) {
|
||||
return [];
|
||||
}
|
||||
$names = array_filter(array_unique(array_map('trim', explode(',', $value))));
|
||||
if (!\is_array($value)) {
|
||||
$names = array_filter(array_unique(array_map('trim', explode(',', $value))));
|
||||
} else {
|
||||
$names = $value;
|
||||
}
|
||||
|
||||
// get the current tags and find the new ones that should be created
|
||||
$tags = $this->tagRepository->findBy(['name' => $names]);
|
||||
if ($this->create) {
|
||||
// works, because of the implicit case: (string) $tag
|
||||
$newNames = array_diff($names, $tags);
|
||||
$tags = [];
|
||||
foreach ($names as $tagName) {
|
||||
if ($tagName === null || $tagName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($newNames as $name) {
|
||||
$tagName = trim($tagName);
|
||||
$tag = null;
|
||||
|
||||
if (is_numeric($tagName)) {
|
||||
$tag = $this->tagRepository->find($tagName);
|
||||
}
|
||||
|
||||
if ($tag === null) {
|
||||
$tag = $this->tagRepository->findTagByName($tagName);
|
||||
}
|
||||
|
||||
// get the current tags and find the new ones that should be created
|
||||
if ($this->create && $tag === null) {
|
||||
$tag = new Tag();
|
||||
$tag->setName(mb_substr($name, 0, 100));
|
||||
$tag->setName(mb_substr($tagName, 0, 100));
|
||||
$this->tagRepository->saveTag($tag);
|
||||
}
|
||||
|
||||
if ($tag !== null) {
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ trait FormTrait
|
||||
return;
|
||||
}
|
||||
|
||||
$options['projects'] = $data['project'];
|
||||
$options['projects'] = \is_string($data['project']) ? (int) $data['project'] : $data['project'];
|
||||
|
||||
$event->getForm()->add('activity', ActivityType::class, $options);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ trait ToolbarFormTrait
|
||||
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class);
|
||||
$builder->add('size', PageSizeType::class);
|
||||
}
|
||||
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder): void
|
||||
|
||||
@@ -36,13 +36,15 @@ final class TagsSelectType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
if (!$options['allow_create']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
|
||||
/** @var array<string> $tagIds */
|
||||
$tagIds = $event->getData();
|
||||
|
||||
// this is mainly here, because the link from tags index page uses the non-array syntax
|
||||
if (\is_string($tagIds) || \is_int($tagIds)) {
|
||||
$tagIds = array_filter(array_unique(array_map('trim', explode(',', $tagIds))));
|
||||
}
|
||||
|
||||
if (!\is_array($tagIds)) {
|
||||
return;
|
||||
}
|
||||
@@ -59,13 +61,15 @@ final class TagsSelectType extends AbstractType
|
||||
$tag = $this->tagRepository->findTagByName($tagId);
|
||||
}
|
||||
|
||||
if ($tag === null) {
|
||||
if ($options['allow_create'] && $tag === null) {
|
||||
$tag = new Tag();
|
||||
$tag->setName(mb_substr($tagId, 0, 100));
|
||||
$tag->setName($tagId);
|
||||
$this->tagRepository->saveTag($tag);
|
||||
}
|
||||
|
||||
$tags[] = $tag->getId();
|
||||
if ($tag !== null) {
|
||||
$tags[] = $tag->getId();
|
||||
}
|
||||
}
|
||||
|
||||
$event->setData($tags);
|
||||
@@ -79,6 +83,9 @@ final class TagsSelectType extends AbstractType
|
||||
'class' => Tag::class,
|
||||
'label' => 'tag',
|
||||
'allow_create' => false,
|
||||
'choice_value' => function (Tag $tag) {
|
||||
return $tag->getId();
|
||||
},
|
||||
'choice_attr' => function (Tag $tag) {
|
||||
$color = $tag->getColor();
|
||||
if ($color === null) {
|
||||
|
||||
@@ -60,7 +60,7 @@ class Kernel extends BaseKernel
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->environment === 'test' && getenv('TEST_WITH_BUNDLES') === false) {
|
||||
if ($this->environment === 'test') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ class Kernel extends BaseKernel
|
||||
$loader->load($file->getPathname());
|
||||
}
|
||||
|
||||
if (is_file($confDir . '/packages/local.yaml')) {
|
||||
if ($this->environment !== 'test' && is_file($confDir . '/packages/local.yaml')) {
|
||||
$loader->load($confDir . '/packages/local.yaml');
|
||||
}
|
||||
$loader->load($confDir . '/services' . self::CONFIG_EXTS, 'glob');
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
|
||||
namespace App\Project;
|
||||
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectRate;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
@@ -19,10 +17,10 @@ use App\Repository\ProjectRateRepository;
|
||||
final class ProjectDuplicationService
|
||||
{
|
||||
public function __construct(
|
||||
private ProjectService $projectService,
|
||||
private ActivityRepository $activityRepository,
|
||||
private ProjectRateRepository $projectRateRepository,
|
||||
private ActivityRateRepository $activityRateRepository
|
||||
private readonly ProjectService $projectService,
|
||||
private readonly ActivityRepository $activityRepository,
|
||||
private readonly ProjectRateRepository $projectRateRepository,
|
||||
private readonly ActivityRateRepository $activityRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -30,6 +28,7 @@ final class ProjectDuplicationService
|
||||
{
|
||||
$newProject = clone $project;
|
||||
$newProject->setName($newName);
|
||||
$newProject->setNumber($this->projectService->calculateNextProjectNumber());
|
||||
|
||||
foreach ($project->getTeams() as $team) {
|
||||
$newProject->addTeam($team);
|
||||
@@ -49,7 +48,6 @@ final class ProjectDuplicationService
|
||||
$this->projectService->saveNewProject($newProject);
|
||||
|
||||
foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) {
|
||||
/** @var ProjectRate $newRate */
|
||||
$newRate = clone $rate;
|
||||
$newRate->setProject($newProject);
|
||||
$this->projectRateRepository->saveRate($newRate);
|
||||
@@ -68,7 +66,6 @@ final class ProjectDuplicationService
|
||||
$this->activityRepository->saveActivity($newActivity);
|
||||
|
||||
foreach ($this->activityRateRepository->getRatesForActivity($activity) as $rate) {
|
||||
/** @var ActivityRate $newRate */
|
||||
$newRate = clone $rate;
|
||||
$newRate->setActivity($newActivity);
|
||||
$this->activityRateRepository->saveRate($newRate);
|
||||
|
||||
@@ -112,7 +112,7 @@ final class ProjectService
|
||||
return $this->repository->findOneBy(['number' => $number]);
|
||||
}
|
||||
|
||||
private function calculateNextProjectNumber(): ?string
|
||||
public function calculateNextProjectNumber(): ?string
|
||||
{
|
||||
$format = $this->configuration->find('project.number_format');
|
||||
if (empty($format) || !\is_string($format)) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class BaseQuery
|
||||
/** @var array<string, string|int|null|bool|array<mixed>|DateRange> */
|
||||
private array $defaults = [
|
||||
'page' => 1,
|
||||
'pageSize' => self::DEFAULT_PAGESIZE,
|
||||
'size' => self::DEFAULT_PAGESIZE,
|
||||
'orderBy' => 'id',
|
||||
'order' => self::ORDER_ASC,
|
||||
'searchTerm' => null,
|
||||
@@ -137,6 +137,16 @@ class BaseQuery
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->getPageSize();
|
||||
}
|
||||
|
||||
public function setSize(?int $size): void
|
||||
{
|
||||
$this->setPageSize($size);
|
||||
}
|
||||
|
||||
public function getOrderBy(): string
|
||||
{
|
||||
return $this->orderBy;
|
||||
|
||||
@@ -53,6 +53,8 @@ final class SessionHandler extends PdoSessionHandler
|
||||
if (false === $limit->isAccepted()) {
|
||||
throw new BadRequestHttpException('Too many requests with invalid Session ID. Prediction attack?');
|
||||
}
|
||||
|
||||
usleep(250000); // slow down potential attacks
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -60,6 +60,7 @@ class UserService
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
$user->setRoles([User::DEFAULT_ROLE]);
|
||||
$user->setTimezone($this->configuration->getUserDefaultTimezone());
|
||||
$user->setLanguage($this->configuration->getUserDefaultLanguage());
|
||||
$user->setPreferenceValue(UserPreference::SKIN, $this->configuration->getUserDefaultTheme());
|
||||
@@ -92,6 +93,7 @@ class UserService
|
||||
|
||||
$this->hashPassword($user);
|
||||
$this->hashApiToken($user);
|
||||
$user->eraseCredentials();
|
||||
|
||||
$this->dispatcher->dispatch(new UserCreatePreEvent($user)); // @CloudRequired
|
||||
$this->repository->saveUser($user);
|
||||
@@ -120,6 +122,7 @@ class UserService
|
||||
|
||||
$this->hashPassword($user);
|
||||
$this->hashApiToken($user);
|
||||
$user->eraseCredentials();
|
||||
|
||||
$this->dispatcher->dispatch(new UserUpdatePreEvent($user));
|
||||
$this->repository->saveUser($user);
|
||||
@@ -179,7 +182,6 @@ class UserService
|
||||
|
||||
$password = $this->passwordHasher->hashPassword($user, $plain);
|
||||
$user->setPassword($password);
|
||||
$user->eraseCredentials();
|
||||
}
|
||||
|
||||
private function hashApiToken(User $user): void
|
||||
@@ -192,7 +194,6 @@ class UserService
|
||||
|
||||
$password = $this->passwordHasher->hashPassword($user, $plain);
|
||||
$user->setApiToken($password);
|
||||
$user->eraseCredentials();
|
||||
}
|
||||
|
||||
public function deleteUser(User $delete, ?User $replace = null): void
|
||||
|
||||
@@ -35,6 +35,7 @@ final class UserVoter extends Voter
|
||||
'hourly-rate',
|
||||
'view_team_member',
|
||||
'contract',
|
||||
'hours',
|
||||
'supervisor',
|
||||
];
|
||||
|
||||
@@ -73,6 +74,10 @@ final class UserVoter extends Voter
|
||||
return $this->permissionManager->hasRolePermission($user, 'contract_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'hours') {
|
||||
return $this->permissionManager->hasRolePermission($user, 'hours_other_profile');
|
||||
}
|
||||
|
||||
if ($attribute === 'access_user') {
|
||||
return $user->canSeeUser($subject);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user